Passing data from NSWindowController to its content NSViewController.

Hey there. I'm struggling to deal with the two-phase initialization pattern that Storyboards push you into.

Some of my AppDelegate's methods open new windows. They do this by first by instantiating a new MainWindowController from a Storyboard, initializing some properties on it, and then calling showWindow.

When initializing the window controller, the storyboard calls an initializer whose signature I can't control (init?(coder:)). Similarly, when the storyboard automatically instantiates the MainWindowContentViewController. This is the two-stage initialization pattern I mentioned earlier. In attempt to work with it, I came up with this convention:
  1. Each window/view controller has a custom "initialize" method with supplementary parameters that couldn't have been passed to init?(coder:). Since this isn't the real init, all fields set from this psuedo-initilizer are forced to be implicitly unwrapped optionals.

  2. Each custom initialize method is responsible to call similar initialize methods of descendant views.

  3. During viewDidLoad, a view controller can expect that its custom initialize method has already been called by the parent, and that the IUOs are safe to access.

  4. By the time viewWillAppear, most of the the heavy lifting is done, so (for example) presentation of tab views can be very snappy.

This works for the most part, except for one big fly in the ointment: NSWindowController.windowDidLoad() is called after all subviews were loaded. If I hook into it to start this top-down initializer call chain, any fields set by those calls won't be ready in time for viewDidLoads (since those already fired before we even started). I can't find any earlier lifecycle methods on NSWindowController I can hook into to start my top-down initialization pattern. In practice, this means that I don't have a way to pass data from my MainWindowController to my MainWindowContentViewController in a way that makes it available by the time viewDidLoad is called.

I've made a minimal demo project that illustrates my issue. In that example, I compare passing a toolbar button driven Publisher from the window to its content view. Because windowDidLoad is called after viewDidLoad of the content view, I had to move my content view's self-initializing logic to viewDidAppear (I actually meant to use viewWillAppear, but mixed that up).

Fundamentally, I'm trying to use dependency injection rather than singletons (or global variables in general), so that my AppDelegate can pass some objects to the window controller which can pass it to its content view's view controller, which can pass it down to its descedant views' view controllers, and so on.

What' s the right way to achieve this?

Replies

Thought I'd share my research on what workarounds might exist, in case that sparks any ideas.
  1. In narrow cases (IBActions for the menu or toolbar items), the need for top-to-bottom data passing can be replaced with the responder chain. However, this won't help for other kinds of information, e.g. injecting a DownloaderService object into the view from the window, from the app delegate.

  2. It looks like the new NSStoryBoard.instantiateInitialController(creator:) APIs introduced in 10.15 can remove the two-stage initialization pattern for NSWindowControllers. You're given the coder instance and expected to initialize the window controller yourself. This allows you to call any initializer you want.

    • This is great, because you can make a MyWindowController.init?(coder: NSCoder, mySupplimentaryValues: ...).

    • Since you're now calling a real init, the fields it sets no longer have to be implicitly unwrapped optionals. Sweet!

    • Even still, I'm not sure how/when you would pass data from the window controller to the content view controller.

    • aaaaand it appears to be total completely broken, so I can't use it yet. Huge bummer :(

  3. Segues are usually how you can pass data from one controller to another, but from what I can tell. This seems like a good point to "kick off" the top-down initialization process, but it appears that the containment segue between a window controller and its content view controller doesn't actually trigger a segue.

    • Similarly, if you have a @IBSegueAction setup between an NSWindowController and an NSViewController, it will be ignored, as mentioned in the "known issues" section of the Xcode 11 release notes

  4. I can give up on Storyboards and switch back to Nibs, where each window/view has its controller manually instantiated by code from its parent, allowing me to override init?(coder:) with init?(coder: NSCoder, mySupplimentaryValues: ...), where I can do all my work with all the values I need. I'm apprehensive to do this however, because I find it hard to believe that such a simple use case can't be implemented in Storyboards.

  5. I can pass data from my App Delegate directly to my main window's content view controller using a global variable, or a singleton. yuck.

I can give up on Storyboards and switch back to Nibs

On iOS, I use storyboard extensively. It is extremely convenient, as you see the app logic and you usually go from one view through the other by some type of segue.
But on MacOS, I stay with Nibs. I don't find storyboard well suited with the multi window environment usual for Mac apps where you go from one window to another from many different actions (menus, contextual menus, …).
In addition, it is clear that storyboard are not as well designed for Mac apps (just an example: 5 years later, no zoom yet).

So, I would advise to go back to Nibs.
Aw man, that's disappoint to hear. But I think it's time for me to roll up my sleeves and move things back over.