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.

theMikeSwan
  • 4,739
  • 2
  • 31
  • 44

4 Answers4

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.

theMikeSwan
  • 4,739
  • 2
  • 31
  • 44
  • 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. – AutomatonTec Jul 16 '16 at 03:13
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
Sandy Chapman
  • 11,133
  • 3
  • 58
  • 67
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

stevesliva
  • 5,351
  • 1
  • 16
  • 39
  • 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. – theMikeSwan Jan 28 '15 at 08:41
  • 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). – stevesliva Jan 28 '15 at 18:49
  • 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! – theMikeSwan Jan 28 '15 at 22:44
  • 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. – stevesliva Jan 29 '15 at 03:00
  • 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. – Elise van Looij Sep 29 '16 at 09:56
  • @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. – stevesliva Sep 29 '16 at 14:50
  • @stevesliva 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. – Elise van Looij Sep 30 '16 at 17:41
  • @theMikeSwan Technical Q&A QA1871 Cocoa Bindings with Storyboards (https://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.' – Elise van Looij Sep 30 '16 at 18:31
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.

  • 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. – theMikeSwan Sep 16 '15 at 19:34
  • 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. – Frank Nichols Sep 29 '15 at 02:27
  • 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. – theMikeSwan Sep 29 '15 at 22:02
  • 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! – Frank Nichols Sep 30 '15 at 02:38
  • 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. – The Cappy Jan 25 '16 at 01:45