Reputation: 874
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?
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
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
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
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