I'm working on a new app that uses Core Data and iCloud. I'm following the iCloudCoreDataStack demo and the iCloud Design Guide.
So far the synchronization between devices is working well, but I haven't figured out how to seed a small amount of data the first time the app is used on the user's first device and skip the seeding if the app is used on a second device (since it should download from iCloud instead).
This should be easy, just ask the iCloud Container if it has any data. Download the data if it exists or create new data if it doesn't.
But I couldn't find a way to do this :-(
I can think of three ways to solve this:
Use migratePersistentStore:toURL:options:withType:error:
I have a very small amount of data, so for this case this feels like overkill
Store a value on NSUbiquitousKeyValueStore to mark if the initial synchronization has been made
I tried using NSUbiquitousKeyValueStore, but sometimes it would take too long to get the value from
the UbiquitousKeyValueStore, so the seed data would be created even when not needed, resulting in duplicates.
Use a sentinel file to have the same effect of #2 (I'm not sure how to implement this)
The App is iOS 7 only and new, so there's no need to migrate old user data.
Every relevant tutorial and book that I found seemed to be using the pre-iOS7 super complex way of doing things (e.g. using a fallback store) that is not necessary on iOS 7.
Either I'm missing something (often the case) or this is more complicated than it should be.
I appreciate any suggestions and pointers.
It is never a good idea to seed a distributed datastore with an initial dataset. Generally this initial data can be packaged into a store file that is shipped with the application, and added as a second persistent store to the coordinator used by your application's managed object context.
That said, it is possible, although unwise to seed based on the completion of Core Data's initial import.
You need to wait for NSPersistentStoreCoordinatorStoresDidChangeNotification with NSPersistentStoreUbiquitousTransitionTypeKey set to NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted.
If there is no data in the store you can seed your initial data set at that point.
However it is important to realize that multiple devices could receive the initial import notification without importing the seeded data and thus seed it again. There is no way to avoid this.
On the point of shipping a second persistent store with your application, to serve as seed data.
This is accomplished as Marcus points out below by adding it as a read only store to the persistent store coordinator that is in use by your app's managed object context.
NSDictionary *options = #{ NSReadOnlyPersistentStoreOption: #YES };
[_psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:seedStoreURL
options:options
error:&localError];
NSDictionary *iCloudOptions = #{ NSPersistentStoreUbiquitousContentNameKey: #"storeName" };
[_psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:iCloudStoreURL
options:iCloudOptions
error:&localError];
_moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[appMOC setPersistentStoreCoordinator:_psc];
This way your application's managed object context has access to data from both stores and will fetch both sets from fetch requests. The managed object context is also smart enough to automatically insert new objects into the correct store (because one of them is read only).
The only trick is that if you want your user to be able to modify data from the seed store you'll need to move those objects to the iCloud store.
This is a much easier approach than trying to migrate the dataset wholesale because ideally your user will only be using a single device at a time. In the case of a collision you'll at most have to resolve a few duplicate records as opposed to trying to detect duplication across the entire dataset.
Try this approach:
Before creating the local store check if one has already been created in iCloud by looking for the presence of a directory with the 'NSPersistentStoreUbiquitousNameKey' name in the iCloud /CoreData directory.
If one exists then this means that some other device has already created a store and shared it in iCloud so when creating your local store on the device don't add seed data because this will already exist in iCloud.
If one does not exist then no other device has shared the store in iCloud yet so you can probably create your seed data and sync to iCloud.
NOTE that the following scenarios are not catered for:
A user is running the app on a device that does not have iCloud
enabled - if the user chooses to now sync with iCloud from this
device you will have to deal with issues arising from trying to merge
data from this device with data already in iCloud. Once again you
can check for the presence of data in iCloud and then ask the user
whether they want to try merging data from the device or whether they
want to replace the data with data from iCloud.
A user is running the app on a device that is not connected to the
network - and has not had any data synced from iCloud yet so thinks
there is no file already in iCloud. The app will then create seed
data and when it gets a network connection Core Data will merge the
data - you app may have to deal with the ensuing problems.
For these scenario's you may need to try user education.
Related
Don't know if this type of question is ok here but I'll ask anyway.
I have an iOS iCloud Core Data application that works decently well (though I think it does need some synchronization tweaking) and am considering allowing the user so save a copy of the database to iCloud drive in a UIDocument. This would allow the user to possibly segment the database manually and load only the portions they want to use at the moment as well as provide peace of mind considering that the data may include some insurance claims data.
I'm weighting several alternatives to allow this to happen.
Option 1 is to just create an iOS based save and restore process which, when triggered, would dump the database (maybe at max 1000 objects) into an NSArray of NSDictionary objects (one dictionary per row) and then JSONize the NSArray into NSData and save it in a new UIDocument on iCloud drive. A restore process would list possible archives to restore and then allow the user to pick one. The restore process would do the reverse of the save process.
Option 2 would be to create Mac App to handle the save/restore process which would allow the user to save the archives on the user's Mac. This seems a lot less difficult though it would require a set of apps working together. The mechanism would be similar as option 1 but the data would go to a file specified in a standard Mac file system pick screen.
Personally, I'd like to do both of them but before I start I want to make sure that there's not an easier way than dumping the entire SQL database into an NSArray of NSDictionaries.
A complicating factor is that the database contains images which are in the database as NSData. I could string-ize the NSData, add it into the NSDictionary build, then let the JSON process do it's thing.
I'm just fine with proceeding this way but I wanted to make sure I wasn't overlooking some type of Apple method of doing this in an easier way.
Another thought that came to me was maybe using some sort of SQLite utility to do save and restore locally in one session which would then replicate the results across all of the devices via iCloud. Again, sounds like more of a Mac answer to me but I don't really know.
Again, any suggestions would be great.
iCloud supports synchronizing CoreData databases. See this article for some more information.
Also, you might consider looking at CloudKit for storing this data directly in iCloud, so it can be accessed on multiple devices.
I have a CoreData app (using https://github.com/lhunath/UbiquityStoreManager), backed by iCloud. In one use case a user with a local store enables iCloud (where data already exists). I want to prompt the user to make a decision of whether to migrate the local data to iCloud or just use the iCloud version. As part of this, I'd like to display the device name and last sync date of the version in iCloud.
I've been tinkering around with my NSPersistentStore's metadata, but that doesn't appear to get synced to iCloud.
Any suggestions?
You could use iCloud's key-value store to store the device name & date of the last sync.
My no doubt unpopular suggestion is "don't". Trying to determine what is in iCloud at any given time puts you on pretty shaky ground. You may be able to get it to work most of the time, but there will always be circumstances where it breaks down.
If you really must import some data when first enabling iCloud, I suggest just always importing the data, and then deduping later as the iCloud data comes in. As ugly as it sounds, that's the only approach really guaranteed to work with Apple's approach.
It is worth taking a look at other Core Data sync frameworks like TICDS and Ensembles. They take a more sane approach to data identity, which means you can avoid the whole deduping step. (Disclosure: I develop Ensembles)
do a metadata query on the iCloud files and check the most recent transaction log file in iCloud. See the link below for a sample app that uses this approach to check whether the app is properly synchronised with iCloud.
http://ossh.com.au/design-and-technology/software-development/sample-library-style-ios-core-data-app-with-icloud-integration/
EDIT:
I just realised I don't get the actual device name, but once you have found the most recent log file then use this to get the device. Just be aware this call may be expensive.
NSFileVersion *openedVersion = [NSFileVersion currentVersionOfItemAtURL:fileURL];
return openedVersion.localizedNameOfSavingComputer;
When core data syncs with iCloud, occasionally all of my records are doubled. I believe this happens on reinstall, where the app has not yet synced data, creates a new data set, and then iCloud syncs and there are double of each record for each model.
Is there a way to prevent this sort of behavior? Right now I'm checking every model on load and data sync for duplication, but this seems messy and hackish.
iCloud transfers data between devices asynchronously, and there can be quite some time between when you add data to the iCloud container, and when it actually gets transferred. Metadata is transferred between devices faster, but even this can be quite delayed.
The reason this is important is that you can never do a test on one device that will guarantee that data has not already been added to iCloud. One device may have seeded data, but not yet begun to upload its files/metadata. At that point, a second device cannot know about the seeded data from the first device.
Apple's advice is to handle this by de-deplicating your data after each merge. Each device just adds its own seed data, and if you find that it has been added twice, you delete half of it, being careful to ensure corresponding objects are deleted on each device.
This approach works, but feels a bit like a hack. An alternative, used in my Ensembles framework, is to supply global identifiers for your objects so that the sync framework can automatically import and merge seed data. That is the approach taken in Ensembles, and it makes seeding data quite a bit easier and less ad hoc.
Trying to use just an iCloud store is folly. Data must be stored both locally and in iCloud, or you will run into serious problems.
Use MagicalRecord if you want core data.
I am creating an app where data needs to be displayed right away from the local datastore. I fire off a background thread when the app starts to determine if iCloud is available.
I looked everywhere and can't find the solution to this: when iCloud becomes available, can I change the "options" on the persistentStore to start using iCloud transactions?
I'm not sure what the proper approach is in this situation. Everything I try causes the application to crash.
Originally I had it so the iCloud checking wasn't in a background thread and the app worked fine, but occasionally timed out.
You have not to know when iCloud becomes available. You just work with data but you don't send them directly to iCloud. iOS does it instead you. So only it knows when and how it should send data.
No, you can't change the options on an NSPersistentStore object once it exists. You can only specify the options when adding the persistent store to the NSPersistentStoreCoordinator. The closest you could get to changing options would be to tear down the entire Core Data stack and start over with different options.
That wouldn't help, though, because:
Even if you have detected that iCloud is available (I'm guessing using NSFileManager, either via its ubiquityIdentityToken or by calling URLForUbiquityContainerIdentifier:), your call to addPersistentStoreWithType:configuration:URL:options:error: might still block for a while. If there's new data available in iCloud, it doesn't start downloading until you add the persistent store, and that method doesn't return until the download process is finished. And sometimes, iCloud just makes that method block for a while for no readily apparent reason.
If you let the user make any changes to the data while using non-iCloud options, those changes will not get automatically sent to the cloud later. Core Data only sends changes to iCloud when the data changes while iCloud is active-- which makes it generate a transaction. You'd have to load and re-save any changes the user made, or those changes would never make it to the cloud.
You have, unfortunately, hit on one of the major stumbling points when using Core Data with iCloud. You can't make the full data store available until Core Data finishes communicating with iCloud-- because your call to add the persistent store doesn't return until then. And you can't do anything to speed up that process. This is just one of the headaches you'll run into if you continue trying to use iCloud with Core Data.
Depending on the nature of your data you might be able to use two data stores, one purely local and one synced via iCloud. You could make the purely local data store available while the iCloud one tries to get its act together well enough to be useful. If you stick with one data store though, you're stuck with the delay.
I have database with some default content. How can I use iCloud to sync changes in database on different devices?
I know how sync devices if CoreData was empty first and I can migrate from external sqlite file to CoreData.
If you have an existing Core Data store and you want to add iCloud support, you'll need to transfer all of your existing data to a new data store and save the result. This is necessary because iCloud only generates transactions when you save changes-- so you need to effectively save changes for everything to jumpstart the syncing process.
In most cases you can do this in a single step by using NSPersistentStoreCoordinator's migratePersistentStore:toURL:options:withType:error:. Pass in the same iCloud options that you would use when calling addPersistentStoreWithType:configuration:URL:options:error:. Use this alternate approach only when moving from a non-iCloud data store to one that does use iCloud.
Also, beware of using iCloud with Core Data. It has not earned a reputation for reliability. Pay close attention to Apple's documentation and sample projects, and even then be prepared for it to just not work sometimes.