NSDocument window controller

I created a new NSDocument based Xcode project with storyboard. When I runn the app and open a document, the following method is called:


func read(from data: Data, ofType typeName: String) throws

{

Swift.print("Number of WindowControllers:",self.windowControllers.count)

throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)

}


This prints:

Number of WindowControllers: 0


I want to change the text in the textfield that is set by default in the window's default content view. But I can't seems to be able to get reference to the window.


The default app also has this code:


override func makeWindowControllers() {

/

let storyboard = NSStoryboard(name: "Main", bundle: nil)

let windowController = storyboard.instantiateController(withIdentifier: "Document Window Controller") as! NSWindowController

self.addWindowController(windowController)

}


Why is it not working?

Accepted Reply

The count of window controllers is 0 because (presumably) the window controller hasn't been created yet. The read(from:ofType:) method is for (re)creating the data model, which eventually the window controller will be "asked" to present to the user.


Please note — it's a crucial concept — that a data model doesn't "know" anything about the UI through which it going to be presented. That's the point of the MVC design pattern. It makes no difference to the data model whether there's any UI at all. This means you shouldn't be expecting to populate the text field from the read method. It happens the other way around. You (re)create your data model, then when the NSDocument machinery later creates window controllers and view controllers, those controllers make it possible for the UI elements (views) to find the data model.


>> I would think I will need windowController for anything which does more


Possibly, and maybe even probably, for a non-trivial document-based app. However, it's usual (in modern Cocoa apps) to put functionality in a view controller by preference, and only fall back to the window controller for functionality that applies to the window as a whole. For example, if you need to do something just before the window is closed, you'll likely put a "windowWillClose" in your window controller, rather than trying to capture this via a view controller. There are no absolute boundaries here. You'll end up doing what makes sense at the time. 🙂

Replies

There are a few things going on, which aren't at all obvious until you've made the mental connections. It's also complicated by the fact that the NSDocument machinery was invented in the days before storyboards, and so a new superstructure has been retrofitted onto the old understructure.


A document (your NSDocument subclass) has one or more window controllers associated with it. Normally, there's just one, and it's created for you automatically, in "makeWindowControllers". The window controller is a NSWindowController instance, although it can be a subclass of NSWindowController if you wish. If you weren't using a storyboard, you'd almost certainly want a subclass, but because of the way storyboards are organized, you may not need a subclass in your case.


The window controller has a "contentViewController" property which is a reference to a NSViewController subclass. If you look in the storyboard, you'll see a window controller "scene" and a root view controller "scene", with its class set to a custom subclass of NSViewController that was provided for you by the Xcode app template. That's the "content" view controller.


So, you'll want to do the following:


1. In ViewController.swift, create an outlet to the text field if there isn't one already, and connect it from the view controller to the text field in the storyboard scene.


2. In the simplest case, you can set up the text field in the "viewDidLoad" method, or possible "viewWillAppear" or "viewDidAppear", depending on the exact time you want or need.


But there's usually a bit more involved. You'll likely want to follow the MVC pattern, which means your document contains its data model. The data model is created by a new document initially, or read from a document file after it's been saved, using the method you mentioned in your post.


That means you'll likely want your view controller to be able to find the document data model. This is very slightly tricky, because you don't get a direct reference for free. You can, however, get from the view controller to its view, then from its view to the window, then from the window to the window controller, then from the window controller to the document, then from the document to its "model" property (if that's the property you set up when reading in the document data). So, from the view controller (ignoring optionals and type casting):


self.view.window.windowController.document.model


However, you must remember that this chain of references isn't complete untill after the view is added to the window's view hierarchy, which happens after "viewDidLoad". So, this method of initialization must be delayed to (say) "viewWillAppear".


If you ever need to go in the opposite direction, from document to window controller to view controller, you can do it like this (ignoring optionals and type casting):


self.windowControllers [0].contentViewController


You should read the document architecture documentation:

developer.apple.com/library/prerelease/content/documentation/DataManagement/Conceptual/DocBasedAppProgrammingGuideForOSX/Introduction/Introduction.html

but be aware that this was written before there were storyboards on the Mac, and before the major NSViewController changes introduced in 10.10. A lot of the UI related behavior it describes for window controllers is now normally handled by the root/content view controller.

I did that:


labelText.stringValue = (view.window?.windowController?.document as! Document).myText


It sets the right value but I still get 0 windowControllers count. Yesterday, I was able to bind that NSTextField onto the Document by setting the viewController's representedObject, and it worked but I would think I will need windowController for anything which does more.

The count of window controllers is 0 because (presumably) the window controller hasn't been created yet. The read(from:ofType:) method is for (re)creating the data model, which eventually the window controller will be "asked" to present to the user.


Please note — it's a crucial concept — that a data model doesn't "know" anything about the UI through which it going to be presented. That's the point of the MVC design pattern. It makes no difference to the data model whether there's any UI at all. This means you shouldn't be expecting to populate the text field from the read method. It happens the other way around. You (re)create your data model, then when the NSDocument machinery later creates window controllers and view controllers, those controllers make it possible for the UI elements (views) to find the data model.


>> I would think I will need windowController for anything which does more


Possibly, and maybe even probably, for a non-trivial document-based app. However, it's usual (in modern Cocoa apps) to put functionality in a view controller by preference, and only fall back to the window controller for functionality that applies to the window as a whole. For example, if you need to do something just before the window is closed, you'll likely put a "windowWillClose" in your window controller, rather than trying to capture this via a view controller. There are no absolute boundaries here. You'll end up doing what makes sense at the time. 🙂

Now I understand, thanks, that makes so much sense. I did felt odd doing that but I wasn't quite sure what I was doing wrong. I should load the content onto the view from the data model if I don't want to go through the binding route, instead, I was trying to make the model tell the view that it's ready but ofcourse neither the view not the window is loaded yet. Thank you so much!