22

So it seems to me like there's a Catch-22 situation here. Note the following widely (and smartly) held stances on application architecture:

  1. Apple (and most alpha devs) recommend not using a singleton or accessing the app delegate singleton for retrieving a NSManagedObjectContext. Rigidity, poor design, etc. OK-- I agree!
  2. Iterating over a UITabbarController's child view controllers and giving each child VC a reference to the context (dependency injection) is also stupid, because you are loading every tab in your application at application launch time, just to pass a reference. This also goes against everything Apple recommends for application architecture.

How do you resolve this? Do you use NotificationCenter to post a notification when a view controller awakes from a nib, and have the app delegate pass in the context reference then? That's about the only way I can think of that jives with both #1 and #2, but it also feels like some contortion to me.

Is there a more elegant way?

Edit: doing a notification when initializing a view controller can be a race condition, since if you're doing things with Storyboard, your tabbar's child view controllers tend to be initialized (though sans-view loading) at launch. So you'd have to do such a notification in viewDidLoad, which is a bad idea vis-a-vis MVC convention. It also ties your hands from doing anything with data models (like pre-caching for performance) before the user does anything view-related.

Eric G
  • 1,429
  • 18
  • 26
  • It seems not-so-bad to walk over the UITabBarController's view controllers, as iOS 5 runtime seems to wake each component view controller up anyway. It just doesn't load the views. – Eric G Dec 25 '11 at 05:11
  • 1
    It's been a while since this has received activity. @EricGoldburg have you had any success with finding best practices? – MrJD Apr 27 '12 at 06:05
  • @MrJD: It turns out the world loves singletons, even if many developers don't. I ended up using RestKit for this project, which abstracts away the need to pass contexts, but it also uses a singleton internally for the default case for its object loader and managed object handling classes. C'est la vie. – Eric G Apr 27 '12 at 20:46
  • I'm still unconvinced. It's not difficult to pass between view controllers. Passing through tab controllers and navigation controllers without sub-classing becomes difficult but I'm sure with delegation you can reduce coupling which is a result of singleton use. I wonder if I'll give in to it. – MrJD Apr 28 '12 at 01:12
  • No, it's not difficult... it's just annoying. Like if you don't need Core Data until 8 view controllers into a nav controller, you have to pass the context around basically to everyone just in case. In the web dev world, who ever creates a DB connection in the front controller and passes it to every page controller? But yeah, at some point I'm a pragmatist and just "gave in" to it. I also like delivering prototypes in Pragmatic Time. :) – Eric G Apr 28 '12 at 16:15
  • 2
    I am slightly more convinced. I'm interested because I'm irritated by the lengths in going through to pass from delegate to tab to nav to view controllers. Would you strongly recommend the subclassed singleton method below as a effective alternative? I noticed apple strongly recommends against subclassing NSManagedObjectContext – MrJD Apr 29 '12 at 01:03

4 Answers4

13

When you pass NSManagedObject instances to a view controller, that view controller can retain those objects. It can then get to the NSManagedObjectContext through those managed objects by calling

-[NSManagedObject managedObjectContext]

I'm not sure if this will work in your particular case, but often it will. The app delegate or the root view controller creates a context and then passes along managed objects.

Update

If you need to use the context in multiple places, there's another pattern that I've found useful:

Subclass NSManagedObjectContext:

@interface MyManagedObjectContext : NSManagedObjectContext

+ (MyManagedObjectContext *)mainThreadContext;

@end

i.e. a singleton context for the UI / main thread. This is cleaner than using the App delegate, since other classes won't have to get to the App delegate, but can use this class directly. Also the initialization of the store and model can be encapsulated into this class:

@implementation MyManagedObjectContext

+ (MyManagedObjectContext *)mainThreadContext;
{
   static MyManagedObjectContext *moc;
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       moc = [[self alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
       // Setup persistent store coordinator here
   });
   return moc;
}

@end
Daniel Eggert
  • 6,665
  • 2
  • 25
  • 41
  • That's very true. However, with Apple's way of building the Core Data stack in the Application Delegate, if your window's root view controller is a tab bar controller, you're still doing semi-weird (IMHO) contortions to pass the context (or managed objects) to the tab bar's owned view controllers. It works, but it feels like casting a wide, overly generalized net to solve the problem. Especially if the UITabBarController has UINavigationControllers as children. Still, though, thanks for your answer which will add to SO users' knowledge when they view this question. :) – Eric G Dec 27 '11 at 02:08
5

I see that some time has passed since you posted your question, but I have a different approach that I would like to share with you and everybody else.

I assume that you want to inject the managed object context in all/some of the view controllers that are shown as tabs of the UITabViewController and that you are using a Storyboard with a UITabBarController as the rootViewController.

  1. In your AppDelegate header make your AppDelegate to implement the UITabBarControllerDelegate protocol.

    @interface AppDelegate : UIResponder <UIApplicationDelegate, UITabBarControllerDelegate>
    ...
  2. In your AppDelegate implementation add the following UITabBarControllerDelegate method. It will take care of setting the managed object context in any view controller that has the property.

    - (BOOL) tabBarController:(UITabBarController *)tabBarController
    shouldSelectViewController:(UIViewController *)viewController {
    if ([viewController respondsToSelector:@selector(setManagedObjectContext:)]) {
        if ([viewController performSelector:@selector(managedObjectContext)] == nil) {
            [viewController performSelector:@selector(setManagedObjectContext:) withObject:self.managedObjectContext];
        }
    }
    return YES;
    }
  3. In your application:didFinishLaunchingWithOptions: set self as the UITabBarController delegate.

    UITabBarController *tabBarController = (UITabBarController *) self.window.rootViewController;
    tabBarController.delegate = self;
  4. Sadly the first view controller to be loaded is not ready at that time (in tabBarController.selectedViewController), and doesn't invoke the delegate either. So the easiest way to set the first one is to observe the selectedViewController property of the TabBarController

    [tabBarController addObserver:self forKeyPath:@"selectedViewController"
                          options:NSKeyValueChangeSetting context:nil];

    and set it there.

    - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    [object removeObserver:self forKeyPath:@"selectedViewController"];
    UIViewController *viewController = [change valueForKey:NSKeyValueChangeNewKey];
    if ([viewController respondsToSelector:@selector(setManagedObjectContext:)]) {
        if ([viewController performSelector:@selector(managedObjectContext)] == nil) {
            [viewController performSelector:@selector(setManagedObjectContext:) withObject:self.managedObjectContext];
        }
    }
    }
Jorge Ortiz
  • 1,556
  • 1
  • 16
  • 19
1

I would create a core data provider class, which is a singleton. This class could either just provide the persistent store so that each view controller can create it's own context if needed. Or you could use the provider class more like a manager and return new (if needed) context. It should of course also setup a merge notification to each context so that you can listen to changes between threads.

With this setup each view controller can ask the provider/manager for the context and the provider/manager will handle everything internally and return a context for the view controller.

What do you think?

Markus Persson
  • 1,103
  • 9
  • 21
  • What I don't like about that is that each view controller will have its own managed object context's perspective of the data. And even if you don't use a new context in each VC, you're still having to go to a singleton for the shared context.. which basically means you're hard coding a tighter coupling between the VCs and the singleton. This is one reason dependency injection is nicer: it's more flexible, and each VC can be naive as to how it got the context; it just uses the context it is given. – Eric G Dec 25 '11 at 10:11
  • Well isn't this pretty much dependency injection? – lbrndnr Dec 25 '11 at 14:16
  • 1
    No, Larcus94, that'd be dependency non-injection. :) If an instance looks to a "known" place for something, it's just resolving a dependency via tight coupling with the "known" place. What if you change the place? Or want to use a different place for just one view controller? Or run unit tests against a new data store? Or test a new persistent store coordinator? I think it's best for view controllers to naively use whatever context they are given, for flexibility and maintainability. – Eric G Dec 25 '11 at 23:35
0

IMHO I find it more elegant not to do Core Data heavy lifting in a ViewController. I'd suggest that you encapsulate data manipulation into an object and let the object reference/encapsulate your Core Data stack.

When your application and datamodel grows, you want to be really strict with how you pass MOCs. This becomes even more important if youre using concurrency and have to deal with multiple MOCs. Besides, your ViewControllers only need a MOC for FetchedResultsControllers. In most other cases youre fine passing ManagedObjects.

Mani
  • 1,597
  • 15
  • 19
  • Another good point. Still have to be careful that you're not introducing unnecessary abstraction, though. I agree with you, though. My non-Core Data projects always have persistent object handling done in their own encapsulated places. I think starting with Apple's getting started documentation leads people down the path of view controller-based access of the contexts. Probably Apple did this for simplicity's sake. – Eric G Dec 28 '11 at 07:21
  • 1
    I have a couple of apps that have evolved for two years. I began with Apples take on Core Data and found very quickly that it doesnt scale/age very well. – Mani Dec 28 '11 at 11:46