Skip to content

Commit

Permalink
Merge branch 'master' of github.com:lhunath/UbiquityStoreManager
Browse files Browse the repository at this point in the history
  • Loading branch information
lhunath committed Apr 28, 2013
2 parents d95d169 + a09cc63 commit 9a4d52c
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 3 deletions.
71 changes: 70 additions & 1 deletion README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,75 @@ If you read the previous section carefully, you should understand that problems

Many of these methods take a `localOnly` parameter. Set it to `YES` if you don’t want to affect the user’s iCloud data. The operation will happen on the local device only. For instance, if you run `[manager deleteCloudStoreLocalOnly:YES]`, the cloud store on the device will be deleted. If `cloudEnabled` is `YES`, the manager will subsequently re-open the cloud store which will cause a re-download of all iCloud’s transaction logs for the store. These transaction logs will then get replayed locally causing your local store to be repopulated from what’s in iCloud.

# Under The Hood

`UbiquityStoreManager` tries hard to hide all the details from you, maintaining the persistentStoreCoordinator for you. If you're interested in what it does and how things work, read on.

## Terminology

* USM = `UbiquityStoreManager`, this project.
* PSC = `Persistent Store Coordinator`, the object that coordinates between the store file, the store model, and managed contexts.
* MOC = `Managed Object Context`, a "view" on the data store. Each context has their own in-memory idea of what the store's objects look like.

## The Ideology

The idea behind USM was to create an implementation that hides all the details of loading the persistent store and allows an application to focus on simply using it.

To accomplish this, USM exposes only the API needed for an application to begin using a store, in addition to some lower-level API an application might need to handle exceptional situations in non-default ways. USM implements sane and safe default behavior for everything that isn't just "using the store", but allows an application to make different choices if it wants to.

There are different ways one may want to set up and configure their persistence layer. USM has made the following choices/assumptions for you:
* There is a difference between the user's iCloud data and the user's local data:
* When iCloud is **disabled**, an independant local store will be loaded. The user will not see their iCloud data.
* When iCloud is **enabled**, the iCloud store will be loaded. This store will initially be a copy of the local store, but any changes will **not** be saved in the local store: they will only exist on the cloud.
* The choice of whether iCloud is enabled or not is a **device-specific** one: enabling iCloud on one device does **not** cause all devices to suddenly switch over to iCloud.
* Application developers are recommended to ask their user upon first start-up of the app whether they want iCloud enabled and allow them to switch it on or off with a setting.
* When the user enables iCloud and there is no cloud store yet, a new iCloud store to be created and seeded by (filled up with) the data in the local store.
* When the user enables iCloud on two devices at the same time, the **last device** that finishes seeding a cloud store will win, and **both devices** will begin using the winning cloud store.
* When the user enables iCloud and there is already an active cloud store, the device will begin using the existing cloud store.
* To allow users to re-seed the cloud store from a new device, USM provides utilities for deleting the cloud store. After deletion of the cloud store, enabling iCloud will cause a new store to be seeded from the local store again.
* When a user manually deletes the cloud store from their iCloud account (eg. through their device's Settings), USM will clean up after it and switch back to the user's local store. The intention of the user was clearly to delete the data in the cloud.
* An application hook exists which allows the application to decide how to handle the case, if they don't want USM to switch to the local store.
* USM immediately unloads the cloud store if the user logs out of their iCloud account on the device or switches to another user's iCloud account. In the latter case, an iCloud store will be created on the new cloud account from the local store.

## How Things Work

When initialized, USM will begin loading a store. Before any store is loaded, the application's delegate is first notified via `willLoadStoreIsCloud:`. At this point, the application may still make any chances it wishes to USM's configuration, and since this method is called from the internal persistence queue, it is in fact the **recommended** place to perform any initial configuration of USM. Do not do this from the method that called USM's init.

The `cloudEnabled` property determines what store will be loaded. If `YES`, USM will begin loading the cloud store. If `NO`, the local store.

The process of loading a local store is relatively simple. The directory hierarchy to the store file is created if it didn't exist yet, same thing for the store file. Automatic light-weight store model migration is enabled and mapping inference is as well. You can specify additional store loading options via USM's init method, such as file security options. If the store is loaded successfully, the application is notified and receives the PSC it needs to access the store via
`didLoadStoreForCoordinator:isCloud:`. If the store fails to load for some reason, the application will not have access to persistence and is notified via `failedLoadingStoreWithCause:context:wasCloud:`. It can choose to handle this failure in some way.

The process of loading a cloud store is somewhat more involved. It's mostly the same as for the local store, but there is a bunch of extra logic:
* If the cloud content no longer exists but the local cloud store file does, it is assumed the user wanted to delete their cloud data and the local store file is deleted.
* If no cloud content exists, a new cloud store is created by migrating the local store's contents.
* This new cloud store is identified by a random UUID.
* The new UUID is kept locally until the migration and opening process for this store is a success.
* Upon such success, this store is marked as the "active" cloud store by making its UUID ubiquitous.
* If the cloud store fails to load once, a recovery attempt is made which deletes the local cloud store file and re-opens the cloud store, allowing iCloud to re-initialize the local cloud store file by importing all the cloud content again.
* If the cloud store continues to fail loading, the store is marked as "corrupted".
* If the store is successfully loaded, USM waits 30 seconds (to see if any cloud content will fail to import) and if no failure is detected, it checks to see if other devices have reported the cloud store as "corrupted". If so, cloud content recovery is initiated from this device which is, due to its success in loading the store, deemed healthy.

When a store is loaded, USM monitors it for deletion. When the cloud store is deleted, USM will clean up any "corruption" marker, the local cloud store file, and will fall back to the local store unless the application chooses to handle the situation via `ubiquityStoreManagerHandleCloudContentDeletion:`. When the local store is deleted, USM just reloads causing a new local store to be created.

The `cloudEnabled` setting is stored in `NSUserDefaults` under the key `@"USMCloudEnabledKey"`. When USM detects a change in this key's value, it will reload the active store allowing the change to take effect. You can use this to add a preference in your `Settings.bundle` for iCloud.

Whenever the application becomes active, USM checks whether the iCloud identity has changed. If a change is detected and iCloud is currently enabled, the store is reloaded allowing the change to take effect. Similarly, when a change is detected to the active ubiquitous store UUID and iCloud is currently enabled, the store is also reloaded.

When ubiquitous changes are detected in the cloud store, USM will import them using a private MOC. Your application's delegate can specify a custom MOC to use for this, so that it can become aware of these changes immediately. To do this, the application should return its MOC via `managedObjectContextForUbiquityChangesInManager:`. If ubiquitous changes fail to import, the store is reloaded to retry the process and verify whether any corruption has occurred. Upon successful completion,
the `UbiquityManagedStoreDidImportChangesNotification` notification is posted.

The cloud store is marked as "corrupted" when it fails to load or when cloud transaction logs fail to import. To detect the failure of transaction log import attempts made by Apple's Core Data, USM swizzles `NSError`'s init method. This way, it can detect when an `NSError` is created for transaction log import failures and act accordingly.

When cloud "corruption" is detected the cloud store is immediately unloaded to prevent further desync. When the store is not healthy on the current device (the store failed to load or failed to import ubiquitous changes), USM will just wait and the persistence layer will remain unavailable. When the store is healthy on the current device, USM will initiate a rebuild of the cloud content by deleting the cloud content from iCloud and creating a new cloud store with a new random UUID, which
will be seeded from the healthy local cloud store file. The new cloud store will be filled with the old cloud store's data and healthy cloud content will be built for it. Upon completion, the non-healthy devices that were waiting will notice a new cloud store becoming active, will load it, and will become healthy again. The application can hook into this process and change what happens by implementing `handleCloudContentCorruptionWithHealthyStore:`.

Any store migration can be performed by one of four strategies:
* `UbiquityStoreMigrationStrategyIOS`: This strategy performs the migration via a coordinated `migratePersistentStore:` of the PSC. Some iOS versions have bugs here which makes this generally unreliable.
* `UbiquityStoreMigrationStrategyCopyEntities`: This strategy performs the migration by copying over any non-ubiquitous metadata and copying over all entities, properties and relationships from one store to the other. This is the default strategy.
* `UbiquityStoreMigrationStrategyManual`: This strategy allows the application's delegate to perform the migration by implementing `manuallyMigrateStore:`. This may be necessary if you have a really huge or complex data model or want some more control over how exactly to migrate your entities.
* `UbiquityStoreMigrationStrategyNone`: No migration is performed and the new store is opened as-is.

# License

`UbiquityStoreManager` is licensed under the [LGPLv3](LICENSE). Feel free to use it in any of your applications. I’m also happy to receive any comments, feedback or review any pull requests.
`UbiquityStoreManager` is licensed under the [LGPLv3](LICENSE). Feel free to use it in any of your applications. I’m also happy to receive any comments, feedback or review any pull requests.
4 changes: 2 additions & 2 deletions UbiquityStoreManager/UbiquityStoreManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ typedef enum {
/** Triggered when the cloud content is deleted while cloud is enabled.
*
* When the cloud store is deleted, it may be that the user has deleted his cloud data for the app from one of his devices.
* It is therefore not necessarily desirable to immediately re-create a cloud store. By default, the manager will just unload the store,
* leaving you with no persistence.
* It is therefore not necessarily desirable to immediately re-create a cloud store. By default, the manager will unload the cloud store
* and fall back to the local store.
*
* It may be desirable to show UI to the user allowing him to choose between re-enabling iCloud ([manager deleteCloudStoreLocalOnly:NO])
* or disabling it and switching back to local data (manager.cloudEnabled = NO).
Expand Down
2 changes: 2 additions & 0 deletions UbiquityStoreManager/UbiquityStoreManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,8 @@ - (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError
if (self.cloudEnabled) {
if ([self.delegate respondsToSelector:@selector(ubiquityStoreManagerHandleCloudContentDeletion:)])
[self.delegate ubiquityStoreManagerHandleCloudContentDeletion:self];
else
self.cloudEnabled = NO;
}
else
[self reloadStore];
Expand Down

0 comments on commit 9a4d52c

Please sign in to comment.