Integrate your codebase with CloudKit is actually a tricky stuff for me since the first impression the CloudKit gave me several years ago is not good.
In recent days, I had to cope with this bad feeling (Never mind, CloudKit) in the development of Sideloader.
CoreData is not an easy-API framework, as we all know. However, CloudKit defeats CoreData. You can imagine how frustrated I felt when I first use CloudKit to synchronize data stored in local CoreData.
Setup your CoreData with CloudKit
Since Sideloader is an action-extension based app, the CoreData database is saved within the AppGroup shared folder.
1
2
3
4
5
6
7
8
9
10
11
// Function helps you get specific file under appGroup folder.publicextensionURL {
/// Returns a URL for the given app group an/// d database pointing to the sqlite database.staticfuncstoreURL(for appGroup: String, databaseName: String) -> URL {
guardlet fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}
You can use the NSPersistentStoreDescription to tell the CloudKitContainer where the local database is, just like the code below.
1
2
3
4
5
6
let container = NSPersistentCloudKitContainer(name: momdName, managedObjectModel: managedObjectModel)
let storeURL = URL.storeURL(for: Constants.groupContainerName, databaseName: "Sideloader")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.app.chen.ios.Sideloader")
container.persistentStoreDescriptions = [storeDescription]
container.viewContext.automaticallyMergesChangesFromParent = true
Migration from your local store to Cloud-based store
publicfinalclassPersistentManager {
privateinit() {
// Register the Transformer used in Custom CoreData Models YoutubeVideoMetaInfoTransformer.register()
}
publicstaticlet shared = PersistentManager()
// MARK: - Core Data Saving supportpubliclet momdName: String = "Sideloader"publiclazyvar managedObjectModel: NSManagedObjectModel = {
// Resource generated by compiler will be "Sideloader.momd"let bundle = Bundle(for: PersistentManager.self)
guardlet modelURL = bundle.url(forResource: momdName, withExtension: "momd") else {
fatalError("Error loading model from bundle")
}
guardlet mom = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
return mom
}()
lazyvar persistentContainer: NSPersistentCloudKitContainer = {
// NSPersistenCloudKitContainer is a subclass of NSPersistentContainer, adding the CloudKit capability.let container = NSPersistentCloudKitContainer(name: momdName, managedObjectModel: managedObjectModel)
let storeURL = URL.storeURL(for: Constants.groupContainerName, databaseName: "Sideloader")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.app.chen.ios.Sideloader")
container.persistentStoreDescriptions = [storeDescription]
container.viewContext.automaticallyMergesChangesFromParent = true container.loadPersistentStores(completionHandler: { (storeDescription, error) iniflet error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.// fatalError() 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./*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/ fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazyvar managedObjectContext: NSManagedObjectContext = {
return persistentContainer.viewContext
}()
// MARK: - Core Data Saving supportfuncsaveContext () {
let context = managedObjectContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.// fatalError() 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.let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
Handle Synchronization
CloudKit can do all the synchronizations silently in the background. If you are debugging with Xcode, you can see lots of CloudKit-related logs in the console view. There you can see every model we create using CoreData will be saved in iCloud as CKRecord.
Though CloudKit can do almost everything for you automatically, a problem exists which you should take care of.
Think about a scene our users definitely would encounter. They are using your app on their iPhones, while CloudKit synchronize changes to your local store in the background, say a piece of data on the list has been deleted. When the bad-luck user tap the cell with this piece of data, what would happen? CloudKit documentation tells as below:
Isolate the Current View from Store Changes
Yes, we do not have to show the users every change CloudKit fetch. Keep current data view from the remote one is really necessary. You should pin a context