Sync an interactive widget's Core Data store with the main app (and iCloud)

Hi everyone!

I have an app on the App Store that uses Core Data as its data store. (It's called Count on Me: Tally Counter. Feel free to check it out.) One of the app's core feature is an interactive widget with a simple button. When the button is tapped, it's supposed to update the entity in the store.

My requirement is that the changes are then reflected with minimal latency in the main app and – ideally – also on other devices of the same iCloud user. And vice-versa: When an entity is updated in the app (or on another device where the same iCloud user is logged in), the widget that shows this entity should also refresh to reflect the changes.

I have read multiple articles, downloaded sample projects, searched Stackoverflow and the Apple developer forums, and tried to squeeze a solution out of AI, but couldn't figure out how to make this work reliably.

So I tried to reduce the core problem to a minimal example project. It has two issues that I cannot resolve:

  1. When I update an entity in the app, the widget is immediately updated as intended (due to a call to WidgetCenter's reloadAllTimelines method). However, when I update the same entity from the interactive widget using the same app intent, the changes are not reflected in the main app.

  2. For the widget and the app to use the same local data store, I need to enable App Groups in both targets and set a custom location for the store within the shared app group. So I specify a custom URL for the NSPersistentStoreDescription when setting up the Core Data stack. The moment I do this, iCloud sync breaks.

Issue no. 1 is far more important to me as I haven't officially enabled iCloud sync yet in my real app that's already on the App Store. But it would be wonderful to resolve issue no. 2 as well. Surely, there must be a way to synchronize changes to the source of truth triggered by interactive widget with other devices of the same iCloud user. Otherwise, the feature to talk to the main app and the feature to synchronize with iCloud would be mutually exclusive.

Some other developers I talked to have suggested that the widget should only communicate proposed changes to the main app and once the main app is opened, it processes these changes and writes them to the NSPersistentCloudKitContainer which then synchronizes across devices. This is not an option for me as it would result in a stale state and potential data conflicts with different devices. For example, when a user has the same widget on their iPhone and their iPad, taps a button on the iPhone widget, that change would not be reflected on the iPad widget until the user decides to open the app on the iPhone. At the same time, the user could tap the button multiple times on their iPad widget, resulting in a conflicting state on both devices. Thus, this approach is not a viable solution.

An answer to this question will be greatly appreciated. The whole code including the setup of the Core Data stack is included in the repository reference above. Thank you!

Answered by DTS Engineer in 831117022

The reason your main app doesn't update when your widget changes the shared store is most likely because the viewContext in your main app doesn't automatically detect and merge remote changes. Here, remote changes mean the changes made by a different process, or by using batch processing.

Your main app can detect and handle remote changes in the following way:

  1. Observe the the .NSPersistentStoreRemoteChange notification to get notified of any remote change.

  2. In the notification handler, consume the store persistent history to detect the relevant changes and merge them to the managed object context tied to your main app UI. Alternatively, you can manage to reset the context, and then re-fetch, which should give you the up-to-date data.

For more information about this topic, see the following Apple sample projects:

Regarding using Core Data + CloudKit (NSPersistentCloudKitContainer) in an extension, I'd like to suggest against doing that, as discussed in the following technote:

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

PS: What's curious:

When someone downloads my app from the App Store, the synchronization between the app and the widget actually seems to work. I get lots of user feedback and only once has a user reported that the widget is not in sync with the app.

However, when I run the app directly from Xcode on my (real) devices, I observe the buggy behavior described above. I could hope that it magically works once I release it on the App Store, but that's too risky and I don't want to do that until I have figured out what the underlying problem is.

The reason your main app doesn't update when your widget changes the shared store is most likely because the viewContext in your main app doesn't automatically detect and merge remote changes. Here, remote changes mean the changes made by a different process, or by using batch processing.

Your main app can detect and handle remote changes in the following way:

  1. Observe the the .NSPersistentStoreRemoteChange notification to get notified of any remote change.

  2. In the notification handler, consume the store persistent history to detect the relevant changes and merge them to the managed object context tied to your main app UI. Alternatively, you can manage to reset the context, and then re-fetch, which should give you the up-to-date data.

For more information about this topic, see the following Apple sample projects:

Regarding using Core Data + CloudKit (NSPersistentCloudKitContainer) in an extension, I'd like to suggest against doing that, as discussed in the following technote:

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Dear Ziqiao Chen,

thank you for your reply and the hints pointing me in the right direction. I managed to solve my problem no. 1 and now the sync works both between the app and the widget, but also between different devices.

I had already observed the .NSPersistentStoreRemoteChange notification in my sample project, but it turns out that wasn't even necessary when automaticallyMergesChangesFromParent is set to true on the viewContext.

The Problem with Syncing from a Widget

Now the problem is the following:

Regarding using Core Data + CloudKit (NSPersistentCloudKitContainer) in an extension, I'd like to suggest against doing that

⬆️ Your reply and the linked Technical Note both imply that syncing data with iCloud this way from both the app and its widget is not reliable as two NSPersistentCloudKitContainers pointing to the same persistent store can get in conflict (running on different threads) and throw the following error:

CloudKit setup failed because there is another instance of this persistent store actively syncing with CloudKit in this process.

In other words: I cannot do that in a production app.

From Avoid synchronizing a store with multiple persistent containers:

When working with an extension, you don’t control its lifecycle. It is perfectly possible that your extension is launched when your app is running, or vice versa, and both of them try to load the shared store. To avoid the conflict, consider having the app in charge of the synchronization. An extension that has the capability to present UI can remind users to launch the app to synchronize with CloudKit, if that is an appropriate user experience.

So now I know what doesn't work. My question is: What does?

What's the recommended, safe approach to sync data from an interactive widget with iCloud?

Showing a hint to users inside the widget that they should open the main app in order to sync is hardly a practical solution and destroys not only the user experience, but the very purpose of interactive widgets. If I need to open the app each time I've pressed a button on the widget, it's not very interactive after all.

Sync an interactive widget's Core Data store with the main app (and iCloud)
 
 
Q