Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITableView with NSFetchedResultsController Does Not Load the Second Time

Update 3 These are the logs after the first run with an empty data store.

2013-02-07 20:57:06.708 Five Hundred Things[14763:c07] mainMOC = <NSManagedObjectContext: 0x7475a90>
2013-02-07 20:57:06.711 Five Hundred Things[14763:1303] Import started
2013-02-07 20:57:06.712 Five Hundred Things[14763:1303] backgroundMOC = <NSManagedObjectContext: 0x8570070>
2013-02-07 20:57:06.717 Five Hundred Things[14763:c07] FRC fetch performed
2013-02-07 20:57:06.718 Five Hundred Things[14763:c07] numberOfSectionsInTableView returns 1
2013-02-07 20:57:06.720 Five Hundred Things[14763:c07] numberOfSectionsInTableView returns 1
2013-02-07 20:57:06.720 Five Hundred Things[14763:c07] numberOfRowsInSection returns 0
2013-02-07 20:57:06.728 Five Hundred Things[14763:1303] call contextDidSave
2013-02-07 20:57:06.736 Five Hundred Things[14763:1303] call contextDidSave
2013-02-07 20:57:06.736 Five Hundred Things[14763:c07] numberOfSectionsInTableView returns 1
2013-02-07 20:57:06.737 Five Hundred Things[14763:c07] numberOfSectionsInTableView returns 1
2013-02-07 20:57:06.737 Five Hundred Things[14763:c07] numberOfRowsInSection returns 5
2013-02-07 20:57:06.758 Five Hundred Things[14763:1303] call contextDidSave
2013-02-07 20:57:06.759 Five Hundred Things[14763:1303] Refresh complete
2013-02-07 20:57:06.759 Five Hundred Things[14763:c07] numberOfSectionsInTableView returns 1
2013-02-07 20:57:06.760 Five Hundred Things[14763:c07] numberOfSectionsInTableView returns 1
2013-02-07 20:57:06.761 Five Hundred Things[14763:c07] numberOfRowsInSection returns 5

Note that the FRC fetch is performed, the number of rows in the section is 0, but then after the second contextDidSave, it changes to 5 to match the number of categories in the data store.

On the second run with the crash, here are the logs:

2013-02-07 21:01:11.578 Five Hundred Things[14800:c07] mainMOC = <NSManagedObjectContext: 0x8225650>
2013-02-07 21:01:11.581 Five Hundred Things[14800:1303] Import started
2013-02-07 21:01:11.582 Five Hundred Things[14800:1303] backgroundMOC = <NSManagedObjectContext: 0x7439850>
2013-02-07 21:01:11.592 Five Hundred Things[14800:c07] FRC fetch performed
2013-02-07 21:01:11.594 Five Hundred Things[14800:c07] cat = Attraction
2013-02-07 21:01:11.594 Five Hundred Things[14800:c07] cat = Beverage
2013-02-07 21:01:11.595 Five Hundred Things[14800:c07] cat = Entertainment
2013-02-07 21:01:11.595 Five Hundred Things[14800:c07] cat = Hotel
2013-02-07 21:01:11.596 Five Hundred Things[14800:c07] cat = Restaurant
2013-02-07 21:01:11.597 Five Hundred Things[14800:c07] numberOfSectionsInTableView returns 1
2013-02-07 21:01:11.598 Five Hundred Things[14800:c07] numberOfSectionsInTableView returns 1
2013-02-07 21:01:11.599 Five Hundred Things[14800:c07] numberOfRowsInSection returns 0
2013-02-07 21:01:11.602 Five Hundred Things[14800:1303] call contextDidSave
2013-02-07 21:01:11.610 Five Hundred Things[14800:1303] call contextDidSave

The FRC is initialized, and immediately afterward the Categories are logged to show that they are indeed in the FRC. The number of rows in the section, however, is 0 and never gets updated. Instead the app crashes with the stack below.

On the third and subsequent runs, this is what the log looks like:

2013-02-07 21:03:55.560 Five Hundred Things[14815:c07] mainMOC = <NSManagedObjectContext: 0x8128860>
2013-02-07 21:03:55.563 Five Hundred Things[14815:1e03] Import started
2013-02-07 21:03:55.564 Five Hundred Things[14815:1e03] backgroundMOC = <NSManagedObjectContext: 0x822b5d0>
2013-02-07 21:03:55.569 Five Hundred Things[14815:c07] FRC fetch performed
2013-02-07 21:03:55.571 Five Hundred Things[14815:c07] cat = Attraction
2013-02-07 21:03:55.572 Five Hundred Things[14815:c07] cat = Beverage
2013-02-07 21:03:55.572 Five Hundred Things[14815:c07] cat = Entertainment
2013-02-07 21:03:55.573 Five Hundred Things[14815:c07] cat = Hotel
2013-02-07 21:03:55.573 Five Hundred Things[14815:c07] cat = Restaurant
2013-02-07 21:03:55.574 Five Hundred Things[14815:c07] numberOfSectionsInTableView returns 1
2013-02-07 21:03:55.576 Five Hundred Things[14815:c07] numberOfSectionsInTableView returns 1
2013-02-07 21:03:55.576 Five Hundred Things[14815:c07] numberOfRowsInSection returns 5
2013-02-07 21:03:55.581 Five Hundred Things[14815:1e03] call contextDidSave
2013-02-07 21:03:55.592 Five Hundred Things[14815:1e03] call contextDidSave
2013-02-07 21:03:55.593 Five Hundred Things[14815:c07] numberOfSectionsInTableView returns 1
2013-02-07 21:03:55.594 Five Hundred Things[14815:c07] numberOfSectionsInTableView returns 1
2013-02-07 21:03:55.595 Five Hundred Things[14815:c07] numberOfRowsInSection returns 5
2013-02-07 21:03:55.606 Five Hundred Things[14815:1e03] call contextDidSave
2013-02-07 21:03:55.606 Five Hundred Things[14815:1e03] Refresh complete

This is how the behavior should look on the second run; the data is already in the store, the number of rows in the section returns 5, and the categories appear in the table view immediately.


Update 2 Here's a stack trace of the main thread, which is where the crash occurs. Since it's occurring on the main thread, I'm thinking it has something to do with the UITableView. I'm not using NSDictionary or NSMutableDictionary in the UITableView though. My thought now is that numberOfRowsInSection returning 0 on the second run is causing the issue but I'm not sure how to resolve it. It returns the correct number (5 with the data I'm using) on the third run, and seems to populate the data store correctly on the first run, so I'm confused as to why on the second run it returns 0 and doesn't update.

frame #0: 0x013ede52 libobjc.A.dylib`objc_exception_throw
frame #1: 0x020330de CoreFoundation`-[__NSDictionaryM setObject:forKey:] + 158
frame #2: 0x01211d7a CoreData`-[NSFetchedResultsController(PrivateMethods) _preprocessUpdatedObjects:insertsInfo:deletesInfo:updatesInfo:sectionsWithDeletes:newSectionNames:treatAsRefreshes:] + 1994
frame #3: 0x01212ed7 CoreData`-[NSFetchedResultsController(PrivateMethods) _managedObjectContextDidChange:] + 2455
frame #4: 0x00b9e4f9 Foundation`__57-[NSNotificationCenter addObserver:selector:name:object:]_block_invoke_0 + 40
frame #5: 0x0200a0c5 CoreFoundation`___CFXNotificationPost_block_invoke_0 + 85
frame #6: 0x01f64efa CoreFoundation`_CFXNotificationPost + 2122
frame #7: 0x00ad2bb2 Foundation`-[NSNotificationCenter postNotificationName:object:userInfo:] + 98
frame #8: 0x01125163 CoreData`-[NSManagedObjectContext(_NSInternalNotificationHandling) _postObjectsDidChangeNotificationWithUserInfo:] + 83
frame #9: 0x011bed2f CoreData`-[NSManagedObjectContext(_NSInternalChangeProcessing) _createAndPostChangeNotification:withDeletions:withUpdates:withRefreshes:] + 367
frame #10: 0x01121128 CoreData`-[NSManagedObjectContext(_NSInternalChangeProcessing) _postRefreshedObjectsNotificationAndClearList] + 136
frame #11: 0x0111f8c0 CoreData`-[NSManagedObjectContext(_NSInternalChangeProcessing) _processRecentChanges:] + 80
frame #12: 0x0111f869 CoreData`-[NSManagedObjectContext processPendingChanges] + 41
frame #13: 0x010f3e38 CoreData`_performRunLoopAction + 280
frame #14: 0x01f78afe CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 30
frame #15: 0x01f78a3d CoreFoundation`__CFRunLoopDoObservers + 381
frame #16: 0x01f567c2 CoreFoundation`__CFRunLoopRun + 1106
frame #17: 0x01f55f44 CoreFoundation`CFRunLoopRunSpecific + 276
frame #18: 0x01f55e1b CoreFoundation`CFRunLoopRunInMode + 123
frame #19: 0x01f0a7e3 GraphicsServices`GSEventRunModal + 88
frame #20: 0x01f0a668 GraphicsServices`GSEventRun + 104
frame #21: 0x00021ffc UIKit`UIApplicationMain + 1211
frame #22: 0x000022dd Five Hundred Things`main(argc=1, argv=0xbffff31c) + 141 at main.m:16
frame #23: 0x00002205 Five Hundred Things`start + 53

Update: I've managed to get an actual crash instead of just no response.

* Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '* setObjectForKey: object cannot be nil (key: _ContentChange_OldIndexPathKey)'

This SO question is the closest I could find to the error, but it discusses the value being nil instead of the key. It looks like it occurs when the Categories are being saved to the Core Data store, but all of the categories have values.

The entity Category contains category_id - Integer 16 category_name - String

It has a to-many relationship with the entity Thing, but this particular part of the code is not doing anything to that relationship; it is only setting the category_id and category_name. Later in the import (after the MOC save in question) is when the relationship is set.

Code in question from the import operation:

//import categories

    NSString *categoryPath = [[NSBundle mainBundle] pathForResource:@"category" ofType:@"json"];

    NSData *categoryData = [NSData dataWithContentsOfFile:categoryPath];

    NSDictionary *categoryResults = [NSJSONSerialization
                                     JSONObjectWithData:categoryData
                                     options:NSJSONReadingMutableLeaves
                                     error:&error];

    NSEntityDescription *categoryEntity = [NSEntityDescription entityForName:@"Category"
                                              inManagedObjectContext:context];

    NSMutableArray *categories = [[NSMutableArray alloc] init];

    NSString *categoryPredicateString = [NSString stringWithFormat: @"category_id == $CATEGORY_ID"];

    NSPredicate *categoryPredicate = [NSPredicate predicateWithFormat:categoryPredicateString];


    for (NSDictionary *categoryKey in categoryResults){

        NSFetchRequest *categoryFetchRequest = [[NSFetchRequest alloc] init];

        [categoryFetchRequest setEntity:categoryEntity];

        NSNumber *categoryID = [NSNumber numberWithInt:[[categoryKey objectForKey:@"category_id"] integerValue]];

        [categories addObject:categoryID];

        NSDictionary *categoryVariables = [NSDictionary dictionaryWithObject:categoryID forKey:@"CATEGORY_ID"];

        NSPredicate *catSubPredicate = [categoryPredicate predicateWithSubstitutionVariables:categoryVariables];

        [categoryFetchRequest setPredicate:catSubPredicate];

        NSArray *categoryArray = [[NSArray alloc] init];
        categoryArray = [context executeFetchRequest:categoryFetchRequest error:&error];

        Category *categoryObject = [categoryArray lastObject];
        NSNumber *categoryNum = [categoryObject valueForKey:@"category_id"];
        NSInteger categoryInt = [categoryNum integerValue];

        if (categoryInt != [[categoryKey objectForKey:@"category_id"] integerValue]){
            categoryObject = [NSEntityDescription
                              insertNewObjectForEntityForName:@"Category"
                              inManagedObjectContext:context];
            categoryObject.category_id = [NSNumber numberWithInt:[[categoryKey objectForKey:@"category_id"] intValue]];
        }
        if (categoryObject.category_name != [categoryKey objectForKey:@"category"]){
            categoryObject.category_name = [categoryKey objectForKey:@"category"];
        }

    }

    //Remove unneeded Categories from Core Data Store

    NSFetchRequest *removeUnusedCategories = [[NSFetchRequest alloc] init];
    [removeUnusedCategories setEntity:categoryEntity];
    NSArray *fetchedCategories = [context executeFetchRequest:removeUnusedCategories error:&error];

    for (Category *fetchedCategory in fetchedCategories){
        if (![categories containsObject:fetchedCategory.category_id]){
            [context deleteObject:fetchedCategory];
            NSLog(@"Object deleted");
        }
    }

    if (![context save:&error]) {
        NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
    }

The [context save] occurs on the background MOC and is synced to the main MOC (in the app delegate) through the notification center. It listens for NSManagedObjectContextDidSaveNotification and runs mergeChangesFromContextDidSaveNotification: on the main MOC.

The first run and the third run work perfectly. It always occurs on the second run.


I'm using Core Data on an iOS project and so far it's working well except for one problem.

The app populates the Core Data store from JSON files and the initial UITableViewController loads up with animation as it should. However, the second time the app launches, the initial UITableView is blank. I've checked in multiple places and the data is in the Core Data store when the second launch begins, but none of the UITableView or NSFetchedResultsController methods are called.

On the first launch, the number of rows in section returns 0 but after the Core Data store is loaded returns 5 as it should. On the second launch, the number of rows in the section (only one section) returns 0 and doesn't update. On the third and all subsequent launch, the number of rows in the section returns 5 as it should.

Neither the UITableView's cellForRowAtIndexPath nor the NSFetchedResultsController's didChangeObject methods are called on the second launch of the app. The UITableViewController is the UITableViewDelegate, UITableViewDataSource, and NSFetchedResultsControllerDelegate.

As suggested in the Core Data guidelines, the app delegate and table view controller share a managed object context while the data loading is being done on a background MOC in another thread. These are being synced when the context's save method is called through the mergeChangesFromContextDidSaveNotification:.

To reproduce, I delete the app from the simulator, run once and the database populates and the app displays correctly. I stop the app and run again and nothing displays. I stop the app and run a third time and it displays correctly.

All of this seems to work correctly except for the second time launching the app. The first and third times work properly. What am I missing?

As for my code, I'm not sure what to put here. Let's start with the UITableViewController's implementation.

@implementation FTWTMasterViewController

@synthesize managedObjectContext;
@synthesize categoryController = _categoryController;
@synthesize catLocViewController;

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (NSFetchedResultsController *)categoryController {
    if (_categoryController != nil) {
        return _categoryController;
    }

    NSLog(@"tableview MOC = %@", self.managedObjectContext);

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

    NSSortDescriptor *sort = [[NSSortDescriptor alloc]
                              initWithKey:@"category_name" ascending:YES];
    [fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];

    [fetchRequest setFetchBatchSize:20];

    NSFetchedResultsController *theFetchedResultsController =
    [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                        managedObjectContext:self.managedObjectContext
                                          sectionNameKeyPath:nil
                                                   cacheName:@"CategoryTable"];
    _categoryController = theFetchedResultsController;
    _categoryController.delegate = self;

    return _categoryController;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.tableView.dataSource = self;
    self.tableView.delegate = self;

    NSError *error;
    if (![[self categoryController] performFetch:&error]) {
        // Update to handle the error appropriately.
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        exit(-1);  // Fail
    }

    NSLog(@"Fetch called");

    self.title = @"Categories";
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    NSLog(@"number of sections = %lu", (unsigned long)[[self.categoryController sections] count]);
    return [[self.categoryController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    id  sectionInfo =
        [[_categoryController sections] objectAtIndex:section];

    NSLog(@"numberOfObjects = %lu", (unsigned long)[sectionInfo numberOfObjects]);
    return [sectionInfo numberOfObjects];
}

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    Category *category = [_categoryController objectAtIndexPath:indexPath];
    cell.textLabel.text = category.category_name;
    NSLog(@"config cell %@", category.category_name);
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"tableView setup");
    static NSString *CellIdentifier = @"categoryCell";

    UITableViewCell *cell =
    [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    // Set up the cell...
    [self configureCell:cell atIndexPath:indexPath];

    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    Category *aCategory = [self.categoryController objectAtIndexPath:indexPath];

    if (self.catLocViewController == nil){
        FTWTCatLocationViewController *aCatLocController = [[FTWTCatLocationViewController alloc] init];
        self.catLocViewController = aCatLocController;
    }

    self.catLocViewController.selectedCat = aCategory;
    aCategory = nil;

    self.catLocViewController.managedObjectContext = self.managedObjectContext;

    [self.navigationController pushViewController:self.catLocViewController animated:YES];

    self.catLocViewController = nil;
}

#pragma mark - Fetched results controller delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller is about to start sending change notifications, so prepare the table view for updates.
    [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {

    NSLog(@"didChangeObject");

    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 insertRowsAtIndexPaths:[NSArray
                                               arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}


- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )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 {
    // The fetch controller has sent all current change notifications, so tell the table view to process all updates.
    [self.tableView endUpdates];
}

@end
like image 466
scottoliver Avatar asked Feb 03 '13 20:02

scottoliver


Video Answer


1 Answers

Contrary to what I assumed in above comment, the same effect can happen if a sectionNameKeyPath is specified to create table view sections (I could reproduce this with a test program).

When the app is started the first time, the following happens:

  1. A persistent store file "appname.sqlite" is created.
  2. A fetched results controller for the table view is created with the cacheName parameter set, so that a section cache file is created. At this point, all sections are empty.
  3. A background MOC is created that reads some JSON data from a resource file and add objects to the context.
  4. The background MOC is saved.

(Btw. the cache file is

   Library/Caches/<bundle-id>/.CoreDataCaches/SectionInfoCaches/<tablename>/sectionInfo

in the application bundle.)

When the app is started the second time, the fetched results controller checks whether the section info cache is still valid or has to be recreated. According to the documentation it compares the modification times of the persistent store file and the section cache file.

Now the interesting part: If (in the first run) the creation of the store file (step 1) and saving the updated context (step 4) happen in the same second, then the modification date of the store file is not changed in step 4!!

Therefore the section cache file is still seen as valid and not recreated. Since all sections were empty (in step 2), the FRC uses this cached information and displays only empty sections.

The background MOC is started again and saves the context. Now the store file has a new modification date, therefore the sections and rows are displayed correctly in the third run of the app.

To confirm my "theory", I did a manual "touch" of the store file between first and second run to enforce a changed modification date. All sections and rows were then displayed correctly.

(I tested this only in the iPhone Simulator. I don't know if the HFS+ file system generally has a 1 second resolution of the modification date, or if SQLite does something special here. I will try to investigate that later.)

Conclusion: If the creation of the store file and saving modified data happen in the same second, a section info cache file might not be regenerated if necessary.

like image 191
Martin R Avatar answered Nov 04 '22 09:11

Martin R