Search code examples
core-databackground-processwatchos

How to handle watchOS CoreData background save correctly?


My watchOS app uses core data for local storage. Saving the managed context is done in background:

var backgroundContext = persistentContainer.newBackgroundContext()
//…
backgroundContext.perform {
    //…
    let saveError = self.saveManagedContext(managedContext: self.backgroundContext)
    completion(saveError)
}
//…
func saveManagedContext(managedContext: NSManagedObjectContext) -> Error? {
    if !managedContext.hasChanges { return nil }
    do {
        try managedContext.save()
        return nil
    } catch let error as NSError {
        return error
    }
}

Very rarely, my context is not saved. One reason I can think of is the following:

After my data are changed, I initiate a background core data context save operation.
But before the background task starts, the watch extension is put by the user into background, and is then terminated by watchOS. This probably also prevents the core data background save to execute.

My questions are:
- Is this scenario possible?
- If so, what would be the correct handling of a core data background context save?

PS: On the iOS side, I do the same, but here it is possible to request additional background processing time using

var bgTask: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: { 
//…
    application.endBackgroundTask(bgTask)
}

Solution

  • By now, I think I can answer my question:

    If the watch extension is put by the user into background, the extension delegate calls applicationDidEnterBackground(). The docs say:

    The system typically suspends your app shortly after this method returns; therefore, you should not call any asynchronous methods from your applicationDidEnterBackground() implementation. Asynchronous methods may not be able to complete before the app is suspended.

    I think this also applies to background tasks that have been initiated before, so it is actually possible that a core data background save does not complete.

    Thus, the core data save should be done on the main thread. My current solution is the following:

    My background context is no longer set up using persistentContainer.newBackgroundContext(), since such a context is connected directly to the persistentContainer, and when this context is saved, changes are written to the persistent store, which may take relatively long. Instead, I now set up the background context by

    var backgroundContext = NSManagedObjectContext.init(concurrencyType: .privateQueueConcurrencyType)  
    

    and set its parent property as

    backgroundContext.parent = container.viewContext  
    

    where container is the persistent container. Now, when the background context is saved, it is not written to the persistent store, but to its parent, the view content that is handled by the main thread. Since this saving is only done in memory, it is pretty fast.
    Additionally, in applicationDidEnterBackground() of the extension delegate, I save the view context. Since this is done on the main thread, The docs say:

    The applicationDidEnterBackground() method is your last chance to perform any cleanup before the app is terminated.

    In normal circumstances, enough time should be provided by watchOS. If not, other docs say:

    If needed, you can request additional background execution time by calling the ProcessInfo class’s performExpiringActivity(withReason:using:) method.

    This is probably equivalent to setting up a background task in iOS as shown in my question.

    Hope this helps somebody!