How do you bind a storyboard view to a Core Data entity when using NSDocument?
Asked Answered
S

1

5

I'm building an OS X app that uses core data, NSDocument, storyboards, and Cocoa bindings.

My expectation is that the following occurs:

  1. An instance of MyDocument (NSDocument subclass) is created.

  2. MyDocument creates a Core Data NSManagedObjectContext that represents the document's data.

  3. MyDocument instantiates an NSWindowController from the storyboard by its identifier.

  4. Within the storyboard, the window controller contains DocumentEditorViewController (NSViewController subclass) which displays and edits the document.

  5. Within the storyboard, DocumentEditorViewController has an NSArrayController that's bound to MyDocument's managed object context.

  6. Within the storyboard, DocumentEditorViewController has a table view that's bound to the NSArrayController.

This way any changes in the UI make it all the way to the NSManagedObjectContext, without any glue code.

I expect this to be straightforward, as I believe I'm using these technologies in the way they are intended. However I have been unable to get the bindings to work, particularly at steps 5 and 6. All of the project templates and example projects I've found either don't use Core Data, don't use storyboards, or don't use NSDocuments.

Which objects should be bound to which? What should teh NSArrayController's class, keys and keypath be?

Another way to answer this question is to point out a working sample project that uses all these technologies together.

Skylar answered 3/2, 2016 at 2:2 Comment(11)
You are using a separate NSManagedObjectContext for each document? Is that the recommended way in OSX/document-based apps? In (non-document-based) iOS apps, I typically use a single context (owned by the app delegate), and separate instances of NSManagedObject for each model object...Roldan
I don't see any examples or guidance from Apple that answer your question. I think that separate managed object contexts makes more sense because the documents are completely independent. It doesn't make sense to me to mix their data into a single context, and then having to filter all access to the context for the current view's subset of the data.Skylar
Have you tried a testcase where you programmatically add a few MOC objects and see the table view display them? It's not clear when you reference 5&6 if you've tried to debug the bindings separate from the AC's ability to create content.Virtuosity
@Virtuosity I have test data in the MOC. I have never gotten any data to appear in the UI. My guess is that I have the binding from the table view to the array controller correct, so my likeliest problem is getting the MOC binding correct on the array controller. I don't know how to debug it in any more detail than that.Skylar
@BobWhiteman you need the AC bound to the context and fetching an entity of a specific type... if you're at wits end you can screenshot the IB configuration for the AC. And it might be placebo, but product->clean?Virtuosity
@Virtuosity I've made some progress. The DocumentEditorViewController exposes the Managed Object Context as a property called moc. In its makeWindowControllers() I set the representedObject on the DocumentEditorViewController to MyDocument. The NSArrayController is bound to the DocumentEditorViewController, with the Model Key Path set to representedObject.moc. Then I bind the table view to the array controller normally, and I actually get the right number of rows in the table. That makes me think the AC is bound properly. However the text in the cells is blank.Skylar
Possible duplicate of What is the new way of binding an NSArrayController to the managed object context of a Core Data document?Papst
@Papst That looks very similar. It seems to validate the overall design I proposed where there is one managed object context per document. However the answers only discuss the binding from the array controller to the managed object context, and there's other bindings that seem to be different for this design. Following the advice in the question, I still don't have a functioning app as described in my previous comment above.Skylar
How did you bind the text fields in the table view?Papst
@Papst The table view's Content is bound to the array controller with the controller key set to arrangedObjects. Then the text field's Value is bound to the table cell view with the model key path set to objectValue.name (and "name" is the name of the Core Data attribute I want to use to populate the text field. I've sketched out the entire class diagram in this image: imgur.com/1o9cSaR (Sorry for the whiteboard photo.)Skylar
@Papst With the arrangement mentioned in my previous comment I get an error "Cannot perform operation without a managed object context". I've done everything I can to verify that the managed object context on the array controller is set properly.Skylar
P
13

Steps to create a sample Xcode Document-Based Application project with Core Data, Storyboard, NSArrayController, NSTableView and Bindings.

Step 1 Create a Xcode project. Choose OS X Cocoa Application and select ‘Use Storyboards’, ‘Create Document-Based Application’ and ‘Use Core Data’.

Step 2 Select the data model. Add entity ‘Person’ and string attributes ‘name’ and ‘address’.

Step 3 Select Main.storyboard. Add a NSArrayController to the view controller scene. Set Mode to ‘Entity Name’ and set Entity Name to ‘Person’. Check ‘Prepares Content’. Bind Managed Object Context of the array controller to View Controller, Model Key Path representedObject.managedObjectContext.

Step 4 Go to the view of the view controller scene. Remove ‘Your document contents here’. Add a NSTableView. Bind Content to Array Controller, Controller Key arrangedObjects. Bind Selection Indexes to Array Controller, Controller Key selectionIndexes. Bind Sort Descriptors to Array Controller, Controller Key sortDescriptors.

Step 5 Bind Value of the text fields in the table view to Table Cell View, Model Key Path objectValue.name and objectValue.address. Check 'Conditionally Sets Editable'.

Step 6 Add two Push Buttons ‘Add’ and ‘Remove’ to the view of the view controller scene. Connect the actions to actions add: and remove: of the array controller.

Step 7 (Objective-C) Select Document.h. In method makeWindowControllers, replace statement [self addWindowController:… by

NSWindowController *aWindowController = [[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateControllerWithIdentifier:@"Document Window Controller"];
[self addWindowController:aWindowController];
aWindowController.contentViewController.representedObject = aWindowController.document;

Step 7 (Swift) Select Document.swift. In method makeWindowControllers, at the end after self.addWindowController(windowController) add

 windowController.contentViewController!.representedObject = windowController.document

Step 8 Build, Run, Test.

Papst answered 10/2, 2016 at 15:36 Comment(3)
I was about to answer my own question with basically what you just posted. I had two major bugs in my code. One is unrelated, but the other is a problem with the new project template. It overrides the representedObject property's didSet method without implementing KVC. If you set windowController.contentViewController!.representedObject it works fine, but if you cast the contentViewController to the type of your window controller's subclass, it defeats KVO and you get the "Cannot perform operation without a managed object context" error.Skylar
This seems to work for me without binding the Selection Indexes or Sort Descriptors in step 4. Can you describe why those are necessary?Skylar
With the binding of selection indexes and sort descriptors, the selection and sorting of the array controller and table view are in sync. 'Select Inserted Objects' of the array controller, sorting by clicking in the column headers and binding elements of a detail view to the selection of the arraycontroller will work.Papst

© 2022 - 2024 — McMap. All rights reserved.