ArrayController deleting from tableview but not CoreData - objective-c

I have a pretty simple setup.
I have a NSTableView within my MainWindow.xib, whose value is bound to an ArrayController like so:
The ArrayController is setup like this:
I have a TestModel.xcdatamodeld which contains one entity, Test, with one attribute, a body with type of string.
I then have my window set up simply like this, with a textfield and two buttons to add and remove from the array controller:
When I add or remove any entry into the tableview, it works fine. But when I close and restart my app, those changes aren't synced to the core data.
What am I doing wrong?
Thanks
Edit: I'm using Magical Record, too.

Did you save changes with the context?
Something like:
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}

Related

Use NSManagedObject's own context property to delete it?

It may be kinda naive but I was wondering if it is correct to use the following statement to delete a managed object from the persistent store of Core Data:
[managedObject.managedObjectContext deleteObject:managedObject];
I ran the above in Xcode debugger - it didn't complain but the object's content was still there. Could it be that context was referenced through the object to be deleted, and thus causing a memory lock preventing deletion of the object?
In regards to your content persisting, you still need to call save: on the context after deleting the object.
I can't answer specifically if you will have an issue by referencing the managedObjectContext in the managedObject as I usually use a 'DataManager' to manage my managedObjectContext. Below is an example of a delete method that I used in one of my dataManagers:
- (void)deleteReport:(Report*)aReport inContext:(NSManagedObjectContext*)context {
if (aReport != nil) {
if (context == nil) {
context = self.managedObjectContext;
}
context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;
[context deleteObject:aReport];
NSError *error = nil;
[context save:&error];
if (error) {
//NSLog(#"%#", error);
}
}}
EDIT: For clarification, the Report in this method is an instance of NSManagedObject, and the method takes NSManagedObjectContext as a parameter, because the application that it was pulled from supports the use of multiple contexts.

Dealing with background location updates and Core Data file protection

I've been experimenting with CLLocationManager's startMonitoringSignificantLocationChanges and I've run into some problems with Core Data. It turns out that since iOS 5.0, Core Data defaults to using NSFileProtectionCompleteUntilFirstUserAuthentication. This means that if a passcode is set, the persistent store is unavailable from the time the device is turned on until the time the passcode is first entered. If you're using location updates, it's possible your app may get launched during that time, and Core Data will get an error trying to load the persistent store.
Obviously switching to NSFileProtectionNone would be the easiest way to solve this. I'd prefer not to though—I'm not storing anything super sensitive in the database, but these location updates aren't super critical either.
I know I can use [[UIApplication sharedApplication] isProtectedDataAvailable] to check whether the data has been unlocked yet, and I can use applicationProtectedDataWillBecomeUnavailable: in my application delegate to respond appropriately once it is unlocked. This seems messy to me though—I'll have to add in a bunch of extra checks to make sure nothing goes wrong if the persistent store is unavailable, re-setup a bunch of things once it does become available, and so on. And that extra code doesn't offer much benefit—the app still won't be able to do anything if it launches in this state.
So I guess I'm just not sure which is the more "proper" way to deal this:
Switch to NSFileProtectionNone.
Add in the extra checks to skip over things if the store is unavailable, and use applicationProtectedDataWillBecomeUnavailable: to set things up again once it is.
If the app is launched in the background ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) and protected data is unavailable ([[UIApplication sharedApplication] isProtectedDataAvailable] == NO)) just call exit(0) (or something similar) to quit the app. On one hand this seems like the simplest solution, and I don't really see any downsides. But it also seems… "wrong"? I guess I can't decide if it's a clean solution or just a lazy one.
Something else I'm just not thinking of?
After thinking this over for a while I've come up with a solution I'm happy with. One thing to consider with the exit(0) option is that if the user takes a while to unlock the device, the app could be continually loading, quitting, and reloading. Whereas if you simply prevent the app from doing much, it will probably only have to load once, and will most likely be more efficient. So I decided to try my option 3 and see how messy it really was. It turned out to be simpler than I thought.
First I added a BOOL setupComplete property to my app delegate. This gives me an easy way to check if the app was fully launched at various points. Then in application:didFinishLaunchingWithOptions: I attempt to initialize the managed object context, then do something like this:
NSManagedObjectContext *moc = [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
} else {
UIApplication *app = [UIApplication sharedApplication];
if ([app applicationState] == UIApplicationStateBackground && ![app isProtectedDataAvailable]) {
[app beginIgnoringInteractionEvents];
} else [self presentErrorWithTitle:#"There was an error opening the database."];
}
setupWithManagedObjectContext: is just a custom method that finishes setting up. I'm not sure the beginIgnoringInteractionEvents is necessary, but I added it to be on the safe side. That way when the app is brought to the front, I can be sure the interface is frozen until setup is complete. It might avoid a crash if an eager user is tapping anxiously.
Then in applicationProtectedDataDidBecomeAvailable: I call something like this:
if (!self.setupComplete) {
NSManagedObjectContext *moc = [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
UIApplication *app = [UIApplication sharedApplication];
if ([app isIgnoringInteractionEvents]) [app endIgnoringInteractionEvents];
} else [self presentErrorWithTitle:#"There was an error opening the database."];
}
That finishes the setup and re-enables the interface. That's most of the work, but you'll also need to check through your other code to make sure nothing that relies on Core Data is getting called before your persistent store is available. One thing to watch out for is that applicationWillEnterForeground and applicationDidBecomeActive may get called before applicationProtectedDataDidBecomeAvailable if the user launches the app from this background state. So in various places I've added if (self.setupComplete) { … } to make sure nothing runs before it's ready. I also had a couple of places where I needed to refresh the interface once the database was loaded.
In order to (partially) test this without a lot of driving around, I temporarily modified application:didFinishLaunchingWithOptions: to not set up the database:
NSManagedObjectContext *moc = nil; // [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
} else {
UIApplication *app = [UIApplication sharedApplication];
// if ([app applicationState] == UIApplicationStateBackground && ![app isProtectedDataAvailable]) {
[app beginIgnoringInteractionEvents];
// } else [self presentErrorWithTitle:#"There was an error opening the database."];
}
Then I moved my code in applicationProtectedDataDidBecomeAvailable: over to applicationWillEnterForeground:. That way I could launch the app, make sure nothing unexpected happens, press the home button, open the app again, and make sure everything was working. Since the actual code requires moving a significant distance and waiting five minutes each time, this gave me a good way to approximate what was happening.
One last thing that tripped me up was my persistent store coordinator. A typical implementation might look something like this:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Test.sqlite"];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _persistentStoreCoordinator;
}
This is loosely based on Apple's sample code, which does explain in comments that you need to handle the error appropriately. My own code does a bit more than this, but one thing I hadn't considered is that if there's an error loading the persistent store, this will return a non-nil result! That was allowing all my other code to proceed as though it was working correctly. And even if persistentStoreCoordinator was called again, it would just return the same coordinator, without a valid store, instead of trying to load the store again. There are various ways you could deal with this, but to me it seemed best to not set _persistentStoreCoordinator unless it was able to add the store:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Test.sqlite"];
NSError *error = nil;
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if ([coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
_persistentStoreCoordinator = coordinator;
} else {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _persistentStoreCoordinator;
}
I have experienced, that you have to check
[[UIApplication sharedApplication] isProtectedDataAvailable]
and process
applicationProtectedDataWillBecomeUnavailable
to be sure you don't access a protected file.
Checking for
managedObjectContext
did not work for me.

MagicalRecord + Core Data not finding objects between contexts despite objectID string comparison working fine

I have an NSFetchedResultsController that queries on a Core Data entity, 'MyGalleryPhoto'.
I'm trying to delete some objects, and coming up against some problems. I'm using MagicalRecord. Here is my original attempt at the code, which in my view should work fine. At the point the code is run, the objects definitely exist, because they display in the fetchedResultsController.
[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext) {
for (MyGalleryPhoto *myGalleryPhoto in [self.fetchedResultsController.fetchedObjects objectsAtIndexes: self.selectedIndexes]) {
NSError *error = nil;
MyGalleryPhoto *localMyGalleryPhoto = (MyGalleryPhoto *) [localContext existingObjectWithID: myGalleryPhoto.objectID error: &error];
NSLog(#"error: %#:%#", [error localizedDescription], [error userInfo]);
NSLog(#"mygp: %#", [localMyGalleryPhoto description]);
[localMyGalleryPhoto deleteInContext: localContext];
}
} completion:^(void){
}];
This code does not work. The myGalleryPhoto entry is not found and the error returned is: "The operation couldn’t be completed. (Cocoa error 133000.)" I've also tried using MR_inContext, which just calls existingObjectWithId:error:.
After a lot of messing around I've come up with this vile frankenstein's monster, that gets all the records out of the entity and compares the string representations of the ObjectIDs. This works fine. Why? I'm using a copy of MagicalRecord I downloaded from GitHub today, XCode up to date, latest SDK, et cetera.
[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext) {
NSArray *allMyGalleryPhotos = [MyGalleryPhoto findAllInContext: localContext];
for (MyGalleryPhoto *myGalleryPhoto in [self.fetchedResultsController.fetchedObjects objectsAtIndexes: self.selectedIndexes]) {
MyGalleryPhoto *myGalleryPhotoToDelete = nil;
for (MyGalleryPhoto *existingMyGalleryPhoto in allMyGalleryPhotos) {
NSString *existingURLString = [[existingMyGalleryPhoto.objectID URIRepresentation] absoluteString];
NSString *URLString = [[myGalleryPhoto.objectID URIRepresentation] absoluteString];
NSLog(#"ExistingURLString: %#", existingURLString);
NSLog(#"URLString: %#", URLString);
if ([URLString isEqualToString: existingURLString]) {
myGalleryPhotoToDelete = existingMyGalleryPhoto;
}
}
if (myGalleryPhotoToDelete) [myGalleryPhotoToDelete deleteInContext: localContext];
}
} completion:^(void){
}];
Cocoa Error 13000 is a Referential Integrity error, as described in the documentation. That means you're looking for an object that doesn't exist in the store. On a more practical level, that means that your contexts (I'm assuming you have more than one Managed Object Context) are not in sync. That is, you've added a new object to one context, while the other doesn't have that object because the previous context has not been saved.
Regarding your code, the first problem I see in the first example is that you are crossing thread boundaries at the very start. The fetchedResultsController has a reference to objects in another context (I'm going to assume the default context). Every time saveInBackground is called, it gives you a new context to use, but it also puts that block of code on a background thread. Crossing thread boundaries, even in the new version of Core Data, is going to give you crazy, hard to track down problems at random times.
The gist if what you're trying to do in the first (simpler) block of code is you have a collection of photo objects you want to remove from your application. I would do something like this instead:
NSPredicate *objectsToDelete = [NSPredicate predicateWithFormat:#"self in %#", self.fetchedResultsController.fetchedObjects];
[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *)localContext
{
[MyGalleryPhoto deleteAllMatchingPredicate:objectsToDelete inContext:localContext];
}];
The deleteAllMatchingPredicate method should do a lookup of the objects in the correct context (which you weren't doing in the first block of code) so they can be deleted. It also sets up the objects to load as faults, so we're not going to load everything in memory, only to delete it immediately. It'll load only what it needs, and no more.
I would not use existingObjectWithID: in this case. This method never loads faults. Your use case means it'll load the entire object into memory, only to delete it anyway.

App crashes when saving UIManagedDocument

I have an application that first loads some data into an UIManagedDocument, then executes saveToURL:forSaveOperation:completionHandler:. Inside the completionHandler block, it does an update of various elements of this database, and when it's done, it does another saving.
Besides that, the app has 3 buttons that reload the data, re-update the data, and delete one entity of the database, respectively. In every button method, the last instruction is a saving as well.
When I run all this in the simulator, all goes smoothly. But in the device doesn't. It constantly crashes. I have observed that, normally, it crashes when pressing the "delete" button, or when reloading or re-updating the database. And it's always in the saveToURL operation.
In my opinion, the problem comes when there are multiple threads saving the database. As the device executes the code slower, maybe multiple savings come at same time and the app can't handle them correctly. Also, sometimes the delete button doesn't delete the entity, and says that doesn't exist (when it does).
I'm totally puzzled with this, and all this saving operations must be done...In fact, if I remove them, the app behaves even more incoherently.
Any suggestions of what could I do to resolve this problem? Thank you very much!
[Edit] Here I post the problematic code. For first loading the data, I use a helper class, with this two methods in particular:
+ (void)loadDataIntoDatabase:(UIManagedDocument *)database
{
[database.managedObjectContext performBlock:^{
// Read from de plist file and fill the database
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
[DataHelper completeDataOfDatabase:database];
}];
}
+ (void)completeDataOfDatabase:(UIManagedDocument *)database
{
[database.managedObjectContext performBlock:^{
// Read from another plist file and update some parameters of the already existent data (uses NSFetchRequest and works well)
// [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
[database updateChangeCount:UIDocumentChangeDone];
}];
}
And in the view, I have 3 action methods, like these:
- (IBAction)deleteButton {
[self.database.managedObjectContext performBlock:^{
NSManagedObject *results = ;// The item to delete
[self.database.managedObjectContext deleteObject:results];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}];
}
- (IBAction)reloadExtraDataButton {
[DataHelper loadDataIntoDatabase:self.database];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}
- (IBAction)refreshDataButton {
[DataHelper completeDataOfDatabase:self.database];
//[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}
[Edit 2] More code: First of all, the initial view executes viewDidLoad this way:
- (void)viewDidLoad{
[super viewDidLoad];
self.database = [DataHelper openDatabaseAndUseBlock:^{
[self setupFetchedResultsController];
}];
}
This is what the setupFetchedResultsController method looks like:
- (void)setupFetchedResultsController
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Some entity name"];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.database.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
}
Each view of the app (it has tabs) has a different setupFetchedResultsController in order to show the different entities the database contains.
Now, in the helper class, this is the first class method that gets executed, via the viewDidLoad of each view:
+ (UIManagedDocument *)openDatabaseAndUseBlock:(completion_block_t)completionBlock
{
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Database"];
UIManagedDocument *database = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[database.fileURL path]]) {
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self loadDataIntoDatabase:database];
completionBlock();
}];
} else if (database.documentState == UIDocumentStateClosed) {
// Existe, pero cerrado -> Abrir
[database openWithCompletionHandler:^(BOOL success) {
[self loadDataIntoDatabase:database];
completionBlock();
}];
} else if (database.documentState == UIDocumentStateNormal) {
[self loadDataIntoDatabase:database];
completionBlock();
}
return database;
}
You didn't really provide much code. The only real clue you gave was that you are using multiple threads.
UIManagedDocument has two ManagedObjectContexts (one specified for the main queue, and the other for a private queue), but they still must each only be accessed from within their own thread.
Thus, you must only use managedDocument.managedObjectContext from within the main thread. If you want to use it from another thread, you have to use either performBlock or performBlockAndWait. Similarly, you can never know you are running on the private thread for the parent context, so if you want to do something specifically to the parent, you must use performBlock*.
Finally, you really should not be calling saveToURL, except when you initially create the database. UIManagedDocument will auto-save (in its own time).
If you want to encourage it to save earlier, you can send it updateChangeCount: UIDocumentChangeDone to tell it that it has changes that need to be saved.
EDIT
You should only call saveToURL when you create the file for the very first time. With UIManagedDocument, there is no need to call it again (and it can actually cause some unintended issues).
Basically, when you create the document DO NOT set your iVar until the completion handler executes. Otherwise, you could be using a document in a partial state. In this case, use a helper, like this, in the completion handler.
- (void)_document:(UIManagedDocument*)doc canBeUsed:(BOOL)canBeUsed
{
dispatch_async(dispatch_get_main_queue(), ^{
if (canBeUsed) {
_document = doc;
// Now, the document is ready.
// Fire off a notification, or notify a delegate, and do whatever you
// want... you really should not use the document until it's ready, but
// as long as you leave it nil until it is ready any access will
// just correctly do nothing.
} else {
_document = nil;
// Do whatever you want if the document can not be used.
// Unfortunately, there is no way to get the actual error unless
// you subclass UIManagedDocument and override handleError
}
}];
}
And to initialize your document, something like...
- (id)initializeDocumentWithFileURL:(NSURL *)url
{
if (!url) {
url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Default_Project_Database"];
}
UIManagedDocument *doc = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[doc.fileURL path]]) {
// The file does not exist, so we need to create it at the proper URL
[doc saveToURL:doc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self _document:doc canBeUsed:success];
}];
} else if (doc.documentState == UIDocumentStateClosed) {
[doc openWithCompletionHandler:^(BOOL success) {
[self _document:doc canBeUsed:success];
}];
} else {
// You only need this if you allow a UIManagedDocument to be passed
// in to this object -- in which case the code above that initializes
// the <doc> variable will be conditional on what was passed...
BOOL success = doc.documentState == UIDocumentStateNormal;
[self _document:doc canBeUsed:success];
}
}
The "pattern" above is necessary to make sure you do not use the document until it is fully ready for use. Now, that piece of code should be the only time you call saveToURL.
Note that by definition, the document.managedObjectContext is of type NSMainQueueConcurrencyType. Thus, if you know your code is running on the main thread (like all your UI callbacks), you do not have to use performBlock.
However, if you are actually doing loads in the background, consider..
- (void)backgroundLoadDataIntoDocument:(UIManagedDocument*)document
{
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.parentContext = document.managedObjectContext;
[moc performBlock:^{
// Do your loading in here, and shove everything into the local MOC.
// If you are loading a lot of stuff from the 'net (or elsewhere),
// consider doing it in strides, so you deliver objects to the document
// a little at a time instead of all at the end.
// When ready to save, call save on this MOC. It will shove the data up
// into the MOC of the document.
NSrror *error = nil;
if ([moc save:&error]) {
// Probably don't have to synchronize calling updateChangeCount, but I do it anyway...
[document.managedObjectContext performBlockAndWait:^{
[document updateChangeCount:UIDocumentChangeDone];
}];
} else {
// Handle error
}
}];
}
Instead of parenting your background MOC to the mainMOC, you can parent it to the parentContext. Loading and then saving into it will put the changes "above" the main MOC. The main MOC will see those changes the next time it does a fetch operation (note the properties of NSFetchRequest).
NOTE: Some people have reported (and it also appears as a note in Erica Sadun's book), that after the very first saveToURL, you need to close, then open to get everything working right.
EDIT
This is getting really long. If you had more points, I'd suggest a chat. Actually, we can't do it through SO, but we could do it via another medium. I'll try to be brief, but please go back and reread what I posted, and pay careful attention because your code is still violating several tenants.
First, in viewDidLoad(), you are directly assigning your document to the result of calling openDatabaseAndUseBlock. The document is not in a usable state at that time. You do not want the document accessible until the completion handlers fire, which will not happen before openDatabaseAndUseBlock() returns.
Second, only call saveToURL the very first time you create your database (inside openDatabaseAndUseBlock()). Do not use it anywhere else.
Third. Register with the notification center to receive all events (just log them). This will greatly assist your debugging, because you can see what's happening.
Fourth, subclass UIManagedDocument, and override the handleError, and see if it is being called... it's the only way you will see the exact NSError if/when it happens.
3/4 are mainly to help you debug, not necessary for your production code.
I have an appointment, so have to stop now. However, address those issues, and here's on

NSFetchedResultsController not displaying changes from background thread

My app is using an NSFetchedResultsController tied to a Core Data store and it has worked well so far, but I am now trying to make the update code asynchronous and I am having issues. I have created an NSOperation sub-class to do my updates in and am successfully adding this new object to an NSOperationQueue. The updates code is executing as I expect it to and I have verified this through debug logs and by examining the SQLite store after it runs.
The problem is that after my background operation completes, the new (or updated) items do not appear in my UITableView. Based on my limited understanding, I believe that I need to notify the main managedObjectContext that changes have occurred so that they may be merged in. My notification is firing, nut no new items appear in the tableview. If I stop the app and restart it, the objects appear in the tableview, leading me to believe that they are being inserted to the core data store successfully but are not being merged into the managedObjectContext being used on the main thread.
I have included a sample of my operation's init, main and notification methods. Am I missing something important or maybe going about this in the wrong way? Any help would be greatly appreciated.
- (id)initWithDelegate:(AppDelegate *)theDelegate
{
if (!(self = [super init])) return nil;
delegate = theDelegate;
return self;
}
- (void)main
{
[self setUpdateContext:[self managedObjectContext]];
NSManagedObjectContext *mainMOC = [self newContextToMainStore];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:updateContext];
[self setMainContext:mainMOC];
// Create/update objects with mainContext.
NSError *error = nil;
if (![[self mainContext] save:&error]) {
DLog(#"Error saving event to CoreData store");
}
DLog(#"Core Data context saved");
}
- (void)contextDidSave:(NSNotification*)notification
{
DLog(#"Notification fired.");
SEL selector = #selector(mergeChangesFromContextDidSaveNotification:);
[[delegate managedObjectContext] performSelectorOnMainThread:selector
withObject:notification
waitUntilDone:YES];
}
While debugging, I examined the notification object that is being sent in contextDidSave: and it seems to contain all of the items that were added (excerpt below). This continues to make me think that the inserts/updates are happening correctly but somehow the merge is not being fired.
NSConcreteNotification 0x6b7b0b0 {name = NSManagingContextDidSaveChangesNotification; object = <NSManagedObjectContext: 0x5e8ab30>; userInfo = {
inserted = "{(\n <GCTeam: 0x6b77290> (entity: GCTeam; id: 0xdc5ea10 <x-coredata://F4091BAE-4B47-4F3A-A008-B6A35D7AB196/GCTeam/p1> ; data: {\n changed =
The method that receives your notification must indeed notify your context, you can try something like this, which is what I am doing in my application:
- (void)updateTable:(NSNotification *)saveNotification
{
if (fetchedResultsController == nil)
{
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
//Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
else
{
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
// Merging changes causes the fetched results controller to update its results
[context mergeChangesFromContextDidSaveNotification:saveNotification];
// Reload your table view data
[self.tableView reloadData];
}
}
Hope that helps.
Depending on the specifics of what you are doing, you may be going about this the wrong way.
For most cases, you can simply assign a delegate using NSFetchedResultsControllerDelegate. You provide an implementation for one of the methods specified in "respondingToChanges" depending on your needs, and then send the tableView a reloadData message.
The answer turned out to be unrelated to the posted code which ended up working as I expected. For reasons that I am still not entirely sure of, it had something to do with the first launch of the app. When I attempted to run my update operation on launches after the Core Data store was created, it worked as expected. I solved the problem by pre-loading a version of the sqlite database in the app so that it did not need to create an empty store on first launch. I wish I understood why this solved the problem, but I was planning on doing this either way. I am leaving this here in the hope that someone else may find it useful and not lose as much time as I did on this.
I've been running into a similar problem in the simulator. I was kicking off an update process when transitioning from the root table to the selected folder. The update process would update CoreData from a web server, save, then merge, but the data didn't show up. If I browsed back and forth a couple times it would show up eventually, and once it worked like clockwork (but I was never able to get that perfect run repeated). This gave me the idea that maybe it's a thread/event timing issue in the simulator, where the table is refreshing too fast or notifications just aren't being queued right or something along those lines. I decided to try running in Instruments to see if I could pinpoint the problem (all CoreData, CPU Monitor, Leaks, Allocations, Thread States, Dispatch, and a couple others). Every time I've done a "first run" with a blank slate since then it has worked perfectly. Maybe Instruments is slowing it down just enough?
Ultimately I need to test on the device to get an accurate test, and if the problem persists I will try your solution in the accepted answer (to create a base sql-lite db to load from).