drewmccormack / ensembles

A synchronization framework for Core Data.
MIT License
1.63k stars 132 forks source link

Force save monitoring programmatically - useful in app extensions? #271

Closed UberJason closed 5 years ago

UberJason commented 6 years ago

Hi @drewmccormack,

Please correct my interpretation of how Ensembles monitors saves if necessary. From the documentation book, it seems like whenever I save an object to Core Data, Ensembles passively monitors the persistent store for changes (via notifications like NSManagedObjectContextWillSaveNotification and NSManagedObjectContextDidSaveNotification) before saving the relevant deltas in the event store. These deltas aren't available to other devices until the next mergeWithCompletion: call.

App extensions (e.g. Notification Center widgets and SiriKit intents) are short lived processes that run separate from the main iOS app and which have much more stringent rules on when they may be spun up or killed. In cases where you want to access persistent storage in both an app extension and an iOS app, you have to put the persistent store in a shared app group.

So my question is sort of two-part, a specific part and a general part:

drewmccormack commented 6 years ago

If you want to ensure that a save is committed to disk, you can either do a merge, or use the processPendingChanges method of CDEPersistentStoreEnsemble.

The extension is a separate process, so it cannot see saves from the main app, or vice versa. This means the only really safe way to use Ensembles for each is to have two separate Core Data stores, with an Ensemble in the main app, and one in the extension, using different base URLs (ie where it caches its data). This could be quite heavy.

An alternative solution is to have the extension just have read-only access to the Core Data store, and not to use Ensembles. Instead, updates of the data would be relayed back to the main app via a simple communication mechanism (eg plist), and loaded and saved by the main app, which would trigger the Ensembles update.

I suppose it would be possible to somehow have Ensembles adapted to work with extensions, but it would be a major project, and not entirely clear how that work would work.

UberJason commented 6 years ago

The extension is a separate process, as you said, but it is possible to put the Core Data store in a "shared app group" which the main app and any extensions have access to. That's the recommended technique for shared data access according to Apple.

That said, the completion handler of processPendingChanges gets called when the entire save process (including saving deltas to the change store), right? If so, I think that's exactly what I was looking for. I can set up a background task and end it when processPendingChanges calls its completion handler.

drewmccormack commented 6 years ago

Yes, you can certainly use processPendingChanges like that.

But you still have the issue with Ensembles not seeing the saves when fired in another process. Apple suggest sharing a store in two processes, but Apple are not considering that you have a syncing store. Actually, even without sync, you still have an issue, because you need to notify your main app that it needs to reload data in some way when the extension makes a change.

UberJason commented 6 years ago

Leaving aside the issue of notifying the app of a change for a moment: suppose I keep the syncing store in the same app group, just like the Core Data store, would that work? How does Ensembles handle two instances of Ensembles potentially touching the same syncing store? (E.g.: main app makes a save, Ensembles in main app records the delta. Extension makes a save, Ensembles in extension records the delta. Both are recorded to the same syncing store which is saved in the shared app group.)

As for notifying the app, presumably there are ways around that - a naive solution being for either the extension or the app to write to a file in the shared app group upon each data save, and the other to look at that file upon resume points (e.g. applicationDidBecomeActive) and potentially doing a new Core Data fetch if needed. Or, if the fetch is lightweight enough, simply doing a new Core Data fetch every time the app resumes.

UberJason commented 5 years ago

Just wanted to follow up on this and then close it out, but if anyone else is thinking of doing something like this - this approach does work. By saving the Ensembles data store (and "cloud file system" if using Multipeer Connectivity as the sync mechanism) in the app group along with the Core Data store, you can use Ensembles in an app extension (I used it in an Intent extension for Siri Shortcuts in iOS 12). You probably want to be really careful about memory usage because app extensions get low memory overheads, but it works.