views:

73

answers:

3

I have had a customer contact me about a bug in my application which I believe is related to my use of Core Data. This is currently only a theory as I have not be able to re-create the issue myself.

The problem appears to be if the user creates a new entity object and then edits the entity object straight away it does not seem to persist correctly and the next time the user opens the application the object is not there.

If the user creates the entity object and exits the app without editing it the problem does not occur, and if the user edits the object after re-opening the app it also does not occur.

I have only had one report of this happening and the device in question is a iPhone 3G running IOS4.0.2. I can not re-create it on an iPhone 3GS, iPhone 4 or via the simulator (all running 4.0.2)

I am no Core Data expert and am wondering if there is a problem with the way I am using the framework. I would really appreciate it if someone would review the code I have below and let me know if they see any potential problems.

I have included the methods that interact with core data from the relevant classes.

AppDelegate

- (void)applicationDidEnterBackground:(UIApplication *)application {

    NSError *error = nil;
    if (managedObjectContext_ != nil) {
        if ([managedObjectContext_ hasChanges] && ![managedObjectContext_ save:&error]) {
            // log
        } 
    }
}

- (void)applicationWillTerminate:(UIApplication *)application {

    NSError *error = nil;
    if (managedObjectContext_ != nil) {
        if ([managedObjectContext_ hasChanges] && ![managedObjectContext_ save:&error]) {
            // log
        } 
    }
}

- (NSManagedObjectContext *)managedObjectContext {

    if (managedObjectContext_ != nil) {
        return managedObjectContext_;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        managedObjectContext_ = [[NSManagedObjectContext alloc] init];
        [managedObjectContext_ setPersistentStoreCoordinator:coordinator];
    }
    return managedObjectContext_;
}

- (NSManagedObjectModel *)managedObjectModel {

    if (managedObjectModel_ != nil) {
        return managedObjectModel_;
    }
    managedObjectModel_ = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];    
    return managedObjectModel_;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

    if (persistentStoreCoordinator_ != nil) {
        return persistentStoreCoordinator_;
    }

    NSURL *storeURL = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"mydatabase.sqlite"]];

    NSError *error = nil;
    persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@"An error occurred adding the persistentStoreCoordinator. %@, %@", error, [error userInfo]);
        // Display an alert message if an error occurs
    }    

    return persistentStoreCoordinator_;
}

Controller 1 (creates the entity object using an image selected via the Image Picker)

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {

    UIImage* selectedImage = [info objectForKey:UIImagePickerControllerOriginalImage];

    // Create an image object for the new image.
    NSManagedObject *image = [NSEntityDescription insertNewObjectForEntityForName:@"Image" inManagedObjectContext:managedObjectContext];

    // Set the image for the image managed object.
    [image setValue:[selectedImage scaleAndCropImageToScreenSize] forKey:@"image"];

    // Create a thumbnail version of the image.
    UIImage *imageThumbnail = [selectedImage thumbnailImage:50.0F];

    // Create a new note
    Note *note = [NSEntityDescription insertNewObjectForEntityForName:@"Note" inManagedObjectContext:managedObjectContext];
    NSDate *now = [[NSDate alloc] init];
    note.createdDate = now;
    note.lastModifiedDate = now;
    [now release];
    note.thumbnail = imageThumbnail;
    note.image = image;
    }

    [self dismissModalViewControllerAnimated:YES];
    [picker release];
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NotePadViewController *notePadController = [[NotePadViewController alloc] initWithNibName:@"NotePadView" bundle:nil];
    Note *note = [[self fetchedResultsController] objectAtIndexPath:indexPath];
    notePadController.note = note;
    notePadController.title = note.notes;
    [self.navigationController pushViewController:notePadController animated:YES];
    [notePadController release];
}

- (NSFetchedResultsController *)fetchedResultsController {

    if (fetchedResultsController != nil) {
        return fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Note" inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];

    [fetchRequest setFetchBatchSize:20];
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"createdDate" ascending:NO];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    [fetchRequest setSortDescriptors:sortDescriptors];

    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:@"Root"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;

    [aFetchedResultsController release];
    [fetchRequest release];
    [sortDescriptor release];
    [sortDescriptors release];

    NSError *error = nil;
    if (![fetchedResultsController performFetch:&error]) {
        NSLog(@"An error occured retrieving fetched results. %@, %@", error, [error userInfo]);
        // Display an alert message if an error occurs
    }

    return fetchedResultsController;
}    

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    UITableView *tableView = self.tableView;

    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self setControllerTitle];
    [self.tableView endUpdates];
}

Controller 2 - Used to view and edit the entity object (add text)

- (void)saveAction:(id)sender {

    NSDate *now = [[NSDate alloc] init];

    if (self.note != nil && textView.text.length == 0) {
        self.note.notes = nil;
        self.note.lastModifiedDate = now;
    } else if (textView.text.length != 0) {
        // Create a new note if one does not exist
        if (self.note == nil) {
            VisualNotesAppDelegate *appDelegate = (VisualNotesAppDelegate *)[[UIApplication sharedApplication] delegate];
            NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
            Note *newNote = [NSEntityDescription insertNewObjectForEntityForName:@"Note" inManagedObjectContext:managedObjectContext];
            self.note = newNote;
            self.note.createdDate = now;
        }
        self.note.lastModifiedDate = now;
        self.note.notes = textView.text;
        self.title = [note title]; 
    }

    [now release];

    [self.textView resignFirstResponder];
}

Many thanks in advance.

A: 

First, you really should not be ignoring those errors. You could be throwing an error there and never know it.

Nothing really jumps out about the code itself, but you could easily be having an error during the save, perhaps a non-optional parameter not being set, etc.

Handle the exceptions and see from there.

Marcus S. Zarra
Thanks for looking at the code. I do handle there errors with log messages, I just removed them and replaced them with a comment for the purpose of posting the code on here in order to keep it a short as possible.
lucasweb
A: 

If the entity is only created in the saveAction: method then it might not get created in the first place if the user quits the application before manually choosing save.

TechZen
A: 

The problem is that I am only saving the managed object context when the app terminates or is backgrounded.

I only have about 5 seconds for all of the image blobs that were created by the user to be saved before the app exits. If there are a lot of images then they may not all save in time which is why they do not appear when the app is restarted.

This mainly effects the iPhone 3G as it is the slowest of all the models I support.

I have updated my code to save the managed context when ever a change is made. This has resolved this problem but has exposed another issue:

http://stackoverflow.com/questions/3578951/nsfetchedresultscontrollerdelegate-methods-not-being-called-when-camera-image-sav

lucasweb