WARNING (Jan 26, 2017) . Starting with iOS 10 iCloud Core Data was deprecated. That makes this post pretty useless.
Suppose, you have an app that uses Core Data but doesn’t include iCloud sync, and that app is already in the App Store. Now you want to add iCloud sync to this app. How would you go about it?
If you just add an option NSPersistentStoreUbiquitousContentNameKey to your persistent store, the data that was in that store previously will be lost. You don’t want that.
The way you go about it is you create a new persistent store with option NSPersistentStoreUbiquitousContentNameKey and migrate the data from the old store to the new one. This option is what makes the persistent store support iCloud sync, by the way.
You only need to do this migration once. How do you know when to do it? The best way to find this out it is to use the existence of the old store as an indicator that will tell you if you need to do the migration. At the application start up you check if the old store’s file exists . If it exists, that means the migration hasn’t been performed yet. So, you trigger the migration process. After migration is done you delete the old store.
Enough of theory. Lets see some code. This is an example of Core Data Stack class before we incorporated iCloud:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
import UIKit import CoreData class DataController: NSObject { var managedObjectContext: NSManagedObjectContext init() { // This resource is the same name as your xcdatamodeld contained in your project. guard let modelURL = NSBundle.mainBundle().URLForResource("DataModel", withExtension:"momd") else { fatalError("Error loading model from bundle") } // The managed object model for the application. It is a fatal error for the application not to be able to find and load its model. guard let mom = NSManagedObjectModel(contentsOfURL: modelURL) else { fatalError("Error initializing mom from: \(modelURL)") } let psc = NSPersistentStoreCoordinator(managedObjectModel: mom) managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType) managedObjectContext.persistentStoreCoordinator = psc dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) { let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask) let docURL = urls[urls.endIndex-1] /* The directory the application uses to store the Core Data store file. This code uses a file named "DataModel.sqlite" in the application's documents directory. */ let storeURL = docURL.URLByAppendingPathComponent("DataModel.sqlite") do { try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil) } catch { fatalError("Error migrating store: \(error)") } } } |
I got this piece of code from Apple’s Core Data Programming Guide.
The first thing you do is you use another file for the new persistent store. Let’s call it DataModelWithICloud.sqlite. And you need to add an option NSPersistentStoreUbiquitousContentNameKey to that store. I also added a couple of other useful options. So, you just change the code a little bit starting from line 24 in the previous snippet. Thats what you get (only the changed part is shown):
|
let storeURL = docURL.URLByAppendingPathComponent("DataModelWithICloud.sqlite") do { let options = [NSMigratePersistentStoresAutomaticallyOption : true,NSInferMappingModelAutomaticallyOption: true,NSPersistentStoreUbiquitousContentNameKey:"container-name"] try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: options) } catch { fatalError("Error adding store: \(error)") } |
If you noticed, I also changed “Error migrating store” to “Error adding store”. I don’t know why the word ‘migrating’ was there in the first place, since we a not migrating anything at this point. OK, maybe some migration actually takes place at this point, but it must be a migration from an old version of data model to a new one. Not the migration from an old persistent store to a new one, that we are about to arrange.
But before we go further let’s just make a quick fix to the Apple code. Add this variable declaration to the CoreDataStack:
|
lazy var applicationDocumentsDirectory: NSURL = { let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask) return urls[urls.count-1] as NSURL }() |
We will be using the Documents directory quite often so it makes sense to use this variable instead of putting couple lines of code here and there. Now you can go back to the first snippet and delete lines 19 and 20. Then find this line
|
let storeURL = docURL.URLByAppendingPathComponent("DataModelWithICloud.sqlite") |
and substitute aplicationDocumentsDirectory for docURL. So, you get this:
|
let storeURL = applicationDocumentsDirectory.URLByAppendingPathComponent("DataModelWithICloud.sqlite") |
Now add these methods to CoreDataStack class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
func migrateToICloudIfNeeded(){ let seedStoreURL = self.applicationDocumentsDirectory.URLByAppendingPathComponent("I_Need_to_Know.sqlite") let fileManager = NSFileManager.defaultManager() if fileManager.fileExistsAtPath(seedStoreURL.path!) == true{ _ = self.persistentStoreCoordinator let coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) let failureReason = "There was an error loading the seed store." var seedStore:NSPersistentStore! do { seedStore = try coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: seedStoreURL, options:[NSMigratePersistentStoresAutomaticallyOption:true,NSInferMappingModelAutomaticallyOption:true]) self.migrateToICloudFromStore(seedStore,coordinator: coordinator!) } catch { // Report any error we got. var dict = [String: AnyObject]() dict[NSLocalizedDescriptionKey] = "Failed to initialize the seed store" dict[NSLocalizedFailureReasonErrorKey] = failureReason dict[NSUnderlyingErrorKey] = error as NSError let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict) // Replace this with code to handle the error appropriately. // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)") abort() } }else{ self.coreDataIsReady = true } } func migrateToICloudFromStore(seedStore:NSPersistentStore,coordinator:NSPersistentStoreCoordinator){ let queue = NSOperationQueue() queue.addOperationWithBlock({ () -> Void in do{ let storeURL = self.applicationDocumentsDirectory.URLByAppendingPathComponent("StoreWithICloud.sqlite") try coordinator.migratePersistentStore(seedStore, toURL: storeURL, options: [NSPersistentStoreUbiquitousContentNameKey:"container-name",NSMigratePersistentStoresAutomaticallyOption:true,NSInferMappingModelAutomaticallyOption:true], withType: NSSQLiteStoreType) self.deleteOldStore() self.coreDataIsReady = true }catch{ // Report any error we got. let failureReason = "There was an error migrating to iCloud." var dict = [String: AnyObject]() dict[NSLocalizedDescriptionKey] = "Failed to migrate to iCloud" dict[NSLocalizedFailureReasonErrorKey] = failureReason dict[NSUnderlyingErrorKey] = error as NSError let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict) // Replace this with code to handle the error appropriately. // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)") //self.migrateToICloudFromStore(seedStore, coordinator: coordinator) abort() } }) } func deleteOldStore(){ let seedStoreURL = self.applicationDocumentsDirectory.URLByAppendingPathComponent("DataModel.sqlite") do { try NSFileManager.defaultManager().removeItemAtPath(seedStoreURL.path!) print("old store has been removed") } catch { print("an error during a removing of old store") } } |
Basically, this is it. You should call migrateToICloudIfNeeded() method from your appDelegate’s application:didFinishLaunchingWithOptions method. Now, when the user updates the app, the migration to the new persistent store will take place and the old store will be removed.
PS. There is one more method that may be useful to you. While debugging you might want to delete all data on all devices and even in the iCloud container. So, use this one:
|
func removeICloudContainer(){ let storeURL = self.applicationDocumentsDirectory.URLByAppendingPathComponent("DataModelWithICloud.sqlite") do{ try NSPersistentStoreCoordinator.removeUbiquitousContentAndPersistentStoreAtURL(storeURL, options: [NSPersistentStoreUbiquitousContentNameKey:"container-name"]) print("deleted icloud") }catch{ print("error removing from icloud") } } |
Only make sure not to ship your app with this method.
That’s it. I hope this post was useful to you. If you have any questions leave a comment below.