6

Using storyboards you have no easy access to the first view controller in appDelegate (though once you do prepareForSegue makes it easy to pass the ManagedObjectContext down the navigation stack.

I've settled on giving each view controller (or superclass of each view controller) requiring Core Data access a moc member:

@synthesize moc = _moc;
@property (nonatomic) __weak NSManagedObjectContext *moc;

I'm uneasy about it because it doesn't seem a very elegant way to do it - too much code. But assigning directly requires specifying absolute indexes into the viewControllers arrays and changing appDelegate every time the requirement for ManagedObjectContexts change

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController;

    // rootView gets a tab bar controller
    for(UINavigationController *navController in tabBarController.viewControllers) {

        for(UIViewController *viewController in navController.viewControllers) {

            if([viewController respondsToSelector:@selector(setMoc:)]) {
                [viewController performSelector:@selector(setMoc:) withObject:self.managedObjectContext];
                NSLog(@"Passed moc to %@", [viewController description]); 
            }
        }
    }

    return YES;
}

What are the pitfalls of this approach and is there a better way? Is it better to try and be more generic:

- (void)assignManagedObjectContextIfResponds:(UIViewController *)viewController {

    if([viewController respondsToSelector:@selector(setMoc:)]) {
        [viewController performSelector:@selector(setMoc:) withObject:self.managedObjectContext];
        NSLog(@"Passed moc to %@", [viewController description]); 
    }

}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSMutableArray *viewControllers = [NSMutableArray array];

    UIViewController *firstLevelViewController = self.window.rootViewController;

    if([firstLevelViewController respondsToSelector:@selector(viewControllers)]) {

        NSArray *firstLevelViewControllers = [firstLevelViewController performSelector:@selector(viewControllers)];

        for(UIViewController *secondLevelViewController in firstLevelViewControllers) {

            if([secondLevelViewController respondsToSelector:@selector(viewControllers)]) {

                NSArray *secondLevelViewControllers = [secondLevelViewController performSelector:@selector(viewControllers)];

                for(UIViewController *thirdLevelViewController in secondLevelViewControllers) {

                    [viewControllers addObject:thirdLevelViewController];
                }

            } else {
                [viewControllers addObject:secondLevelViewController];
            }
        }
    } else {
        // this is the simple case, just one view controller as root
        [viewControllers addObject:firstLevelViewController];
    }

    // iterate over all the collected top-level view controllers and assign moc to them if they respond
    for(UIViewController *viewController in viewControllers) {
        [self assignManagedObjectContextIfResponds:viewController];
    }

    return YES;
}
Adam Eberbach
  • 12,309
  • 6
  • 62
  • 114

2 Answers2

4

Don't know if I understood properly, but why don't you left the managed object context directly in AppDelegate class and leave there all the logic for instantiate. And from then you can ask for it.

@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;

then you can recall it anytime from anywhere.

NSManagedObjectContext *moc = [(YourApplicationDelegate*)[[UIApplication sharedApplication] delegate] managedObjectContext];

For convenience I declared a define for it:

#define MOC [(YourApplicationDelegate*)[[UIApplication sharedApplication] delegate] managedObjectContext]

Therefore this become:

[MOC save:&error];

You can take this everywhere you like. Just try to have a look at the auto generated code for a CoreData application in Xcode, you will see that many accessors with CoreData are in there, and the CoreData itself is lazily initialized at first request.

Leonardo
  • 9,607
  • 17
  • 49
  • 89
  • 1
    I know you can do that but I am trying to avoid it. Can't articulate clearly why it is wrong but it feels that way, #importing AppDelegate everywhere. – Adam Eberbach Mar 01 '12 at 02:29
  • 1
    It is not wrong, it is the way of doing, there's no point in passing the context when you can access it easily. The import is only needed for avoid compiler warning (not error), because anytime you could call [UIApplication sharedApplication] from anywhere in your code and it would works regarding the import, this is also what the Xcode generate for a CoreData app, it amaze me why you state it's wrong, and prefer to pass the context in such a way. By centralizing the CoreData accessors you also gain more control for save error, lazy loading, whatever you like. – Leonardo Mar 01 '12 at 06:44
  • http://cocoawithlove.com/2008/11/singletons-appdelegates-and-top-level.html "wrong" was perhaps not the right word. I'm looking for optimal. – Adam Eberbach Mar 01 '12 at 22:40
  • 3
    Here is the answer: `Nested contexts make it more important than ever that you adopt the “pass the baton” approach of accessing a context (by passing a context from one view controller to the next) rather than retrieving it directly from the application delegate.` From here [Nested Managed Object Contexts](http://developer.apple.com/library/mac/releasenotes/DataManagement/RN-CoreData/_index.html#//apple_ref/doc/uid/TP40010637-CH1-SW1) – DanSkeel Aug 27 '12 at 21:40
  • Ok, this is related specifically with nested context, with one context only, it is safe enough to get it from AppDelegate. – Leonardo Aug 28 '12 at 05:42
  • Leonardo: sorry, but accessing your MOC everywhere through the app delegate singleton is *not* the right thing to do. All it does is create tightly coupled code that's harder to test and turn your app delegate into a poor mans service locator. Apple explicitly say you should pass your context to the objects that need them. It's just good object oriented design. – Luke Redpath Dec 15 '12 at 01:14
  • No need to be sorry, it just a technical discussion, and it is nice to share opinion. Tightly coupling, for me, just happen when a view controller rely on its predecessor to do its work, i.e. passing context. Also, the passed context it's the same of the one retrieved by the App Delegate, or an utility class. So what's the point of passing an object ? By the way, I think it depends on the complexity of the app. For most of them it is enough to have the ctx in only one place. – Leonardo Dec 15 '12 at 07:38
  • Leo (and all who follow) the reason that retrieval rather than pass in is a poor design pattern comes from the issues that it creates with encapsulation. With your pattern each viewController must know something about the context (eg. is there more than one. What is the name of the variable, ect..) If any of these things change 1) all controllers will need to be updated. 2) deciding on which ctx you will need to retrieve can be a complex state based decision that has more to do with the app as a whole, breaking encapsulation Solution: In root controllers, reach out, in all others pass down. – Nathan Jun 15 '15 at 07:03
  • Nathan, I like that summary, "In root controllers, reach out, in all others pass down," it seems clear. Could one even go a step farther and say "Pass down everywhere, even to the root controller"? I've seen examples where the moc is seeded in the root controller by the AppDelegate in didFinishLaunchingWithOptions. The root controller is referenced by mainStoryboard.instantiateViewControllerWithIdentifier, requiring a storyboard id assigned to the root controller. – protasm Jan 03 '16 at 15:27
2

Adam,

Whilst I was exploring storyboards I pretty much did it the same way you did except I made each of my view controllers that had a MOC property conform to a protocol.

There's nothing significantly different there, so I'll move on.

I think the point is Storyboards, IMO, are half-baked. Coming from a .Net background what is obviously missing is an object builder framework coupled with an IoC container. When Apple add that Storyboards will be awesome. When the storyboard framework can look at the destinationViewController, determine it's dependencies and resolve those from a container life will be great. For now, all it can really do is look at the destinationViewController and init you a generic one, which is of limited use.

Unfortunately, because it's a half-baked solution I'm sticking with the traditional approach for now so all my view controllers are alloc'd and init'd manually and more importantly I've added a method to each view controller to initWithMOC:(MOC *)moc;

The architect in me is telling me this code is more robust, I guess it's a matter of opinion as to whether it's worth the trade-off.

Anyone else come up with a better way?

CA.

Chris A
  • 116
  • 1
  • 7