I update core data in a background thread, like so:
entry.message = [self contentForNoteWithEDML:note.content];
entry.dataLastModified = [NSDate date];
[entry.managedObjectContext save:nil];
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
[self.tableView reloadData];
});
On each cell of the tableview, it displays a different entry from the fetchedResultsController. On the main thread, I do an NSLog in cellForRowAtIndexPath on the dataLastModified date, and the date doesn't change to the most recent value. If I close the app and run it again, it updates the contents of the cell and the dataLastModified date changes to the correct value.
It seems to be changing the data, as required, but my tableview isn't seeing the changes until the app is restarted. Any ideas why?
EDIT: Doing the NSLog in cellForRowAtIndexPath on a background thread gives the the correct data, but doing it on the main thread does not.
EDIT 2: How my background context works:
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter
addObserver:[AppDelegate applicationDelegate].coreDataManager
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:[AppDelegate applicationDelegate].coreDataManager.managedObjectContext];
NSPersistentStoreCoordinator *journalDataPSC = [AppDelegate applicationDelegate].coreDataManager.managedObjectContext.persistentStoreCoordinator;
dispatch_queue_t addOrUpdateEntriesQueue = dispatch_queue_create("com.App.AddOrUpdateEntries", NULL);
dispatch_async(addOrUpdateEntriesQueue, ^{
NSManagedObjectContext *journalDataMOC = [[NSManagedObjectContext alloc] init];
[journalDataMOC setPersistentStoreCoordinator:journalDataPSC];
//Some code to get me an entry on this context
entry.message = [self contentForNoteWithEDML:note.content];
entry.dataLastModified = [NSDate date];
[entry.managedObjectContext save:nil];
[[NSNotificationCenter defaultCenter] removeObserver:[AppDelegate applicationDelegate].coreDataManager];
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
[self.tableView reloadData];
});
});
dispatch_release(addOrUpdateEntriesQueue);
Make sure you're using MOC's correctly, they are not thread-safe and can only be used in the thread they were created in. In this case, if you're doing it right enter.managedObjectContext is different from the MOC of the fetched results controller (which is in the main thread).
That means saves in the background do not necessarily get propagated to he main thread MOC. Make sure you handle NSManagedObjectContextDidSaveNotification in the main thread by adding an observer when you create the fetched results controller.
Looking at your code here are a few points that I notice:
You should register for the save notification thrown by the background MOC not the main thread MOC.
You should initialize your background MOC with initWithConcurrencyType: using NSPrivateQueueConcurrencyType
Once you're using NSPrivateQueueConcurrencyType it's much better to use performBlock: to make your changes and save instead of using low-level GCD dispatch methods.
Even though I don't know anything about your coreDataManager object your notification registration is wrong. The object you want to observe is your background managed object context journalDataMOC, not your coreDataManager.
So this should work, but notice that you have to move the registration inside your addOrUpdateEntriesQueue:
[notificationCenter
addObserver:[AppDelegate applicationDelegate].coreDataManager
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:journalDataMOC];
But all in all you should use the CoreData API to do this work (like Engin said) as it is much cleaner. So remove all the GCD and notification stuff and use this snippet (not tested):
NSManagedObjectContext *mainContext = [[[AppDelegate applicationDelegate] coreDataManager] managedObjectContext];
NSManagedObjectContext *journalDataMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[journalDataMOC setParentContext:mainContext];
[journaDataMOC performBlock:^{
//Some code to get me an entry on this context
entry.message = [self contentForNoteWithEDML:note.content];
entry.dataLastModified = [NSDate date];
[journalDataMOC save:nil];
[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSError *error;
[mainContext save:&error];
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
[self.tableView reloadData];
}];
}];
When the journalDataMOC saves it will "push" the changes up to the paren context (mainContext).
Note that your [[[AppDelegate applicationDelegate] coreDataManager] managedObjectContext] has to be initialized with type NSMainQueueConcurrencyType. Also note that you have to save your mainContext here or at some time in the future to persist your changes to the database.
Related
I am using global dispatch queue to set up iCloud Coredata in my project. There is a strange problem. iCloud CoreData may take very long time to setup for the first time.
During this long period:
if the app keeps running in the forefront, and user can play with the UI
smoothly.
BUT if the app goes to background, and back to forefront again, the UI
hangs and sometimes cannot get the iCloud Core data set up properly,
(some data not merged).
Does the background process with dispatched queue comes to main thread when it hangs in the second scenario?
Another possible reason: I used a separate class called "DataManager" to handle all those CoreData methods, and it is normal subclass from NSObject.
While Apple's sample code put all those core data stuff in AppDelegate. Might it be the reason?
I have being struggling with the problem for three days. Please help me out. Thanks a lot.
- (NSPersistentStoreCoordinator *)cloud_persistentStoreCoordinator {
if (_cloud_persistentStoreCoordinator != nil) {
return _cloud_persistentStoreCoordinator;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:UBIQUITY_CONTAINER_URL];
if (!cloudURL) {
self.iCloudReady=NO;
self.iCloudCoreDataReady=YES;
DEBUGLog
return nil;
}
self.iCloudReady=YES;
_cloud_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
// prep the store path and bundle stuff here since NSBundle isn't totally thread safe
NSPersistentStoreCoordinator* psc = _cloud_persistentStoreCoordinator;
NSURL *storeUrl = [[self applicationLibraryDirectory] URLByAppendingPathComponent:#"cloud_accounts.sqlite"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL* coreDataCloudContentURL = [cloudURL URLByAppendingPathComponent:#"accounts_v1"];
// The API to turn on Core Data iCloud support here.
NSDictionary* options = [NSDictionary dictionaryWithObjectsAndKeys:#"com.moremore.cloudapp.keys", NSPersistentStoreUbiquitousContentNameKey, coreDataCloudContentURL, NSPersistentStoreUbiquitousContentURLKey, [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,nil];
NSError *error = nil;
[psc lock];
if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
Typical reasons for an error here include:
* The persistent store is not accessible
* The schema for the persistent store is incompatible with current managed object model
Check the error message to determine what the actual problem was.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[psc unlock];
// tell the UI on the main thread we finally added the store and then
// post a custom notification to make your views do whatever they need to such as tell their
// NSFetchedResultsController to -performFetch again now there is a real store
dispatch_async(dispatch_get_main_queue(), ^{
//user userDefaults to mark this will only be run once
NSUserDefaults *userDefault=[NSUserDefaults standardUserDefaults];
if ([userDefault objectForKey:#"oldCloudAccountsmoved"]==nil) {
AppDelegate * appdelegate=(AppDelegate *)[[UIApplication sharedApplication] delegate];
[appdelegate moveOld_Cloud_DatabasetoCoreData];
NSString * confirm=#"YES";
[userDefault setObject:confirm forKey:#"oldCloudAccountsmoved"];
[userDefault synchronize];
}
NSLog(#"asynchronously added persistent store!");
[[NSNotificationCenter defaultCenter] postNotificationName:#"coreData_iCloud_Ready" object:self userInfo:nil];
});
});
return _cloud_persistentStoreCoordinator;
}
To my opinion you should synchronize with iCloud as an asynchronous task. You can use [self performSelectorInBackground:#selector(syncWithIcloud) withObject:nil];
So that synchronizing will happen in background and UI will be free and you can do something with UI to make user busy or waiting.
I could use some assistance in debugging a EXC_BAD_ACCESS error received on the [context deleteObject:loan]; command. The error is received in the following delegate method:
- (void)didCancelNewLoan:(Loan *)loan {
// save the context
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:loan]; // *** EXC_BAD_ACCESS here ***
// This method is called from a the following method in a second class:
- (IBAction)cancel:(id)sender {
[delegate didCancelNewLoan:self.loan];
}
// The loan ivar is created by the original class
// in the below prepare for Segue method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"NewLoan"]) {
UINavigationController *navController = (UINavigationController *)[segue destinationViewController];
LoanViewController *loanView = (LoanViewController *)[[navController viewControllers] lastObject];
loanView.managedObjectContext = self.managedObjectContext;
loanView.delegate = self;
loanView.loan = [self createNewLoan];
loanView.newLoan = YES;
}
// Finally, the loan is created in the above
// method's [self createNewLoan] command:
- (NSManagedObject *)createNewLoan {
//create a new instance of the entity managed by the fetched results controller
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newManagedObject setValue:[NSDate date] forKey:#"timeStamp"];
CFUUIDRef uuid = CFUUIDCreate(NULL);
CFStringRef uuidstring = CFUUIDCreateString(NULL, uuid);
//NSString *identifierValue = (__bridge_transfer NSString *)uuidstring;
[newManagedObject setValue:(__bridge_transfer NSString *)uuidstring forKey:#"identifier"];
CFRelease(uuid);
CFRelease(uuidstring);
NSError *error;
[self.fetchedResultsController performFetch:&error];
NSLog(#"%i items in database", [[self.fetchedResultsController fetchedObjects] count]);
return newManagedObject;
}
Appreciate your looking at the above methods.
Guess #1: you are accessing a deallocated object. To debug: turn on zombies and see what happens.
Update: here's how you turn on zombies in Xcode 5:
Product > Scheme > Edit Scheme, select Diagnostics tab, check "Enable Zombie Objects"
for older Xcode
, edit your build settings, add and enable these arguments in your build scheme:
Guess #2: you have a multithreaded app and you are accessing a managed object context from different threads, which is a no no.
You can add an assert before your delete:
assert( [ NSThread isMainThread ] ) ;
From looking at your code above, there's nothing that stands out as being done incorrectly.
I am wondering whether you are dealing with two different managed object contexts without realising it? You will have to set some breakpoints where you create the Loan object and see if that might be the case.
Also why do you have to get a reference to the context via fetchedResultsController if you already have a declared property for it in self.managedObjectContext ?
The other thing is why do you need to call the fetchedResultsController to performFetch: again when you create a new Loan object? Is your data presented in a table view and have you implemented the NSFetchedResultsController delegate methods?
That call seems unnecessary and it may be causing issues with the cache created by the fetch. See section "Modifying the fetch request" under this link http://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsController_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40008227-CH1-SW24
Finally, try your delete operation directly in the view controller that received the action rather than pass it to the delegate (just to eliminate the possibility that something has been dealloc'd without you knowing).
Here's what I'd do:
- (IBAction)cancel:(id)sender
{
NSError *error;
NSManagedObjectContext *context = [self.loan managedObjectContext];
[context deleteObject:self.loan];
if (![context save:&error])
NSLog (#"Error saving context: %#", error);
}
I got a Bad Access because a deallocated UIViewController was a delegate of a NSFetchedResultsController it had.
The NSFetchedResultsController was deallocated - but when settings a delegate, it observes NSManagedObjectContext for changes, so when NSManagedObjectContext was saved - a bad access would occur when trying to notify the NSFetchedResultsController about the change.
Solution is to clear delegate of NSFetchedResultsController upon deallocation.
- (void)dealloc {
fetchedResultsController.delegate = nil;
}
I try to do the following simple thing:
NSArray * entities = [context executeFetchRequest:inFetchRequest error:&fetchError];
Nothing fancy. But this freezes in iOS 5, it works fine in iOS 4. I don't get exceptions, warnings or errors; my app just simply freezes.
Please help me out! I'm dying here! ;)
I don't know if you also use different Thread. If yes the issue comes from the fact that NSManagedObjects themselves are not thread-safe. Creating a ManagedContext on the main thread and using it on another thread freezes the thread.
Maybe this article can help you :
http://www.cimgf.com/2011/05/04/core-data-and-threads-without-the-headache/
Apple has a demo application for handling Coredata on several threads (usually main & background threads) : http://developer.apple.com/library/ios/#samplecode/TopSongs/Introduction/Intro.html
What I've done to solve this issue is :
In the application delegate : create the persistent store (one for all thread) and create the Coredata managed Context for the main thread,
In the background thread, create a new managed context (from same persistent store)
Notifications are used when saving, to let the mainContext know when background thread has finished (inserting rows or other).
There are several solutions, using a NSQueueOperation. For my case, I'm working with a while loop. Here is my code if it may help you. However, Apple documentation on concurrency and their Top Songs example application are good points to start.
in the application delegate :
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
self.cdw = [[CoreDataWrapper alloc] initWithPersistentStoreCoordinator:[self persistentStoreCoordinator] andDelegate:self];
remoteSync = [RemoteSync sharedInstance];
...
[self.window addSubview:navCtrl.view];
[viewController release];
[self.window makeKeyAndVisible];
return YES;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator == nil) {
NSURL *storeUrl = [NSURL fileURLWithPath:self.persistentStorePath];
NSLog(#"Core Data store path = \"%#\"", [storeUrl path]);
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[NSManagedObjectModel mergedModelFromBundles:nil]];
NSError *error = nil;
NSPersistentStore *persistentStore = [persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil error:&error];
NSAssert3(persistentStore != nil, #"Unhandled error adding persistent store in %s at line %d: %#", __FUNCTION__, __LINE__, [error localizedDescription]);
}
return persistentStoreCoordinator;
}
-(NSManagedObjectContext *)managedObjectContext {
if (managedObjectContext == nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
}
return managedObjectContext;
}
-(NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator == nil) {
NSURL *storeUrl = [NSURL fileURLWithPath:self.persistentStorePath];
NSLog(#"Core Data store path = \"%#\"", [storeUrl path]);
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[NSManagedObjectModel mergedModelFromBundles:nil]];
NSError *error = nil;
NSPersistentStore *persistentStore = [persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil error:&error];
NSAssert3(persistentStore != nil, #"Unhandled error adding persistent store in %s at line %d: %#", __FUNCTION__, __LINE__, [error localizedDescription]);
}
return persistentStoreCoordinator;
}
-(NSManagedObjectContext *)managedObjectContext {
if (managedObjectContext == nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
}
return managedObjectContext;
}
-(NSString *)persistentStorePath {
if (persistentStorePath == nil) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths lastObject];
persistentStorePath = [[documentsDirectory stringByAppendingPathComponent:#"mgobase.sqlite"] retain];
}
return persistentStorePath;
}
-(void)importerDidSave:(NSNotification *)saveNotification {
if ([NSThread isMainThread]) {
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:saveNotification];
} else {
[self performSelectorOnMainThread:#selector(importerDidSave:) withObject:saveNotification waitUntilDone:NO];
}
}
In the object running the background thread :
monitor = [[NSThread alloc] initWithTarget:self selector:#selector(keepMonitoring) object:nil];
-(void)keepMonitoring{
while(![[NSThread currentThread] isCancelled]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
AppDelegate * appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
//creating the cdw here will create also a new managedContext on this particular thread
cdwBackground = [[CoreDataWrapper alloc] initWithPersistentStoreCoordinator:appDelegate.persistentStoreCoordinator andDelegate:appDelegate];
...
}
}
Hope this help,
M.
Thanks for the hints given in this page on how to solve this freezing issue which appeared on upgrading from iOS4. It has been the most annoying problem I have found since I started programming on iOS.
I have found a quick solution for cases where there are just a few calls to the context from other threads.
I just use performSelectorOnMainThread:
[self performSelectorOnMainThread:#selector(stateChangeOnMainThread:) withObject: [NSDictionary dictionaryWithObjectsAndKeys:state, #"state", nil] waitUntilDone:YES];
To detect the places where the context is called from another thread you can put a breakpoint on the NSLog on the functions where you call the context as in the following piece of code and just use performSelectorOnMainThread on them.
if(![NSThread isMainThread]){
NSLog(#"Not the main thread...");
}
I hope that this may be helpful...
I had the same issue. If you run under the debugger and when the app "hangs" stop th app (use the "pause" button on the debugger. If you're at the executeFetchRequest line, then check the context variable. If it has a ivar _objectStoreLockCount and its greater than 1, then its waiting on a lock on the associated store.
Somewhere you're creating a race condition on your associated store.
This really sounds like trying to access a NSManagedObjectContext from a thread/queue other than the one that created it. As others suggested you need to look at your threading and make sure you are following Core Data's rules.
Executing fetch request must happen from the thread where context was created.
Remember it is not thread safe and trying to executeFetchRequest from another thread will cause unpredictable behavior.
In order to do this correctly, use
[context performBlock: ^{
NSArray * entities = [context executeFetchRequest:inFetchRequest error:&fetchError];
}];
This will executeFetchRequest in the same thread as context, which may or may not be the main thread.
In my case the app would freeze before 'executeFetchRequest' without any warning. The solution was to wrap all db operations in #synchronized(persistentStore).
Eg:
NSArray *objects;
#synchronized([self persistentStoreCoordinator]) {
objects = [moc executeFetchRequest:request error:&error];
}
Delete all object with fetchrequest doesn't work for me, the sqlite looks corrupted. the only way I found is
//Erase the persistent store from coordinator and also file manager.
NSPersistentStore *store = [self.persistentStoreCoordinator.persistentStores lastObject];
NSError *error = nil;
NSURL *storeURL = store.URL;
[self.persistentStoreCoordinator removePersistentStore:store error:&error];
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:&error];
//Make new persistent store for future saves (Taken From Above Answer)
if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
// do something with the error
}
Two hopefully minor questions regarding CoreData that I've been unable to find answers to:
1) I have a faulted object. Accessing an attribute as a property is not firing the fault, accessing the same property via KVC IS firing the fault. Any idea why?
i.e. object.title returns nil and object is still faulted, but [object valueForKey:#"title"] returns the title and the object is no longer a fault.
2) Updates to existing records have stopped working. Add/Delete works. Add/Update share the same code path (one is passed the existing object, the other a newly inserted object). However Update wont work. The data in the updated object is correct and set to the new values and the save succeeds with no errors, but the record in the database remains unchanged. Any idea?
NB: There is only one NSManagedObjectContext
Cheers
couldn't tell much from your description without code.
however it looks like you have updated the object in ram but the update wasn't submitted to the database layer making the physical change.
EDIT:
Yes, "Add" and "Delete" is different from "edit/update" a record.
for performance reason mapped objects are saved in memory as entities when you doing manipulation against NSManagedObjectContext you are not coding against database entirely.
check the link below:
http://cocoawithlove.com/2010/02/differences-between-core-data-and.html
normal work flow:
load appropriate rows from a database
instantiate objects from these rows
make changes to the graph objects
that are now in memory
commit the changes back to the
database
This is my core data for saving.
AppDelegate *app = (AppDelegate*)[[UIApplication sharedApplication] delegate];
Tweet *newTweet = (Tweet *)[NSEntityDescription insertNewObjectForEntityForName:#"Tweet" inManagedObjectContext:app.managedObjectContext];
newTweet.status = status;
newTweet.post_date = theDate;
newTweet.post_id = post_id;
newTweet.sent_error = sent_error;
newTweet.sent_status = sent_status;
newTweet.screen_name = [Settings getActiveScreenName];
// SAVE
NSError* error = nil;
if (![app.managedObjectContext save:&error]) {NSLog(#"did this work?? = %# with userInfo = %#", error, [error userInfo]);}
I have this in my app delegate
- (void)applicationWillTerminate:(UIApplication *)application {
// need to check if TweetViewController is activel.
// User is writing a tweet.
UIViewController * topController = [navigationController visibleViewController];
if([topController isKindOfClass:[TweetViewController class]] ){
[Settings setObject:[(TweetViewController*)topController tweetText].text forKey:#"last_tweet_text"];
}
NSLog(#"good bye");
NSError *error;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
// Handle error.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
}
and this as well in AppDelegate
/**
Returns the persistent store coordinator for the application.
If the coordinator doesn't already exist, it is created and the application's store added to it.
*/
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}
// ~/Library/Application Support/iPhone Simulator/User/
NSURL *storeUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: #"tweetv12.sqlite"]];
NSError *error;
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil error:&error]) {
// Handle error
NSLog(#"cannot save data, change db name.");
}
return persistentStoreCoordinator;
}
I am also able to save delete and update data with this.
I am trying to create an iPhone application where the user can add entries. When he presses a new entry, a box will popup asking him for some information. Then he can either press "Cancel" or "Save" to discard the data or save it to disk.
For saving, I am using the Core Data framework, which works pretty well. However, I cannot get the "Cancel" button to work. When the window pops up, asking for information, I create a new object in the managed object context (MOC). Then when the user presses cancel, I try to use the NSUndoManager belonging to the MOC.
I would also like to do it using nested undo groups, because there might be nested groups.
To test this, I wrote a simple application. The application is just the "Window based application" template with Core Data enabled. For the Core Data model, I create a single entity called "Entity" with integer attribute "x". Then inside the applicationDidFinishLaunching, I add this code:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Override point for customization after app launch
unsigned int x=arc4random()%1000;
[self.managedObjectContext processPendingChanges];
[self.managedObjectContext.undoManager beginUndoGrouping];
NSManagedObject *entity=[NSEntityDescription insertNewObjectForEntityForName:#"Entity"
inManagedObjectContext:self.managedObjectContext];
[entity setValue:[NSNumber numberWithInt:x] forKey:#"x"];
NSLog(#"Insert Value %d",x);
[self.managedObjectContext processPendingChanges];
[self.managedObjectContext.undoManager endUndoGrouping];
[self.managedObjectContext.undoManager undoNestedGroup];
NSFetchRequest *fetchRequest=[[NSFetchRequest alloc] init];
NSEntityDescription *entityEntity=[NSEntityDescription entityForName:#"Entity"
inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entityEntity];
NSArray *result=[self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
for(entity in result) {
NSLog(#"FETCHED ENTITY %d",[[entity valueForKey:#"x"] intValue]);
}
[window makeKeyAndVisible];
}
The idea is simple. Try to insert a new Entity object, undo it, fetch all Entity objects in the MOC and print them out. If everything worked correctly, there should be no objects at the end.
However, I get this output:
[Session started at 2010-02-20 13:41:49 -0800.]
2010-02-20 13:41:51.695 Untitledundotes[7373:20b] Insert Value 136
2010-02-20 13:41:51.715 Untitledundotes[7373:20b] FETCHED ENTITY 136
As you can see, the object is present in the MOC after I try to undo its creation.
Any suggestions as to what I am doing wrong?
Your problem is caused by the fact that, unlike OS X, the iPhone managed object context does not contain an undo manager by default. You need to explicitly add one.
Change the generated code in the app delegate for the managedObjectContext property to look like this:
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext != nil) {
return managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
//add the following 3 lines of code
NSUndoManager *undoManager = [[NSUndoManager alloc] init];
[managedObjectContext setUndoManager:undoManager];
[undoManager release];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
return managedObjectContext;
}
After making that change, the 2nd log message is no longer printed.
Hope that helps...
Dave
I tried Dave approach, but did not work for me.
I finally found the solution in Apple's example CoreDataBooks
The trick is to create a new context that shares the coordinator with you App's context. To discard the changes you dont need to do a thing, just discard the new context object. Since you share the coordinator, saving updates your main context.
Here is my adapted version, where I use a static object for the temp context to create a new ChannelMO object.
//Gets a new ChannelMO that is part of the addingManagedContext
+(ChannelMO*) getNewChannelMO{
// Create a new managed object context for the new channel -- set its persistent store coordinator to the same as that from the fetched results controller's context.
NSManagedObjectContext *addingContext = [[NSManagedObjectContext alloc] init];
addingManagedObjectContext = addingContext;
[addingManagedObjectContext setPersistentStoreCoordinator:[[self getContext] persistentStoreCoordinator]];
ChannelMO* aux = (ChannelMO *)[NSEntityDescription insertNewObjectForEntityForName:#"ChannelMO" inManagedObjectContext:addingManagedObjectContext];
return aux;
}
+(void) saveAddingContext{
NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
[dnc addObserver:self selector:#selector(addControllerContextDidSave:)
name:NSManagedObjectContextDidSaveNotification object:addingManagedObjectContext];
NSError *error;
if (![addingManagedObjectContext save:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
[dnc removeObserver:self name:NSManagedObjectContextDidSaveNotification object:addingManagedObjectContext];
// Release the adding managed object context.
addingManagedObjectContext = nil;
}
I hope it helps
Gonso
It should work. Did you correctly assign the undo manager to your managedObjectContext? If you have rightly done that, it by default has undo registration enabled, and you should be good to go. There is a good article on core data here. There is a good tutorial on core data and NSUndoManager here. Hope that helps.