Mixing private context with UI - Core data

Apple says:


Comparing Main Queue and Private Queue Contexts

There are two types of managed object contexts: main queue and private queue. The type of context is defined as part of its initialization.

A main queue context (as defined by a

NSMainQueueConcurrencyType
) is specifically for use with your application interface and can only be used on the main queue of your app.

A private queue context (as defined by a

NSPrivateQueueConcurrencyType
) creates its own queue upon initialization and can be used only on that queue. Because the queue is private and internal to the NSManagedObjectContext instance, it can only be accessed through the
performBlock:
and the
performBlockAndWait:
methods.

My question is :


Can we use a fetch results controller with private context as a collection view data source ?


It enables us to unblock UI when we are trying to write big amounts of data into my core data data base. The sentence above, as far as i understand, does not contradict the above question.


Does someone see any issue doing so (use a fetch results controller with private context as a collection view data source) ?

Replies

If the issue is that the UI is blocked during transactions, why not just perform the writes onto a child private queue context as you've mentioned - then merge it against the main context. The fetch results controller should against the source of truth which is the main context.

nocsi - Thank you for taking a look ! 🙂


The issue is indeed blocked UI during transactions. The setup is exactly as was mentioned here:

perform the writes onto a child private queue context as you've mentioned - then merge it against the main context.


However, when i am saving big amounts of data into core data UI freeze. As far as i understand NSFetchedResultsControllerDelegate will calculate all the required changes in indexPaths on the main thread. And since our UI is tight to the linked FetchResultsController which is with mainQueueConcurrencyType, my UI freeze. The duration of time in which the UI freeze is getting bigger if the amount of objects we are saving into core data is getting bigger.


So i have used privateQueueConcurrencyType instead. And now everything seem to work. No freeze. I do need to dispatch all of my UI transactions into the main thread. But that seem like a good compromise considering previous freeze.


As far as i understand core data is not optimized for storing really big amounts of data. Using the setup mentioned above and measure the amount of time taken to add a constant amount of 2000 random objects over and over again into the data base, i get a growing delay:


delta time 0.3195030689239502 (empty data base)

delta time 1.8090097904205322

delta time 3.1822659969329834

delta time 4.478667736053467

delta time 5.796695947647095

delta time 7.051335096359253

delta time 8.188868999481201


Looks more or less a linear growing delay with respect to the data base size.


So my question is if we can use a privateQueueConcurrencyType for the collection view data source ? Since when i am using this setup i can at least avoid all UI freezing issues.

So one thing to keep in mind, NSManagedObjectContexts are fairly cheap to create. However, you will have the overhead of having to keep track of your child contexts - and especially implement guards to disallow cross context access of NSManagedObjects. So right off the bat, you should not be using any privateQueueConcurrencyType/Background Context for any user-facing interface. Mostly, operations performed on a background context aren't finalized until they're merged against the main persistent store. Basically you'll run into anomalous behavior if not crashes operating on objects that haven't persisted.


My recommendation is to keep using that background context for large transactions, but use either the main viewContext or a child context w/ mainQueueConcurrencyType as a read-only fetch view into the persistent store. Then with the background context, you can perform create/update transactions without blocking the UI.


Here's some snippets on how I've set up my background contexts. Side-note: you kinda have to use privateQueueConcurrencyType anyways if you want to support CloudKit+CoreData as I've discovered through testing.


Creating background context, do your usual context.perform & save

    func newChildCreateContext(type: NSManagedObjectContextConcurrencyType = .privateQueueConcurrencyType, mergesChangesFromParent: Bool = true) -> NSManagedObjectContext {
        let context = NSManagedObjectContext(concurrencyType: type)
        context.parent = self
        context.name = "privateCreateContext"
        context.transactionAuthor = appTransactionAuthorName
        context.automaticallyMergesChangesFromParent = mergesChangesFromParent
        return context
    }


Observe the NSManagedObjectContextDidSave notification from NotificationCenter. Merge the changed objects and the main context should resolve everything.

     updateChangeObserver = NotificationObserver(name: .NSManagedObjectContextDidSave, object: updateContext)
     updateChangeObserver.onReceived = mergeUpdateObjects

     private func mergeUpdateObjects(notification: Notification) {
        DispatchQueue.main.async { [unowned self] in
            self.contextDidSave(self.updateContext, note: notification)
        }
    }

    func contextDidSave(_ context: NSManagedObjectContext, note: Notification) {
        guard context === createContext || context === updateContext else { return }
        context.perform {
            context.mergeChanges(fromContextDidSave: note)
        }
    }


Signal to your FetchedResultsController refresh its fetchedObjects after the merge via some other NotificationCenter observer or something.

Refresh Fetched Objects on your main context controller via a NotificationCenter call or something after the merge. Changes should be reflected in the UI non-blocking.

func refreshFetchedObjects() {
     fetchedResult.fetchedObjects?.forEach({ $0.objectWillChange.send() })
     fetchedResult.managedObjectContext.refreshAllObjects()
}


Also another note, if you're adding a constant amount of objects - are you doing it through the new NSBatchInsertRequest operation? If you aren't already, I've found the new operation to be incredibly performant. You just have to make sure you merge the changes against the main context.


    public func executeAndMergeChanges(using batchInsertRequest: NSBatchInsertRequest) throws {
        batchInsertRequest.resultType = .objectIDs
        let result = try execute(batchInsertRequest) as? NSBatchInsertResult
        let changes: [AnyHashable: Any] = [NSInsertedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
        NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
    }