Mac Storyboards...get a reference to "main window controller" from the App Delegate. Shoebox app getting multiple instances.

I decided to give storyboards on Mac a try again in this little project. Typically, if my app has all windows closed and the user clicks the icon in the dock, I have my app show the "main window" in my app.


This little project is a shoebox app. Normally, I'd have my app delegate create the main window controller in app did finish launching. Something like this:


-(void)applicationDidFinishLaunching:(NSNotification*)aNotification
{
  self.windowController = [[WindowContorller alloc]init];
   [self.windowController showWindow:nil];
}


Then in -(BOOL)applicationShouldHandleReopen:(NSApplication*)sender hasVisibleWindows:(BOOL)flag, if necessary, call showWindow on main window controller again.


In a storyboard, you cannot connect an outlet from the App delegate scene to the initial window controller. So I'm instantiating it programmatically:


-(void)applicationDidFinishLaunching:(NSNotification*)aNotification
{
    NSStoryboard *storyboard = [NSStoryboard storyboardWithName:@"Main" bundle:nil];
    self.windowController = [storyboard instantiateInitialController];

}


Then in applicationShouldHandleReopen:hasVisibleWindows: I do what I normally do. For whatever reason, I am sometimes getting multiple instances of my initial window controller. I changed the presentation to "Single" on the window controller in the storyboard but I'm still getting multiple instances.

Answered by QuinceyMorris in 227363022

"Single" vs. "Multiple":


In a NSDocument-based app, you want to create a new window controller (and window) for each document that's opened, so you want "multiple" instantiation in the storyboard.


In a shoebox app, you want always to get back to the main window, so you want "single" instantiation in the storyboard. In this case, when something causes instantiation of the window scene, the storyboard checks whether the window exists and is or isn't visible, and either (re-)creates it or show it accordingly. (The behavior is a little bit broken on macOS, at least with NSPanel windows, but the principle still applies.)


>> Or do you always have to turn off the the initial controller for every project?


The point of an initial controller is just an automatic instantiation when the storyboard is instantiated through the parameterless method. In the special case of the main storyboard (the one that's referred to by the info.plist file, or equivalently the mina storyboard setting on the project's General tab in Xcode), the storyboard is also instantiated automatically, giving the automatic window controller instantion also for free. It's basically a convenience, and it's fine (if unnecessary, mostly) to arrange the code to do either of these things manually if you prefer.


Back in Feb, your original problem was just that you were instantiating manually when it was already being done automatically as a convenience. As you discovered, you just needed to do one or the other, not both.


However, instead of turning off the automatic behavior in the main storyboard, I would recommending changing the workflow to leave the app delegate out of it (and eliminate its "windowController" property). Instead, in your window controller subclass, create a static property called something like "shared" that's the main window controller, and set it to the actual window controller in windowDidLoad (preferably) or the designated window controller init (if you have to). You can declare this property get/set(private), so that it's read-only outside the window controller subclass. Anything that current goes to the app delegate "windowController" property can go to WindowController.shared instead.


Alternatively…


It's not ridiculous to take your window scene (and its related view subscenes) out of the main storyboard, and putting them in their own storyboard. This is perhaps especially appropriate if you have multiple window types in your shoebox app, and you want to use a different storyboard for each window, to keep the IB complexity down a bit.


In that case, you would manually instantiate the various storyboards as needed, presumably the main one in "applicationDidFinishLaunching:" and the other ones as the result of menu item actions, with the window controller scenes set as "single" and "initial controller" in each storyboard. (Then you can use the simple storyboard instantiation function and don't need to know the storyboard IDs in your code.)


Finally…


In a shoebox app, you should typically not have a "close" function for the main window. (There's a checkbox for this in the window attribute inspector in IB.) Otherwise, closing the window gives you an app whose entire UI is its menu bar, and users find that extremely confusing. Better to keep the main window open, and all of the re-instantiation issues just go away as a side benefit.


Does that cover everything? I think so.

Also changed the windows content view controller, (the one wired up as the window controller's "content" segue) to have a presentation of "Single". But still will get multple instances on next launch.

If I uncheck "Initial controller" on the window controller in the storyboard and just give it an identifier, then in app did finish launching :


NSStoryboard *storyboard = [NSStoryboard storyboardWithName:@"Main" bundle:nil];
self.windowController = [storyboard instantiateControllerWithIdentifier:@"idhere"];
[self.windowController showWindow:nil];


I can avoid having multiple instances. Is that how it's supposed to be done? Or am I missing something?

Just did a little storyboard demo again.


Still wondering about this. Is there anyway the app delegate can get access to the initial controller the storyboard creates *automatically* in app did finish launching? Calling instantiateInitiaController and assigning it to a property in app did finish launching, results in two instances of the controller being created.


Or do you always have to turn off the the initial controller for every project? What is the significance of "Single" or "Multiple" presentation styles? Seems like a quirky way to set up the default template?

Accepted Answer

"Single" vs. "Multiple":


In a NSDocument-based app, you want to create a new window controller (and window) for each document that's opened, so you want "multiple" instantiation in the storyboard.


In a shoebox app, you want always to get back to the main window, so you want "single" instantiation in the storyboard. In this case, when something causes instantiation of the window scene, the storyboard checks whether the window exists and is or isn't visible, and either (re-)creates it or show it accordingly. (The behavior is a little bit broken on macOS, at least with NSPanel windows, but the principle still applies.)


>> Or do you always have to turn off the the initial controller for every project?


The point of an initial controller is just an automatic instantiation when the storyboard is instantiated through the parameterless method. In the special case of the main storyboard (the one that's referred to by the info.plist file, or equivalently the mina storyboard setting on the project's General tab in Xcode), the storyboard is also instantiated automatically, giving the automatic window controller instantion also for free. It's basically a convenience, and it's fine (if unnecessary, mostly) to arrange the code to do either of these things manually if you prefer.


Back in Feb, your original problem was just that you were instantiating manually when it was already being done automatically as a convenience. As you discovered, you just needed to do one or the other, not both.


However, instead of turning off the automatic behavior in the main storyboard, I would recommending changing the workflow to leave the app delegate out of it (and eliminate its "windowController" property). Instead, in your window controller subclass, create a static property called something like "shared" that's the main window controller, and set it to the actual window controller in windowDidLoad (preferably) or the designated window controller init (if you have to). You can declare this property get/set(private), so that it's read-only outside the window controller subclass. Anything that current goes to the app delegate "windowController" property can go to WindowController.shared instead.


Alternatively…


It's not ridiculous to take your window scene (and its related view subscenes) out of the main storyboard, and putting them in their own storyboard. This is perhaps especially appropriate if you have multiple window types in your shoebox app, and you want to use a different storyboard for each window, to keep the IB complexity down a bit.


In that case, you would manually instantiate the various storyboards as needed, presumably the main one in "applicationDidFinishLaunching:" and the other ones as the result of menu item actions, with the window controller scenes set as "single" and "initial controller" in each storyboard. (Then you can use the simple storyboard instantiation function and don't need to know the storyboard IDs in your code.)


Finally…


In a shoebox app, you should typically not have a "close" function for the main window. (There's a checkbox for this in the window attribute inspector in IB.) Otherwise, closing the window gives you an app whose entire UI is its menu bar, and users find that extremely confusing. Better to keep the main window open, and all of the re-instantiation issues just go away as a side benefit.


Does that cover everything? I think so.

>In a shoebox app, you want always to get back to the main window, so you want "single" instantiation in the storyboard.


Well, yeah that's what I figured it's for. On iOS, if you needed access to the initial view controller that is automatically created from within the app delegate, you can just get it from UIWindow if you have to. Interestingly though, i'd think you'd have to do that less often (access the view controller from the App Delegate) on iOS than you would on Mac due to the full screen nature of iOS apps. But you can easily.


My point is/was sort of that you can't really make a shoebox app behave properly without having a reference to the window in some way (either via its window controller, or with an old school/simple app where the app delegate has the window property) or if you have the app terminate after the last window is closed. If you create a non-document based app the template you get by default should be configured better. Presumably storyboarding on macOS is to help iOS devs port to the Mac and feel more at home. Maybe it does accomplish this, I don't know. The template for non-doc based app should do probably do one of the following:


1) In the app delegate include the app should terminate after last window is closed method and return YES

or

2) Create a storyboard that doesn't have an initial window controller, but a window controller with an identifier that the app delegate instantiates in app did finish launching, so the AppDelgate can re-show the main window when the window menu item is set up in the menu bar (if you don't do this, your app will get rejected if you submit to the Mac App Store anyway) and when the icon is clicked in the dock when no windows are open.


> Instead, in your window controller subclass, create a static property called something like "shared" that's the main window controller, and set it to the actual window controller in windowDidLoad (preferably) or the designated window controller init (if you have to). You can declare this property get/set(private), so that it's read-only outside the window controller subclass. Anything that current goes to the app delegate "windowController" property can go to WindowController.shared instead.


Ahh..mmm..I guess that's a way to do it. I'd just make the window controller a singleton (if I needed one) and have the app delegate show it in app did finish launching rather than that. That feels sort of like a workaround to the storyboard magic. I'd rather just disable the magic (the inital controller)


>In a shoebox app, you should typically not have a "close" function for the main window. (There's a checkbox for this in the window attribute inspector in IB.) Otherwise, closing the window gives you an app whose entire UI is its menu bar, and users find that extremely confusing. Better to keep the main window open, and all of the re-instantiation issues just go away as a side benefit.


You could or you could have the main window come back when the user clicks the icon in the dock using the app should handle reopen method. I don't think it's *bad* for a Shoebox to do this.


The "initial controller" concept to me feels like it was just brought over from iOS but not adapted to meet Mac development needs. I can see myself turning off the initial controller in every single project, document-based or not. But maybe it's just me.


I'm marking your answer as the correct one because I believe you answered the question: there is no elegant way to access the initial controller a storyboard creates for you by default (unless you instantiate the storyboard programmatically I presume and then instantiate the initial constroller programmatically, which I guess would leave you to set up your app delegate and in Main...).

I think you're overrating the naturalness of handling the singleton reference to the window controller in app delegate, because it's the traditional way of doing it. It's not natural, particularly. It makes just as good sense to make the WindowController subclass responsible for tracking its singleton instance, in the same way that FileManager does, that UserDefaults does, that Application does, that NotificationCenter does, and so on.


I also think it's pointless to disable the automatic instantiation (of the main storyboard and/or the initial controller) just to keep the "windowController" easily settable in the app delegate when … well, what the above paragraph says.


So, I think that seeing it as a "sort of like a workaround" really is just you. Or, rather, just reflects your current comfort level with the old way.


>> On iOS, if you needed access to the initial view controller that is automatically created from within the app delegate, you can just get it from UIWindow if you have to.


Sidestepping to view controllers doesn't help your argument. There is, in fact, an equivalent NSWindowController property "contentViewController" that's set for you when you use a storyboard. (And view controllers have an accessible childViewController hierarchy, too, so you're good all the way down.)


This is just about window controllers, and the only real difference with storyboards is that is very marginally more difficult to get hold of the initial window controller reference.


The other thing that could/ought to be different when using storyboards is to migrate as much functionality that would have gone in your window controller to your root view controller, more like iOS. In this universe, direct global references to your window controller are going to be much rarer.


>> You could or you could have the main window come back when the user clicks the icon in the dock using the app should handle reopen method. I don't think it's *bad* for a Shoebox to do this.


Trust me, it's really bad. Users are used to "seeing" apps by their windows, and they will literally stare at the menu bar (assuming your app has no windows but is still frontmost) without realizing that the next-topmost window's app isn't active. The dock really has the same problem, especially since some users set the dock to auto-hide. A shoebox app with no windows is nothing but an app hogging resources.

>I think you're overrating the naturalness of handling the singleton reference to the window controller in app delegate, because it's the traditional way of doing it. It's not natural, particularly. It makes just as good sense to make the WindowController subclass responsible for tracking its singleton instance, in the same way that FileManager does, that UserDefaults does, that Application does, that NotificationCenter does, and so on.


Well, the necessity to do it in certain (many?) apps is because *only* the app delegate can handle certain situations. The application should handle reopen method gets sent to the app delegate. You can't handle this anywhere else. The argument that your app should just quit instead of showing a main window or a welcome window or whatever is,well, making too many assumptions about too many apps that you aren't developing (not *you* but the template). I don't think the AppDelegate should do too much talking to the main window, or main window controller or whatever, but it has to do what it has to do.


Anyway, I was mainly referring to this suggestion as a workaround to the magic (specifically the part about setting it in windowDidLoad):


>Instead, in your window controller subclass, create a static property called something like "shared" that's themain window controller, and set it to the actual window controller in windowDidLoad (preferably)


Setting a singleton in windowDidLoad IMO ties your window controller too closely with your storyboard and initial controller magic. You may make another instance of that window controller (presumably you'd protect against potentially resetting this in windowDidLoad). I mean if I needed a singleton, I'd just make a class constructor for it +sharedWindowController or whatever. I'm not saying it's wrong if you do it right, it's just not how I'd do it.



>Sidestepping to view controllers doesn't help your argument. There is, in fact, an equivalent NSWindowController property "contentViewController" that's set for you when you use a storyboard. (And view controllers have an accessible childViewController hierarchy, too, so you're good all the way down.)


I mean, on iOS, if you needed to do some kind of configuration on the initial controller (which is a view controller on iOS, but on mac it is a window controller) you don't have to untick the initial controller in the storyboard. You can reference it by grabbing it from the window. I'm not saying I feel great about doing that, but you can if you need to. It's been awhile since I've been on iOS....though.


>Trust me, it's really bad. Users are used to "seeing" apps by their windows, and they will literally stare at the menu bar (assuming your app has no windows but is still frontmost) without realizing that the next-topmost window's app isn't active. The dock really has the same problem, especially since some users set the dock to auto-hide. A shoebox app with no windows is nothing but an app hogging resources.


Maybe. Sometimes I'll close a window but intend to bring it back at some point later. And I'll click the icon in the dock to do it. If I was using an app that quit, I wouldn't like it, but I would also know to minimize the window instead of closing it if it was an app I used a lot. I'd get used to it I guess. I'm aware that an app is using resources by seeing the little black dot in the dock that let's me know it's running. If I need to free up some resources I quit them. Don't most Mac users know that? No big deal either way. iTunes doesn't auto quit when the last window is closed I don't believe. Neither does Mail. Shoebox or not, document based apps with welcome windows need that delegate method too. So there's that.

>> Sometimes I'll […]. And I'll […]


This is the kind of language that brings on an intervention and 28 days in UI rehab. If you're doing anything non-standard (for the platform), consulting your own understanding and preferences is not the way to go. You actually have to test UIs with actual users, and you are disqualified as a test subject.


Quoting from the Mac Human Interface Guidelines:


developer.apple.com/library/content/documentation/UserExperience/Conceptual/OSXHIGuidelines/StartStop.html


"Most users don’t distinguish between closing an app’s main window and quitting the app. Unless there are good reasons for drawing attention to the difference, it’s best to respond to both events in the same way. […] If your app displays only a single window, it's often appropriate to quit automatically when users close the window."


At this point, you're well aware of the technical background to everything discussed in thread. It's ultimately your choice about how to proceed.

>>This is the kind of language that brings on an intervention and 28 days in UI rehab. If you're doing anything non-standard (for the platform), consulting your own understanding and preferences is not the way to go. You actually have to test UIs with actual users, and you are disqualified as a test subject.


No, no. I'm not just talking about my apps. I'm talking about abouts apps I use from other developers.


At this point, you're well aware of the technical background to everything discussed in thread. It's ultimately your choice about how to proceed.


I generally do my best to not do anything non-standard on the platform and comply with the HIG. Really, unless you are arguing that all apps should quit when last window is closed, we got off on a tangent.


Just figured I'd mention (again) iTunes doesn't auto quit when last window has closed. Mail doesn't either. Xcode doesn't, I see the black dot in the dock. And if I click Xcode's icon, the welcome window shows up (I of course can turn this preference off if I want). There are application events the app delegate gets. What about an RSS reader shoebox app the user wants running. It'll potentially fetch articles in the background and *badge* its icon in the dock when it periodically pulls new articles in (like Mail does when you get a new email). Again, you are making too many assumptions about apps you do not know about. The HIG is a great document, but should be balanced with context, not worshipped as a Holy document. App development is creative work.


Let's not be

sheeple

Mac Storyboards...get a reference to "main window controller" from the App Delegate. Shoebox app getting multiple instances.
 
 
Q