15

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?

Swift Dev Journal
  • 19,282
  • 4
  • 56
  • 66
  • Just stick it in the app delegate as a property... (joking, mostly.) But what keypaths exist to parent views in OSX storyboards? – stevesliva Nov 05 '14 at 18:03
  • 1
    NSViewController has a parentViewController property. But if I add an array controller to the split view controller and bind the table view column's Value binding to the table view controller using a model key path of parentViewController.arrayController.arrangedObjects, the app crashes saying the class is not KVC-compliant for the key arrayController. I added an outlet for the array controller to my split view controller subclass and connected the outlet to the array controller I created in IB, and the same crash occurs. – Swift Dev Journal Nov 05 '14 at 19:10
  • I was reading this again... Perhaps override the parentViewController getter to return the specific subclass that has your arrayController property? That's ugly, though. – stevesliva Mar 03 '15 at 01:43
  • I tried overriding the parentViewController getter, and I ended up with an empty table. Thanks for the suggestion. I've concluded it's currently not possible to share an array controller with Mac storyboards. Maybe Apple will add this capability in OS X 10.11. – Swift Dev Journal Mar 03 '15 at 06:06

1 Answers1

21

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

Andrew
  • 7,630
  • 3
  • 42
  • 51
A.C. Wright
  • 921
  • 9
  • 21
  • 1
    Impressive. I downloaded your example, and it works. I wish I would have this answer months ago. I gave up on using bindings for my current project, and I'm not switching back, but I will keep this answer in mind for future projects that use storyboards. – Swift Dev Journal Apr 27 '15 at 20:40
  • 1
    Thanks! I was about to give up on bindings for the project I was starting as well, but I felt like a solution must be possible! Then it occurred to me that it was really just a matter of keeping the selections in sync! :) – A.C. Wright Apr 27 '15 at 20:57
  • Have you got this to work for Core Data? I had a similar idea to what you've done, but while I can get the selection index and prove it's being updated from one scene, I always end up with an empty array controller in the second scene. – LighteningKid Jan 14 '16 at 18:26
  • Never mind, it does work! The key seemed to be to put a selectedIndexes variable in the Document.swift, which is set as the representedObject of every NSViewController, so you can be sure each gets the same selectedIndexes. You need the Document as the representedObject anyway, so you can set the managedObjectContext of all the NSArrayControllers for Core Data. – LighteningKid Jan 15 '16 at 15:37
  • Fantastic example, this is helpful even when using storyboards without bindings. – Andrew Feb 12 '16 at 21:28
  • Great example. I used the same basic structure with Core Data, with the `DataSource` object exposing a `var context:NSManagedObjectContext` property instead of your `items` array. Works well. – Rob Keniger Jul 14 '16 at 07:28
  • @AaronWright Great answer. In a non-document based app using CoreData, where would I instantiate the dataSource (instead of the NSDocument subclass)? I have implemented that in the split view controller, is there anything speaking against that? Is there a best practice rule? If I need to go down the hierarchy to further subviews, where would I set the representedObject for those? In the viewDidLoad? – DaPhil Jan 27 '17 at 14:12
  • @DaPhil You could possible instantiate the dataSource object in the AppDelegate. – A.C. Wright Nov 19 '19 at 01:52