Using an NSArrayController in Multiple Storyboard Scenes
Asked Answered
M

1

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?

Mali answered 5/11, 2014 at 1:17 Comment(4)
Just stick it in the app delegate as a property... (joking, mostly.) But what keypaths exist to parent views in OSX storyboards?Biernat
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.Mali
I was reading this again... Perhaps override the parentViewController getter to return the specific subclass that has your arrayController property? That's ugly, though.Biernat
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.Mali
W
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

Wiring answered 27/4, 2015 at 19:46 Comment(8)
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.Mali
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! :)Wiring
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.Nomadize
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.Nomadize
Fantastic example, this is helpful even when using storyboards without bindings.Radiosurgery
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.Judie
@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?Kathrynekathy
@Kathrynekathy You could possible instantiate the dataSource object in the AppDelegate.Wiring

© 2022 - 2024 — McMap. All rights reserved.