cocoacocoa-bindingsnsarraycontroller

Using an NSArrayController in Multiple Storyboard Scenes


I have a Mac document-based Core Data application that uses storyboards. The storyboard has the following layout:

Window Controller
    Split View Controller
        Table View Controller
        Text View Controller

My Core Data model contains a Chapter entity that contains two attributes: title and contents. I want the table view to show each chapter title. The text view shows the contents of the selected chapter.

If I was using a xib file, I would add an array controller to the xib file. I would bind the array controller to File's Owner to access my NSPersistentDocument subclass. I would bind the table view to the array controller's arrangedObjects property and bind the text view to the array controller's selection.

But with storyboards things get more complicated. I can add an array controller to the table view controller, bind the table view to the array controller, and have the chapter titles show up in the table view. But the text view controller can't bind to that array controller because the array controller is in another scene.

How do I add an array controller in Interface Builder so that both the table view controller and text view controller can access it and bind to it?


Solution

  • The key to making this work is to have a NSArrayController instance in each of your descending NSViewController subclasses and binding them together through a central data source (most likely your NSDocument subclass). You can then set this data source as your NSViewController subclasses representedObject by passing it down through your descending controllers. Here is an example of a storyboard application with an NSWindowController which has a content view controller that is a NSSplitViewController with two child view controllers (A Master / Detail setup):

    class Document: NSDocument {
    
        var dataSource: DataSource? = DataSource()
    
        ...
    }
    
    class DataSource: NSObject, NSCoding {
    
        var items: [Item] = []
        var selectionIndexes: NSIndexSet = NSIndexSet()
    
        ...
    }
    
    class WindowController: NSWindowController {
    
        override var document: AnyObject? {
            didSet {
                if let document = self.document as? Document {
                    self.contentViewController?.representedObject = document
                }
            }
        }
    
    }
    
    class SplitViewController: NSSplitViewController {
    
        override var representedObject: AnyObject? {
            didSet {
                for viewController in self.childViewControllers as! [NSViewController] {
                    viewController.representedObject = representedObject
                }
            }
        }
    }
    

    The trick is to bind the representedObject to each of your descending view controller's NSArrayController in the storyboard. You need to bind NOT ONLY the contentArray BUT ALSO the selectionIndexes.

    The result is that the selectionIndexes on both descending NSArrayControllers are kept in sync because they are bound through the central data source (DataSource subclass in above example).

    To make this all clearer I have created an example project that demonstrates this here: https://github.com/acwright/StoryboardBindingsExample