David
David

Reputation: 874

iOS 8+ Core Data Faults When Resuming App From Background

This issue only occurs in iOS 8 or higher. I have a core data managed object that I'm assigning to the property of a UIViewController. Whenever I leave the app and resume it from the background, the managed object faults. Whenever I attempt to access a property on the object, the fault does not fire and all of the data returns nil.

I set an observer for UIApplicationWillEnterForegroundNotification to examine the selectedObject, and at that point in the code execution the object has not faulted yet. It only faults following the app entering the foreground. Does anyone have any idea what could be happening here?

Edit #1:

Here's more of the relevant files. Note these have been simplified and variable names changed to protect the original code:

myAppDelegate.h

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

myAppDelegate.m

@synthesize managedObjectContext = __managedObjectContext;

- (NSManagedObjectContext *)managedObjectContext
{
    if (__managedObjectContext != nil)
    {
        return __managedObjectContext;
    }

    // This code only gets hit the first time the app tries to access the context.
    // After that (including when the app resumes from the background), the one stored in __managedObjectContext is returned.
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil)
    {
        __managedObjectContext = [[NSManagedObjectContext alloc] init];
        [__managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return __managedObjectContext;
}

DetailView.h

@property (retain, nonatomic) MyObject *selectedObject;

DetailView.m

@synthesize selectedObject;

- (void)viewDidLoad
{
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(willEnterForeground)
                                                 name:UIApplicationWillEnterForegroundNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(didBecomeActive)
                                                 name:UIApplicationDidBecomeActiveNotification
                                               object:nil];

- (void)willEnterForeground
{
    NSLog(@"App will enter foreground."); // breakpoint here
}

- (void)didBecomeActive
{
    NSLog(@"App became active."); // breakpoint here
}

- (void)viewWillAppear:(BOOL)animated
{
    NSManagedObjectContext *context = ((myAppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext;
    NSError *error = nil;
    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"MyObject" inManagedObjectContext:context];
    NSPredicate *predicate  = [NSPredicate predicateWithFormat:@"(selected == YES)", nil];
    [request setEntity:entity];
    [request setPredicate:predicate];
    selectedObject = (MyObject*)[[context executeFetchRequest:request error:&error] objectAtIndex:0];
}

- (IBAction)buttonPressed:(id)sender
{
    NSString *link = [selectedObject link]; // breakpoint here
}

When I hit the breakpoints in willEnterForeground and didBecomeActive I check the selectedObject:

MyObject: 0x7fa801c80bc0 (entity: MyObject; id: 0xd000000020940002 x-coredata://9775DE4D-2312-4684-904B-613302AC2B19/MyObject/p2085 ; data: { link = "http://www.example.com" }

I also check [selectedProperty managedObjectContext] and the [myAppDelegate managedObjectContext], both give me the following:

NSManagedObjectContext: 0x7fa7fbc96c90

Now if I click the button that is bound to buttonPressed: and re-check everything, [myAppDelegate managedObjectContext] still gives me the same output above, but [selectedObject managedObjectContext] is nil and examining the object gives the following:

MyObject: 0x7fa801c80bc0 (entity: MyObject; id: 0xd000000020940002 x-coredata://9775DE4D-2312-4684-904B-613302AC2B19/MyObject/p2085 ; data: fault)

And when [selectedObject link] is accessed it returns nil. To the best of my knowledge, none of my code is being run following the willEnterForeground and didBecomeActive methods when the app resumes from the background.

Upvotes: 0

Views: 305

Answers (3)

David
David

Reputation: 874

The problem turned out to be some of my code was being executed inadvertently. The main view of my app has a MKMapView displayed. For some reason on iOS 8+ when the app resumed from the background, the regionDidChangeAnimated event was being fired even though the region had not changed and this view wasn't even being displayed.

There was code running for regionDidChangeAnimated that reloads the map data from a web service and stores it in core data (hence why my core data was perma-faulted). This was only meant to be fired when the map was changed via user interaction or code I explicitly wrote to change the map region.

My guess is the map is doing some caching and when it resumes from the background it resets the region which is causing the event to fire inadvertently. To work around this, I added a check to ensure the view is currently displayed.

- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
    if (self.navigationController.visibleViewController != self)
    {
        return;
    }

    // Update map logic here...

It seems strange to me that there wouldn't be some check in place to prevent this event from firing when retrieving from a cached state, but c'est la vie.

Upvotes: 0

LombaX
LombaX

Reputation: 17364

Reading comments in another answer, I see this comment from the OP:

The app delegate isn't returning nil, its returning the same context at the same memory address. Only the [selectedObject managedObjectContext] is returning nil.

If it's true, it means that your app, at some point, is updating its [AppDelegate context] property (probably allocating a new one, for this you should show your AppDelegate code). The previous context is deallocated (since NSManagedObject doesn't retain its own context), the object points to a non valid / nil reference and for this reason is unable to fire the fault.

Upvotes: 0

Mundi
Mundi

Reputation: 80273

The managedObjectContext of a NSManagedObject is not guaranteed. If you need it, you have to also store a strong reference to the context.

If upon resume your data has become stale (you cannot access the stored object's properties), re-fetch in viewWillAppear.

Upvotes: 2

Related Questions