Bindings, ViewControllers, and Storyboards in OSX

(Note: 1/11/16 — This all worked until I implemented saving and loading. Then, I think the problem is that the array controller gets bound to the data after it’s already read in. So far I haven’t been able to work around this problem)

After playing around with storyboards in OSX app development, I’ve back-tracked a bit and have set up what I consider a good base project that uses view controllers and multiple XIBs, but doesn’t use storyboards. The reason has to do with a failure to get bindings to work in document- and storyboard-based apps. The sequence of initialization seems to preclude it. (If you have a way to pull this off I’d love to know, as I’m pretty sure storyboards are going to be more useful soon in OSX).

The basic setup for this base project is to create a custom window controller. This is pretty generic and all it knows how to create the top-level view controller (class ‘ViewController’) and window, and how to associate the two. Here it is in its entirety:

lass MainWindowController: NSWindowController {

	override func windowDidLoad() {
		super.windowDidLoad()
		let viewController = ViewController()
		viewController.title = "ViewController View"
		
		let window = NSWindow(contentViewController: viewController)
		window.makeKeyAndOrderFront(self)
	}
}

The ViewController part comes in two pieces. My current parent ViewController is generic, and has no special code currently:

class ViewController: NSViewController {
	
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do view setup here.
    }
 
	override var nibName: String? {
		return "ViewController"
	}
}

However, I added an Extension to NSViewController:

extension NSViewController {
	var document: Document? {
		get {
			if let w = view.window {
				return (NSDocumentController.sharedDocumentController().
					documentForWindow(w)! as! Document)
			} else {
				return nil
			}
		}
	}	
}

I was nervous about this because this property has to be optional (in the scheme of things it gets evaluated before the ViewController’s view has a window), and I didn’t know I could bind an ArrayController’s ContentArray to an optional (document.flows, see below) and get it to work, but it does!

The document class itself is somewhat stripped down:

class Document: NSDocument {
	
	override init() {
		super.init()
		// Add your subclass-specific initialization here.
	}
	
	override class func autosavesInPlace() -> Bool {
		return true
	}
	
	override func makeWindowControllers() {
		let mainWindowController = MainWindowController(windowNibName: "Document")
		self.addWindowController(mainWindowController)
	}
	
	dynamic var flows: [Flow] = []
}

Of course I’ll have to add code, at least for load/save, when I get to it. FWIW a ‘Flow’ is just a simple object with a ‘name’ (String) and ‘active’ (Bool).

The Document.xib doesn’t need a Window anymore. The ViewController.xib is where all the IB layout happens.

As far as bindings go, as you can see above there is a [Flow] in the Document. I set up a class-based ArrayController in ViewController.xib, ObjectController class ‘Flow’. The Content Array is bound to ‘File’s Owner’.document.flows. I have a TableView bound in the normal way to this array controller. And I added a checkbox and bound it to Array Controller.selection.active to make sure that part of bindings worked. I cheated a bit and bound the checkbox Enabled to the ArrayController’s canRemove, since for single selection tables I think this probably is equivalent to ‘there’s an item selected’.

Next up I think is going to be a top-level split view controller.

Leave a Reply

Your email address will not be published. Required fields are marked *