views:

386

answers:

1

I see how to solve the problem but it bothers me that I don't understand why this doesn't work. I have a UIViewController subclass that uses Core Data, so it needs the NSManagedObjectContext. The controller is loaded from a nib file where it's placed under a navigation controller which is inside a tab controller.

I tried doing this in initWithCoder and viewDidLoad and for some reason it doesn't work:

MyAppDelegate *appDelegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
self.managedObjectContext = [[appDelegate managedObjectContext] retain];

For some reason managedObjectContext returns nil and I get this when I try to create a managed object later:

* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '+entityForName: could not locate an entity named 'LogRecord' in this model.'

Which is what you get when your context is nil or the model can't be loaded (or really lacks the entity).

If I do the exact same thing at the top of my saveLogEntry method (which creates managed objects and saves the context) then it works just fine.

If I do what the Recipes sample application does:

- (void)applicationDidFinishLaunching:(UIApplication *)application {

    loggingViewController.managedObjectContext = self.managedObjectContext;

    // Standard stuff
    [window addSubview:tabBarController.view];
    [window makeKeyAndVisible];
}

(loggingViewController is an IBOutlet in the app delegate).

Does anyone know what specifically might be going on here? It seems like it fails if done "too early" but especially with viewDidLoad I'd expect it to work since I think that occurs after addSubview is called.

+6  A: 

Do exactly what the recipes app does.

If you try it in initWithCoder, you don't know if the app delegate has finished initialization (which it hasn't)

If you try it viewDidLoad, you have a similar problem.

That is why you should NOT be accessing the app delegate like so:

MyAppDelegate *appDelegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
self.managedObjectContext = [[appDelegate managedObjectContext] retain];

This is bad form. It introduces coupling into your design. Use dependency injection, just like the example. It makes your app more flexible.

Because from the app delegate you know exactly what initialization has been performed and can pass in the context at the appropriate time.


Update:

The issue is that your View Controller instance is likely being instantiated in the Mainwindow.xib. Mainwindow.xib (and any other nibs it references) is "defrosted" before the app delegate receives UIApplicationDidFinishLaunchingNotification notification.

The order in which objects are defrosted from nibs is not guaranteed. When initWithCoder: is called on your View Controller you have no idea what other objects have been defrosted from the nib. You also can't be sure whether the app delegate has received the UIApplicationDidFinishLaunchingNotification notification.

It is similar for viewDidLoad. In viewDidLoad, you can be sure that all other objects in the nib have been properly defrosted and initialized, but since the configuration of the app delegate happens outside of the nib file, you can't be sure whether it is safe to call the app delegate.

It is better just to have the app delegate pass in the context when it is "good and ready", preferably in the applicationDidFinishLaunching: method.

Hope that is a little clearer, you should take a look at the iphone programming guide: http://developer.apple.com/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/index.html

To glean a better explanation of the iPhone application life cycle.

Hope that helps.


One More Update:

In depth discussion of the iphone launch sequence: http://www.bit-101.com/blog/?p=2159

Corey Floyd
Very clear explanation. +1
Chuck
Form aside, what is the actual issue here? managedObjectContext (in appDelegate) should by loaded by the method call in the assignment. Are you saying that appDelegate is not getting returned from the UIApplication call?
gerry3
tried to clarify my answer a bit more… Hope that helps.
Corey Floyd
Solid explanation +1
Marcus S. Zarra
Thanks for the clear explanation. I thought I was actually reducing coupling by querying the app delegate for the context because the controller only needs knowledge of itself that way; the app delegate doesn't have to know what controllers need the context if the controllers obtain it themselves somehow.
Nimrod
Easy to think that. I also had a bad habit of querying the app delegate for model objects. It usually introduces subtle problems like these. The plus side is that it provides an opportunity to learn about the iPhone life cycle.
Corey Floyd
Corey: I was at a bookstore earlier flipping through Dave Mark and Jeff LeMarche's Beginning iPhone 3 Development, half of which seems to be about Core Data. It would seem that they also retrieve the managedObjectContext from the appDelegate, but in the one example I noticed they do it in the second way I did it which is "lazily" using a local variable in a function just before it's needed. I guess I might have to get on their forum and ask them about it. (I get obsessed with details like this for some reason.)
Nimrod
I'm sure they are being careful about when in the lifecycle of the app they are querying the app delegate. As long as you call the proper accessor on the app delegate after all the launch sequence has completed, you will obtain the context correctly. It is just a matter of when you make the call. Since it is dynamic data, I usually populate tableviews and such things in viewWillAppear:. Which is usually a safe place to make such calls.
Corey Floyd
About the "good and ready" part. I don't understand how you keep the view controllers waiting around after they are loaded for the app delegate to be good and ready to hand them the context. For example, the iphone Core Data Recipes example's app delegate injects the MOC into the RecipeListTableViewController (as you say it should). But the RecipeListTableViewController doesn't contain any code (that I see) which would make it handle the situation where it was loaded and had not yet received the MOC from the app delegate yet. It looks like it would just fail under that condition. Hmm...
mwt
Or is it the line in the app delegate:[window addSubview:tabBarController.view];that is controlling the timing on the RecipeListTableViewController viewDidLoad method? That would make sense.
mwt
It doesn't seem to be doing that when I test it, however...
mwt
they aren't waiting around. You should pass in the context in the applicationDidFinishLaunching: method. If they were instantiated in the Nib, they will be ready. If not you can also create VCs in code in applicationDidFinishLaunching: as well.
Corey Floyd
also any method that affects a VCs view will trigger viewDidLoad, IF it hasn't already been triggered.
Corey Floyd
Sorry that my comment was unclear. The problem I am having is that my Nibs are loaded long before the applicationDidFinishLaunching is ready to hand anything off to them. If they are dependent on the app delegate handing them the context, then they will fail. It's unclear to me how to have them *not* fail until the app delegate hands them the MOC. Thanks.
mwt