0

Here's my current situation:

  1. When I open up my app, UITableView on my first screen is blank

  2. I fetch my data from a remote server and parse its resultant JSON object, and save it to Core Data storage, and issue a fetched request to the storage and show the result to the aforementioned UITableView

  3. However, even after I post a notification after the parse-save-query and reload my table, the table isn't updated until I tap or scroll over the screen.

I tried [_myTableView reloadData]; and [_myTableView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO]; from notification, but the result doesn't change at all and I still have to tap or scroll over the screen in order to see the reloaded data on the table.

I use Core Data and NSFetchedResultsController, and I confirmed my query was successfully fetched. Just a reload table issue. And also note that I call the notification from AppDelegate.m but my actual table view is on RootViewController.m, another class, for the reasons stated above (remote server data fetching).

Also, when I wrote out the following code:



-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [_myTableView reloadData];
    [_myTableView endUpdates];
}

I still have to see the result by tapping the screen. However, when I added the following code:


-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    NSLog(@"updated begin");
    [_myTableView beginUpdates];
}

I cannot see the reloaded data even after I tap the screen at all.

Also, using printf debug, I confirmed that even if the two methods above are called many, many times (probably it's called in the same number of times of the actual fetched managed objects, right?), the numberOfRowsInSection: and cellForRowAtIndexPath: aren't called at all after I fetched the newly added datasets.

Note that cellForRowsAtIndexPath: is called and the data is displayed if I wait for 10 or 20 seconds or so, which means it's displayed even if I don't tap or scroll over the screen. Weird.

So what's happening here? And how can I fix this strange issue?

I use iOS 7 and Xcode 5.

update

I found out that the reason I wasn't able to see the reloaded data when I write controllerWillChangeContext: is I got the following error: CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null). It's still quite weird that I don't get this error when I implement controllerDidChangeContext: only, but if I write both, I got the error and the table doesn't show the data even after I tap the screen.

So maybe I cannot take the route of displaying blank result first, and updating the table with the new data, and instead I should just make users wait to show the correct data... ?

Blaszard
  • 30,954
  • 51
  • 153
  • 233
  • How are you loading the data? If you are doing so from a background thread then you need to register for moc notifications and when you get a notification that a moc has saved you need to merge the data from the main threads moc. Let me know if you want me to post some code showing how to do this. – Duncan Groenewald Feb 23 '14 at 07:59
  • BTW you need to implement the full set of fetchedResultsController delegate methods, looks like you may be skipping some of them. There are three: willChangeContent, didChangeObject and didChangeContent. https://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intf/NSFetchedResultsControllerDelegate – Duncan Groenewald Feb 23 '14 at 08:04
  • I know there are four delegate methods there but `NSFetchedResultsControllerDelegate` doesn't have any "required" methods. Or if I implement one method it's necessary to also implement the other three? – Blaszard Feb 23 '14 at 08:10
  • http://www.raywenderlich.com/999/core-data-tutorial-for-ios-how-to-use-nsfetchedresultscontroller – Daniel Feb 23 '14 at 08:54
  • Do the three I mentioned to ensure your tableView updates correctly, it does not seem you use sections so those are probably not necessary. Just copy the code directly, don't make any of your own changes. – Duncan Groenewald Feb 23 '14 at 09:31
  • I added all four methods but it still doesn't update my table automatically. As to the number of sections, I currently use one, but would increase once this table issue is fixed. – Blaszard Feb 23 '14 at 10:39
  • That error usually indicates you are doing something wrong in your fetchedResultsController delegate or in your tableView delegate methods. What it means is that you have somehow added an item without correctly adding the item to the tableView. Post your delegate methods and we might be able to help. – Duncan Groenewald Feb 25 '14 at 00:25

2 Answers2

1

Calling reloadData in controllerDidChangeContent: is a wrong approach. When you implement a NSFetchedResultsControllerDelegate the delegate is responsible for all table view updates.


The import from your remote server should be done in a background queue. Before merging your background context with your main context via mergeChangesFromContextDidSaveNotification, you should

  1. Disable the NSFetchedResultsControllerDelegate.
  2. Merge your changes with the main context.
  3. Reload your table.
  4. Re-Enable the NSFetchedResultsControllerDelegate.

Sample:

fetchedResultsController.delegate = nil;
[mainContext mergeChangesFromContextDidSaveNotification:notification];
[tableView reloadData];
fetchedResultsController.delegate = yourController;

Hint: When using the NSFetchedResultsControllerDelegate you have to implement at least:

  • controllerWillChangeContent:
  • controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
  • controllerDidChangeContent:

Read the docs for a sample implementation: NSFetchedResultsControllerDelegate

Florian Mielke
  • 3,310
  • 27
  • 31
  • My data parsing is done in a background queue. I didn't use `mergeChangesFromContextDidSaveNotification` and instead fetched all managed objects, deleted them, and then imported the parsed data to the storage. Now going to use `mergeChangesFromContextDidSaveNotification`, but where should I write it? Is it in `AppDelegate.m` or `RootViewController.m` (class that has my table view)? – Blaszard Feb 23 '14 at 11:04
  • By the way if I deleted `reloadData` in `controllerDidChangeContext:`, the result is even worse, that not only does it display the result to the user until he or she taps the screen, but it also makes it that the tapping reveals only the current cell, not the whole table view. – Blaszard Feb 23 '14 at 11:11
  • Also, I use the same managed object contexts between `AppDelegate.m` and `RootViewController.m`. So why do I have to merge the two? Does one change at one managed object context affect the other automatically, right? – Blaszard Feb 23 '14 at 11:16
  • @florianmielke not sure why you think it necessary to disable the fetchedResultsController - I have never done that and had no problems when merging updates from background threads. – Duncan Groenewald Feb 25 '14 at 00:27
  • You are right, you can use the `fetchedResultsController` to update your table view when merging updates. But in that case you should let the `fetchedResultsController` do the table view updates and not call `[tableView reload]` simultaneously. – Florian Mielke Feb 25 '14 at 16:08
1

Here is some sample code

Loading Data in the Background

First implement the 3 main fetchedResultsControllerDelegate methods correctly to ensure your UITableView gets updates correctly.

  • controllerWillChangeContent:
  • controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
  • controllerDidChangeContent:

Don't call [tableView reload] from within these delegate methods use only the Apple sample code to correctly add/remove/update rows in the tableView.

  1. Create background thread Create managedObjectContext on backgroundthread (bgContext)
  2. Register observer for save notifications on the bgContext (call method storesDidSave)
  3. Run background load/delete job save in batches
  4. Process save notifications
  5. On completion of background job remove observer of bgContext save notifications

You can put this code anywhere, as long as you have access to the MAIN managedObjectContext - probably in the object that is running the background load jobs. MAKE SURE YOUR ARE USING THE CORRECT THREAD TO PROCESS. ManagedObjectContext's are not thread safe!

// NB - this may be called from a background thread so make sure we always run on the main thread !!
// This is when transaction logs are loaded
- (void)storesDidSave:(NSNotification*)notification {

    // Ignore any notifications from the main thread because we only need to merge data
    // loaded from other threads.
    if ([NSThread isMainThread]) {
        FLOG(@" main thread saved context ignore");
        return;
    }

    // Must pass these to the  main thread to process
    [[NSOperationQueue mainQueue] addOperationWithBlock:^ {
        FLOG(@"storesDidSave ");

        // Now merge into the MAIN managedObjectContext and save it.
        // If you don't save then objects in the main context don't seem to update correctly,        
        // especially deletes seem to don't show
        if (self.managedObjectContext) {
            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
                    NSError *error;
            FLOG(@" saving moc");
            if (![self.managedObjectContext save:&error]) {
                FLOG(@" error saving context, %@, %@", error, error.userInfo);
            }
        }
    }];
}

// Sample background dispatcher
- (void)loadDataInBackground {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
        [self loadData];
    });

}
// Background load job calls save after batches of 100 records
- (void)loadData {
    FLOG(@"loadData called");
    _loadJobCount++;
    [self postJobStartedNotification];

    FLOG(@" waiting 5 seconds...");
    sleep(5);
    [self showBackgroundTaskActive];

    NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];

    // Register for saves in order to merge any data from background threads
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(storesDidSave:) name: NSManagedObjectContextDidSaveNotification object:bgContext];


    while (self.persistentStoreCoordinator == nil) {
        FLOG(@" persistentStoreCoordinator = nil, waiting 5 seconds to try again...");
        sleep(5);
    }

    bgContext.persistentStoreCoordinator = [self persistentStoreCoordinator];

    FLOG(@" starting load...");

    for (int i = 1; i<=100; i++) {
        // Add a few companies
        [self insertNewCompany:bgContext count:10];
        [bgContext processPendingChanges];

        // Save the context.
        NSError *error = nil;
        if (![bgContext save:&error]) {
            FLOG(@"  Unresolved error %@, %@", error, [error userInfo]);
        }
        FLOG(@"   waiting 2 seconds...");
        sleep(0.01);
    }

    // Register for saves in order to merge any data from background threads
    [[NSNotificationCenter defaultCenter] removeObserver:self name: NSManagedObjectContextDidSaveNotification object:bgContext];

    FLOG(@" loading ended...");
    [self showBackgroundTaskInactive];

    sleep(2);
    _loadJobCount--;
    [self postJobDoneNotification];
}
Duncan Groenewald
  • 8,496
  • 6
  • 41
  • 76
  • Thanks. You're right, that I didn't execute `reloadData` in main thread. As to the error in my "update" section, I found out that it's related to [this answer by Joseph](http://stackoverflow.com/questions/4858228/nsfetchedresultscontroller-ignores-fetchlimit), since I set `fetchLimit` to my fetch request. Thanks a lot for your great answer. Now I find my table reloaded correctly. – Blaszard Feb 25 '14 at 05:53