What is the new way of binding an NSArrayController to the managed object context of a Core Data document?
Asked Answered
A

4

13

Before Xcode went and added Storyboards for OS X apps you could connect an array controller to your document's managed object context by binding the Managed Object Context of the array controller to File's Owner with a Model Key Path of managedObjectContext. With storyboards there is no more File's Owner so where do you get the context from now?

Apple's documentation is behind in this area and there aren't any obvious places to bind to in Xcode. Obviously I can just fall back to a non-storyboard route and use the old method, but there must be a new way of doing it.

Acetophenetidin answered 28/1, 2015 at 3:37 Comment(0)
A
10

So I have the answer from Apple. This is for Document based Core Data apps, the code is all in Swift but the idea is the same in Objective-C you just have to translate it.

The first answer they gave me was to bind the array controller to the view controller running the view with a model key path of self.view.window.windowController.document.managedObjectContex. The sample I was shown used this method and had no error messages at all, it was however a single view controller inside the window controller with one array controller. My setup is a window to a tab view to the views with two array controllers in the one scene. I was still getting Cannot perform operation without a managed object context once each time a new document was opened or created. The second solution, that worked for me was to still bind the array controller to the view controller but with a model key path of self.representedObject.managedObjectContext and then to add to the end of the document class's makeWindowControllers() function:

override func makeWindowControllers() {
……
    let tabViewController = windowController.contentViewController as NSTabViewController
    for object in tabViewController.childViewControllers {
        let childViewController = object as NSViewController
        childViewController.representedObject = self
    }
}

This solved the issue for me. Hopefully there is enough info here to show up when others google this issue.

Acetophenetidin answered 5/2, 2015 at 19:3 Comment(1)
My problem is that the managedObjectContext for the Object Controller in the storyboard view controller is being asked for during the let windowController = storyboard.instantiateControllerWithIdentifier("Document Window Controller") as! NSWindowController call before I have a chance to set the representedObject to the document.Callida
T
9

Using the default Xcode generated project and including CoreData puts the managedObjectContext member on the AppDelegate. You can add the following code to your ViewController, then use managedObjectContext as the "Model Key Path" with binding to ViewController for your NSArrayController.

lazy var managedObjectContext: NSManagedObjectContext = { 
    return (NSApplication.sharedApplication().delegate
        as? AppDelegate)?.managedObjectContext }()!

This simply creates a member which redirects to where your actual MOC is stored. This is useful because the NSArrayController binding happens before viewDidLoad(), hence why an instance member will not suffice. Also, if you want to refactor to a singleton CoreDataManager class, you can just change where to redirect to. Additionally, you could add this as a class extension to enable all ViewControllers to access your MOC.

Objective-C version upon request:

@interface MyViewController ()

@property (nonatomic, readonly) NSMangedObjectContext* managedObjectContext;

@end

@implementation MyViewController

- (NSManagedObjectContext*)managedObjectContext
{
    return ((AppDelegate*)([NSApplication sharedApplication].delegate)).managedObjectContext;
}

...

@end
Taite answered 19/8, 2015 at 16:20 Comment(3)
@Chapman: Can you please translate your example to objective cYour
@user3175421 : Updated with the Obj-C version. Not quite as concise ;)Taite
Not quite as concise but much, much more readable! Or in an other punctuation: return [(AppDelegate *)[[NSApplication sharedApplication] delegate] managedObjectContext];Your
V
1

You have always been able to bind through NSApplication with a keypath of delegate.managedObjectContext if the application delegate owns the core data stack. Otherwise you could pass pass the MOC through to each view controller with a MOC property on each one, which is strongly preferred by those who argue that the app delegate shouldn't be used to own singleton MOCs, and that there's further utility in being able to provide each VC a separate MOC.

I believe you could also create a MOC instance in the storyboard in IB. There's also always been a MOC object for nibs, at least. Though I haven't used that enough to know how it relates to a programmatic core data stacks. Probably better to just have a MOC property somewhere you can access in either the VC hierarchy or app delegate

Vernacularism answered 28/1, 2015 at 5:41 Comment(8)
I tried passing the MOC through to an outlet that was an IB instance of a NSManagedObjectContext and I tired passing the NSPersistentDocument through and binding to that, neither worked so I filed a DTS incident. I may try just passing the MOC through and not having it be an outlet at the end might make the control-drag to bind work better.Acetophenetidin
My understanding is that adding that MOC object in IB will actually allocate a MOC which the view would own, and which would be released when the view was deallocated. And the bindings to that object will likely point to the object the view owns regardless of whether you try to point the IBOutlet property to a different MOC that you create programatically-- the objects in the view are bound to the MOC in the view. I think it's more straightforward for either the app delegate or one controller or another to create the MOC, and bind through the controller(s).Vernacularism
Yeah, I was pretty sure the MOCs I added in IB weren't getting changed to the one from the document even though I was using an outlet to set them. I asked this over in the Apple Dev Forums as well and got an answer that nearly works: bind the MOC of the array controller to the view controller running it with a key path of view.window.windowController.document.managedObjectContext. I still see one complaint for each tab that gets loaded but it works after that. Best part is there is still no code to get a super simple Core Data document based app!Acetophenetidin
I totally missed that you were using doc-based apps. You can answer yourself if you want. This was sort of an extended comment. I would add that you should add a property to the view or the window controller for the MOC pointer so that you can switch to disposable child contexts with relative ease, should your app start loading data from a server, you may switch the doc's MOC to private queue, and the GUI MOCs to mainQueue. You can also spawn child MOCs for user input that can be cancelled. In any of those events, you would not need to update the bindings in IB.Vernacularism
I've tried adding a managedObjectContext object to the ViewController scene. In one case, the App Delegate suddenly became available for binding, but I've not been able to reproduce that. Anyway, the IB moc object is useless. You can bind to it, but it always gives the error: "NSInternalInconsistencyException:Cannot perform operation since managed object context has no persistent store coordinator". The moc object is not configurable in IB, subclassing might be a solution.Hydrolysate
@ElisevanLooij - subclassing would make sense. I had one obj-C mentor who recommended subclassing MOC with generator methods and a singleton of sorts. That said, though, I assumed the IB MOC was there so that it could be an IBOutlet for the view controller-- or anything else-- too hook up to the PSC. In all though, I have never never seen anyone ever mentioning using the IB MOC object in real examples.Vernacularism
@Vernacularism Weird, but I'm starting to see why. I've tried subclassing NSManagedObjectContext, without success. It kept complaining about a missing persistentStoreCoordinator. Then I tried (one after the other), implementing awakeFromNib , then overriding getPersistentStoreCoordinator, init, initWithCoder: and initWithConcurrencyType: -- no joy. I'm giving up on that route.Hydrolysate
@Acetophenetidin Technical Q&A QA1871 Cocoa Bindings with Storyboards (developer.apple.com/library/content/qa/qa1871/_index.html) notes: 'Binding directly to key paths like "self.view.window.windowController.document" may not be safe, because when the binding is working, the view is not necessarily in the view hierarchy (OS X may remove it from the view hierarchy when it is off-screen), thus "self.view.window" may be nil.'Hydrolysate
C
1

Updated:

@theMikeSwan, well, it almost works for me. Here is what I have:

OSX EL Capitan GM Xcode 7GM and Xcode 7.1 beta

A standard Coredata/Document application

Replaced MainViewController with TabViewController and added 2 ViewControllers to that.

Added in your code to put representedObject in all view controllers in the tabviewcontroller.

Tab one is a view controller with a table, and an array controller that is bound to an entity called Profiles and the tableview is bound to that controller with +/- etc

Tab two is a view with view controller with a table, and an array controller that is bound to an entity called Commands and the tableview is bound to that controller.

There is a one to many relationship between the Profiles and the Commands entities with the names profiles <->> commands.

Both tab's work as expected with no errors independently - meaning I can add and delete Profiles->name in the table in the first tab, and I can add and delete Commands->name in the table in the second tab.

Next I want to enforce the one to many relationship - meaning if I select a Profile in the table in tab 1, and then switch to tab two, I want to see only the commands related to the selected profile in that table. That does not work. All entered Commands are shown in all cases, I have tried filters predicates, fetch predicates, etc, with varying degrees of disaster.

I have tried everything I can think of, and a lot of hacks I would rather not mention -

At this point I have added a second arrayController to the second tab view and bound it to Profiles entity and with self.representedObject.managedObjectContext etc... I added a NSTextField on the second tab view and bound it to the just added profileArrayController -> selection -> name to see what the controller was thinking...

The Profile->name in the second tab never changes regardless of what I select in the first tab's table, it is always showing the same Profiles->name. The commands listed in the table in the second tab are not affected by any selection in the first table.

It "feels" like the MOC on the second tab is not the same as the MOC referenced by the first tab. But that is just a feeling. I am lost, any suggestions on how to do a one to many relationship across tabs on a multi-tab view controller setup like this?

thanks Frank

Edited to add:

BTW, I have on some of those tabs, like the command tab multiple tables configured in one to many relationships on the same tab that work correctly - for example I have a synonyms table with bindings to a synonym entity via an array controller which is a many side of a relation ship with the command entity. It works fine as long as the tables/arraycontrollers are on the same tab, but when on separate tabs it is no joy.

Conchita answered 15/9, 2015 at 5:17 Comment(5)
Take a look at the portion of my answer that mentions binding to self.representedObject.managedObjectContext. Be sure to also use the code shown. I also had issues when using a tab view in a document based core data world. @marcus-s-zarra has written a book about Core Data and you can find several posts on his blog about it as well. I have a few much more meager posts that are fairly old these days but still have useful info in them at www.theMikeSwan.com/blog/. This might have been better served as a new question btw.Acetophenetidin
Updated my problem description to new info - basically, individually arrayControllers seem to be working, but I need them to work in coordinated manner across the tabs - reflecting a one <->> many relationship between the tabs.Conchita
Both array controllers are seeing the same data at the MOC level, the issue is that the selection is stored in the array controller not the MOC. In order to have a selection in one tab affect what is shown in another tab you will need to add something, likely in whatever shared super view or window controller there is between the tabs, to track the selection in the primary tab so that the two tabs have a communication channel. Alternately you might be able to add an entity to your model that just tracks the selected item then use that to control what is shown in the second tab.Acetophenetidin
Thank you very much, that explains a lot of what I am seeing. I was under the impression that MOC contained the selection and now that I think about what you said, it makes perfect sense. Thank you!Conchita
One answer would be to send out "New-Selection-Happened" notifications when when you get a selection. Your various controllers can then listen for those notifications and update accordingly.Asthenia

© 2022 - 2024 — McMap. All rights reserved.