1

I integrated coredata framework in iOS10 (Swift3) app that pulls data from server and display. When the app start for the first time, core data don't have any records. After exchanging some information with server, it starts syncing in background thread. I'm able to see that data is being downloaded from server through web service, parsing and storing in core data. But if I quit and start the app, it's showing all the records.

In my view controller, I'm using "NSFetchedResultsController" to display the records in "TableView". I'm creating fetched results controller as shown below:

fileprivate lazy var inspirationsResults: NSFetchedResultsController<Inspiration> = {
    // Create Fetch Request
    let fetchRequest: NSFetchRequest<Inspiration> = Inspiration.fetchRequest()

    // Configure Fetch Request
    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "timeStamp", ascending: false)]

    // Create Fetched Results Controller
    let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataManager.shared.getContext(), sectionNameKeyPath: nil, cacheName: nil)

    // Configure Fetched Results Controller
    fetchedResultsController.delegate = self

    return fetchedResultsController
}()

In the viewDidLoad method, I wrote below code to fetch:

do {
   try self.inspirationsResults.performFetch()
} catch {
   let fetchError = error as NSError
   print("\(fetchError), \(fetchError.userInfo)")
}

I also added delegate methods "controllerWillChangeContent, controllerDidChangeContent & didChangeObject" to handle the update/modifications.

I'm using persistentContainer to save the object:

 func addInspirations(_ inspirations:[[String: AnyObject]]) {
    persistentContainer.performBackgroundTask({ (bgContext) in
        bgContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

        for tInspiration in inspirations {
            let inspiration = Inspiration(context: bgContext)
            inspiration.inspirationID = tInspiration[kInspirationId] as! Int32
            inspiration.inspirationName = tInspiration[kInspirationName] as? String
        }

        if bgContext.hasChanges {
            do {
                try bgContext.save()
            } catch {
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    })
}

Am I missing anything?

Satyam
  • 15,493
  • 31
  • 131
  • 244
  • Did your background and main thread contexts connected with `mergeChangesFromContextDidSaveNotification:`? Or it's parent-child connection? – bteapot Dec 14 '16 at 12:04
  • @bteapot, I updated my code with "save" details. In that I've setting the mergePolicy of background task to NSMergeByPropertyObjectTrumpMergePolicy – Satyam Dec 14 '16 at 12:10
  • `mergePolicy` is not playing any role in cause of your problem. I'll write an answer. – bteapot Dec 14 '16 at 12:15
  • Yes, you are right. Merge policy is just to resolve the conflicts while saving. – Satyam Dec 14 '16 at 12:18
  • So @bteapot's question remains: how, if at all, are the two contexts (`bgContext` and `CoreDataManager.shared.getContext()`) related? Show the initialisation for each. – pbasdf Dec 14 '16 at 12:54
  • @pbasdf, bgContext is not initialised manually, its part of the closure parameter for "performBackgroundTask". getContext() is nothing but persistentContainer.viewContext – Satyam Dec 14 '16 at 13:00
  • Ah, sorry, I'm still getting used to `NSPersistentContainer`. – pbasdf Dec 14 '16 at 13:39

1 Answers1

0

Two or more NSManagedObjectContexts that draws data from the same persistent store did not automatically notice changes in that store. They needs to be linked with each other to receive deletions, insertions and updates that was performed in one of them. Such a link can be established with one of this two ways:

  1. Merging changes.
  2. Using parent/child pattern.

Merging changes usually done like that:

// initializing your contexts
self.mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.mainContext.persistentStoreCoordinator = self.coordinator;

self.backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.backgroundContext.persistentStoreCoordinator = self.coordinator;

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mainContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.mainContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.backgroundContext];

And somewhere later:

- (void)mainContextDidSave:(NSNotification *)notification
{
    [self.backgroundContext performBlock:^{
        [self.backgroundContext mergeChangesFromContextDidSaveNotification:notification];
    }];
}

- (void)backgroundContextDidSave:(NSNotification *)notification
{
    [self.mainContext performBlock:^{
        [self.mainContext mergeChangesFromContextDidSaveNotification:notification];
    }];
}

That ensures that your context will receive changes right after that changes persisted.

One of the possible parent/child setup variations would be:

// when initializing your contexts
self.mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.mainContext.persistentStoreCoordinator = self.coordinator;

self.backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.backgroundContext.parentContext = self.mainContext;

// when saving your background changes
[self.backgroundContext performBlock:^{
    __block NSError *error;

    if ([self.backgroundContext save:&error) {
        [self.mainContext performBlock:^{

            if (![self.mainContext save:&error]) {
                NSLog(@"Error saving main context");
            }
        }];
    } else {
        NSLog(@"Error saving background context");
    }
}];

That will push changes from background context to main context and save them in persistent store.

Also, NSFetchedResultsController have its own peculiarities like this or this.

And finally, if you have to import relatively large dataset with about few dozens of thousands objects, disconnect your NSFetchedResultsControllers while saving and importing data. That will save a lot of main thread processing time for you. Here's the scheme:

  1. When processing your data, post your own notification right before saving your background context:

    [[NSNotificationCenter defaultCenter] postNotificationName:DBWillUpdateDataNotification object:self];
    
  2. Save your changes and merge them into main context, or push them into parent context and save it.

  3. Post another notification:

    [[NSNotificationCenter defaultCenter] postNotificationName:DBDidUpdateDataNotification object:self];
    
  4. Watch for this two custom notifications in your view controllers. When receiving first one, set the delegate to nil on your NSFetchedResultsControllers. This will stop them from waching for changes in context and reporting them. When receiving second - connect your delegate back, call -performFetch: on your FRCs and reload your interface, i.e. call -reloadData on table views, repopulate custom labels, etc.

Community
  • 1
  • 1
bteapot
  • 1,897
  • 16
  • 24
  • If I'm creating the context manually, this will work. But I'm using "persistentContainer.performBackgroundTask({ (bgContext) in" that will provide the context for us. I tried setting mainContext for "bgContext" available in closure, but its crashing as co-ordinator is already set for that. – Satyam Dec 15 '16 at 02:39
  • Creating your own context is not a problem, it's just couple of lines. – bteapot Dec 15 '16 at 08:16
  • I'm following "Merging changes". I've multiple backgroundContexts running in parallel to update the data. But I'm using main managed object context to update UI. In my case, notification is being called as well, but data is not updating in fetched results controller. – Satyam Dec 15 '16 at 10:37