Handling iCloud Account Transitions with Core Data

There is much confusion about what Core Data does automatically and what it does not do automatically in iOS7.  Many have interpreted the WWDC2013 Session 207 as indicating that the app no longer has to worry about dealing with iCloud account switches because Core Data will do all that automatically but unfortunately that’s not generally true.

Let me clear up a few misconceptions:

  • Core Data WILL NOT migrate your data between the iCloud store and the local store – PERIOD.  You have to do this yourself in your app.
  • If you open a persistentStore with iCloud options regardless of whether an iCloud account is available or not then you will either get a local store created by Core Data for you or you will get a store specific to the iCloud account that is in use created by Core Data for you.  These stores are different stores and will only contain data that has been entered while they are in use.
  • The local store Core Data creates is NOT the same as the fallback store, which Core Data creates and uses while it is busy creating the iCloud store.
  • The fallback store doesn’t really exist as a separate file as far as I can determine.  It seems that during first startup of the app Core Data creates the account store file and gives the app access to it and then creates a separate sideLoad store file where if does the initial import from iCloud.  Once this initial import is done then it somehow merges the sideLoad file contents into the store file your app is already using.

Core Data will create the following stores when you use the iCloud option:

  • A local store that it will automatically use if it detects that the iCloud account is not active or if the Apps Documents & Data security setting is OFF.
  • A sideload or fallback store that it will provide to your app while it is busy creating the iCloud store (which may take some time if the initial import if quite large)
  • An iCloud store which is the store that is synchronised with other peers via iCloud

CoreDataLibraryApp_CDStores

In the simulator’s directory structure shown above you can see all the stores.  Note that Core Data will only create the stores it needs when it needs them so you may not see them all all the time.

Fallback Store

Notice the sideLoad store is sitting in the tmp directory – well as it turns out this is not the fallback store.  Here is how Core Data seems to do things when you open a store with iCloud options for the first time (see the logs at the end of this page):

  • Creates the fallback store at the actual store location (see store marked Core Data’s fallback and iCloud store above) and provides access to your App – doesn’t really seem like a fallback store to me but anyway.
  • Sends a storesDidChange notification with a null transition type and the final store file URL in the added stores array (exactly the same as if it was starting with an iCloud store that already exists)
  • Creates the sideLoad store in a tmp directory and it downloads the initial import from iCloud
  • Sends a storesWillChange notification with transition type 4
  • Merges the sideLoad store with the original fallback store
  • Sends a storesDidChange notification with transition type 4

Now unfortunately because the fallback store uses the exact same store file name as the final iCloud store it is not possible to easily determine on startup whether Core Data is using a fallback store or the “Real” iCloud store because regardless of the initial state you always get the same initial storesDidChange notification and if Core Data had to create a new file for you and download iCloud data then you suddenly get the storesWillChange notification.

So the only way it seems you can determine what state you are in at startup is to first check if the iCloud file exists, and if not then you know to expect a storesDidChange notification with transition type 4 so you can wait for it.  So on startup check if the file exists and if not set a flag indicating _waiting_for_icloud_store (or something).  Then after opening the store don’t enable the user interface until you have received the next storesDidChange notification with transition type 4, perhaps also first checking to see if your seed data is present.  No point checking the store file names because they are always the same!  In subsequent startups when the iCloud file does exist then no need to wait for the next storesDidChange message.

If you are not using iCloud options then don’t expect to get any storesDidChange notifications at all.

Initially I was under the impression that the Using local storage: 1 log message was an indication that Core Data was using the fallback store and Using local storage: 0 meant that it was using the iCloud store.  However my testing shows that you get both these messages on startup regardless of the initial state of the store file so it’s not really clear what they mean for subsequent startups.  See the second set of log records for subsequent startup sequence.

If this is an issue for your app then you should avoid letting the user do anything until you are sure the app has access to the iCloud store.  Now in theory this will always be after you get the storesDidChange notification – assuming the following is true:

  • iCloud is enabled (user logged in and Documents & Data is ON), and
  • You are using the NSPersistentStoreUbiquityNameKey option to open to store

So What to Do ?

Depending on whether you want to allow your user to choose between using iCloud and local storage and exactly what level of control you want to give the user you have two options:

  1. Option 1 is to provide a User Preference setting which allows the user to choose between iCloud or Local storage for the App.  You can implement this either in the App itself with a special settings control view or you could use a Settings bundle which will provide a settings page for your app in the Settings app. Your app then creates a store with or without iCloud options and handles migration to and from iCloud when the user changes their preference.
  2. Option 2 is to always open the store with the iCloud option and to respond to iCloud account changes when your app receives the storesDidChange notification.

These options are explained in more detail below.

Option 1

By default a new applications Documents & Data security setting is set to ON in the Settings app.  Apple seem to indicate that on first installation, if the user is logged in to an iCloud account you should ask the user if they want to use iCloud or local storage, in a manner similar to the way the Pages application does.  Your app then creates either a local store or an iCloud store.

If the user then decides to change their preference your app migrated the existing store to or from iCloud.  If the user has chosen to use local storage your might also need to remove the iCloud content and any copies of the store on other devices.  This seems to be consistent with the behaviour of Apple’s own apps.  However this does make it a bit tricky for the user to keep local copies on multiple devices because once one device has moved the store to local storage the other devices should see the store disappear before they could migrate a copy to local storage themselves.

Alternately you could just leave the store in iCloud but then you would need to provide some alternative method for removing the store from iCloud.

In the sample App I remove the iCloud contents and on peer devices Core Data will detect this removal and switch to an new empty store.

Option 2

With this option you always open a store with iCloud options and when you receive the storesDidChange notification you check to see if Core Data is switching to its local store, in which case you would migrate the previously open iCloud store to the local store.  Exactly how you would do this safely when you receive this notification is not clear but there seems to be some who believe this is an option.

In addition if the user turns iCloud back on you have to figure out how to identify that this has happened and then migrate the local store to iCloud, bearing in mind that the old iCloud store is still there (you couldn’t remove it when you switched to the local store because iCloud was already unavailable then). So now you have to ask the user what they want to do.

There are some risks to taking this approach, the most significant one I believe is the fact that Apple’s documentation indicates that they may remove an iCloud accounts store file at any time once the iCloud account becomes inactive.  By the time you receive the storesDidChange notification the iCloud account is already inactive so you may find the store has been removed before you are able to migrate the file.  Option 1 does not have to deal with this issue because the user has to change the app’s iCloud preference in order to switch between local or iCloud storage and they should do so while still logged in to iCloud.

For Option 2 you would need to store the currently opened store file in NSUserDefaults and then use this when receiving the storesDidChange notification.

Also when you decide you need to upgrade the Core Data model version then your App will also receive the storeDidChange notifications during the upgrade process.  In fact you get them on two successive startups for some reason.

I have not tried using the approach outlined in Option 2.

Startup Log

2014-02-22 09:53:25.939 CoreDataLibraryApp[27026:70b] OSCDStackManager.openPersistentStore store Options are {
NSInferMappingModelAutomaticallyOption = 1;
NSMigratePersistentStoresAutomaticallyOption = 1;
NSPersistentStoreUbiquitousContentNameKey = "persistentStore_ICLOUD";
NSSQLitePragmasOption = {
"journal_mode" = DELETE;
};
}
2014-02-22 09:53:25.939 CoreDataLibraryApp[27026:70b] OSCDStackManager.openPersistentStore addPersistentStoreWithType about to be called...
2014-02-22 09:53:26.966 CoreDataLibraryApp[27026:70b] OSCDStackManager.storesDidChange: called - >>>>>>>>>>>>>>>>>>>>>>>>>>>>
2014-02-22 09:53:26.967 CoreDataLibraryApp[27026:70b] OSCDStackManager.storesDidChange: userInfo is {
added = (
" (URL: file:///Users/duncangroenewald/Library/Application%20Support/iPhone%20Simulator/7.1/Applications/1460D515-A0EE-4396-8EBF-26888EF7DDD0/Documents/CoreDataUbiquitySupport/duncangroenewald~simAABC628E-9D5E-58F7-9B8D-0BC724C6D0C8/persistentStore_ICLOUD/80693193-F21F-4224-ACF1-8214225A913D/store/persistentStore_ICLOUD)"
);
}
2014-02-22 09:53:26.967 CoreDataLibraryApp[27026:70b] OSCDStackManager.storesDidChange: transition type is (null)
2014-02-22 09:53:26.975 CoreDataLibraryApp[27026:70b] -[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](771): CoreData: Ubiquity: duncangroenewald~simAABC628E-9D5E-58F7-9B8D-0BC724C6D0C8:persistentStore_ICLOUD
Using local storage: 1
2014-02-22 09:53:26.975 CoreDataLibraryApp[27026:70b] OSCDStackManager.openPersistentStore addPersistentStoreWithType completed successfully...
2014-02-22 09:53:26.976 CoreDataLibraryApp[27026:70b] OSCDStackManager.persistentStoreCoordinator persistentStoreCoordinator called
2014-02-22 09:53:26.977 CoreDataLibraryApp[27026:70b] OSCDStackManager.getCompanyData Companies: 0
2014-02-22 09:53:26.985 CoreDataLibraryApp[27026:70b] AppDelegate.application:didFinishLaunchingWithOptions: didFinishLaunchingWithOptions done.
2014-02-22 09:53:27.017 CoreDataLibraryApp[27026:70b] MenuViewController.viewWillAppear: called
2014-02-22 09:53:28.138 CoreDataLibraryApp[27026:70b] OSCDStackManager.getCompanyData Companies: 0
2014-02-22 09:53:41.410 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesWillChange: called - >>>>>>>>>>>>>>>>>>>>>>>>>>>>
2014-02-22 09:53:41.411 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesWillChange: transition type is 4
2014-02-22 09:53:41.411 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesWillChange: transition type is NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted
2014-02-22 09:53:41.411 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesWillChange: added stores are (
" (URL: file:///Users/duncangroenewald/Library/Application%20Support/iPhone%20Simulator/7.1/Applications/1460D515-A0EE-4396-8EBF-26888EF7DDD0/Documents/CoreDataUbiquitySupport/duncangroenewald~simAABC628E-9D5E-58F7-9B8D-0BC724C6D0C8/persistentStore_ICLOUD/80693193-F21F-4224-ACF1-8214225A913D/store/persistentStore_ICLOUD)"
)
2014-02-22 09:53:41.412 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesWillChange: removed stores are (
" (URL: file:///Users/duncangroenewald/Library/Application%20Support/iPhone%20Simulator/7.1/Applications/1460D515-A0EE-4396-8EBF-26888EF7DDD0/Documents/CoreDataUbiquitySupport/duncangroenewald~simAABC628E-9D5E-58F7-9B8D-0BC724C6D0C8/persistentStore_ICLOUD/80693193-F21F-4224-ACF1-8214225A913D/store/persistentStore_ICLOUD)"
)
2014-02-22 09:53:41.412 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesWillChange: changed stores are (null)
2014-02-22 09:53:41.412 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesWillChange: This is not the main thread so dispatch a sync job to the main thread
2014-02-22 09:53:41.523 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesDidChange: called - >>>>>>>>>>>>>>>>>>>>>>>>>>>>
2014-02-22 09:53:41.523 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesDidChange: userInfo is {
NSPersistentStoreUbiquitousTransitionTypeKey = 4;
added = (
" (URL: file:///Users/duncangroenewald/Library/Application%20Support/iPhone%20Simulator/7.1/Applications/1460D515-A0EE-4396-8EBF-26888EF7DDD0/Documents/CoreDataUbiquitySupport/duncangroenewald~simAABC628E-9D5E-58F7-9B8D-0BC724C6D0C8/persistentStore_ICLOUD/80693193-F21F-4224-ACF1-8214225A913D/store/persistentStore_ICLOUD)"
);
removed = (
" (URL: file:///Users/duncangroenewald/Library/Application%20Support/iPhone%20Simulator/7.1/Applications/1460D515-A0EE-4396-8EBF-26888EF7DDD0/Documents/CoreDataUbiquitySupport/duncangroenewald~simAABC628E-9D5E-58F7-9B8D-0BC724C6D0C8/persistentStore_ICLOUD/80693193-F21F-4224-ACF1-8214225A913D/store/persistentStore_ICLOUD)"
);
}
2014-02-22 09:53:41.525 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesDidChange: transition type is 4
2014-02-22 09:53:41.525 CoreDataLibraryApp[27026:3a0b] OSCDStackManager.storesDidChange: transition type is NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted
2014-02-22 09:53:41.526 CoreDataLibraryApp[27026:70b] OSCDStackManager.storesDidSave: main thread saved context
2014-02-22 09:53:41.526 CoreDataLibraryApp[27026:3a0b] -[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](771): CoreData: Ubiquity: duncangroenewald~simAABC628E-9D5E-58F7-9B8D-0BC724C6D0C8:persistentStore_ICLOUD
Using local storage: 0
2014-02-22 09:53:42.535 CoreDataLibraryApp[27026:70b] OSCDStackManager.getCompanyData Companies: 1650

Subsequent Startup Logs

2014-02-22 10:35:55.776 CoreDataLibraryApp[27097:70b] OSCDStackManager.openPersistentStore store Options are {
NSInferMappingModelAutomaticallyOption = 1;
NSMigratePersistentStoresAutomaticallyOption = 1;
NSPersistentStoreUbiquitousContentNameKey = "persistentStore_ICLOUD";
NSSQLitePragmasOption = {
"journal_mode" = DELETE;
};
}
2014-02-22 10:35:55.776 CoreDataLibraryApp[27097:70b] OSCDStackManager.openPersistentStore addPersistentStoreWithType about to be called...
2014-02-22 10:35:56.785 CoreDataLibraryApp[27097:70b] OSCDStackManager.storesDidChange: called - >>>>>>>>>>>>>>>>>>>>>>>>>>>>
2014-02-22 10:35:56.785 CoreDataLibraryApp[27097:70b] OSCDStackManager.storesDidChange: userInfo is {
added = (
" (URL: file:///Users/duncangroenewald/Library/Application%20Support/iPhone%20Simulator/7.1/Applications/1460D515-A0EE-4396-8EBF-26888EF7DDD0/Documents/CoreDataUbiquitySupport/duncangroenewald~simAABC628E-9D5E-58F7-9B8D-0BC724C6D0C8/persistentStore_ICLOUD/80693193-F21F-4224-ACF1-8214225A913D/store/persistentStore_ICLOUD)"
);
}
2014-02-22 10:35:56.785 CoreDataLibraryApp[27097:70b] OSCDStackManager.storesDidChange: transition type is (null)
2014-02-22 10:35:56.791 CoreDataLibraryApp[27097:70b] -[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](771): CoreData: Ubiquity: duncangroenewald~simAABC628E-9D5E-58F7-9B8D-0BC724C6D0C8:persistentStore_ICLOUD
Using local storage: 1
2014-02-22 10:35:56.792 CoreDataLibraryApp[27097:70b] OSCDStackManager.openPersistentStore addPersistentStoreWithType completed successfully...
2014-02-22 10:35:56.792 CoreDataLibraryApp[27097:70b] OSCDStackManager.persistentStoreCoordinator persistentStoreCoordinator called
2014-02-22 10:35:56.799 CoreDataLibraryApp[27097:70b] OSCDStackManager.getCompanyData Companies: 1650
2014-02-22 10:35:56.806 CoreDataLibraryApp[27097:70b] AppDelegate.application:didFinishLaunchingWithOptions: didFinishLaunchingWithOptions done.
2014-02-22 10:35:56.819 CoreDataLibraryApp[27097:70b] MenuViewController.viewWillAppear: called
2014-02-22 10:35:57.100 CoreDataLibraryApp[27097:310b] -[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](771): CoreData: Ubiquity: duncangroenewald~simAABC628E-9D5E-58F7-9B8D-0BC724C6D0C8:persistentStore_ICLOUD
Using local storage: 0
2014-02-22 10:35:57.127 CoreDataLibraryApp[27097:70b] OSCDStackManager.storesDidSave: main thread saved context
2014-02-22 10:35:58.136 CoreDataLibraryApp[27097:70b] OSCDStackManager.getCompanyData Companies: 1650

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s