On this page
Integrating your codebase with CloudKit is actually a tricky thing for me, since the first impression CloudKit gave me several years ago wasn’t 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 used 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.
// Function helps you get specific file under appGroup folder.
public extension URL {
/// Returns a URL for the given app group an
/// d database pointing to the sqlite database.
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let 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.
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
My complete CoreDataStack code is shown below:
public final class PersistentManager {
private init() {
// Register the Transformer used in Custom CoreData Models
YoutubeVideoMetaInfoTransformer.register()
}
public static let shared = PersistentManager()
// MARK: - Core Data Saving support
public let momdName: String = "Sideloader"
public lazy var managedObjectModel: NSManagedObjectModel = {
// Resource generated by compiler will be "Sideloader.momd"
let bundle = Bundle(for: PersistentManager.self)
guard let modelURL = bundle.url(forResource: momdName, withExtension: "momd") else {
fatalError("Error loading model from bundle")
}
guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
return mom
}()
lazy var 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) in
if let 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
}()
lazy var managedObjectContext: NSManagedObjectContext = {
return persistentContainer.viewContext
}()
// MARK: - Core Data Saving support
func saveContext () {
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 that every model we create using CoreData is saved in iCloud as a CKRecord.
Though CloudKit can do almost everything for you automatically, a problem exists which you should take care of.
Think about a scenario our users would definitely encounter. They’re using your app on their iPhones while CloudKit synchronizes changes to your local store in the background — say a piece of data on the list has been deleted. When the unlucky user taps the cell with this piece of data, what happens? The CloudKit documentation says:
Isolate the Current View from Store Changes
Yes, we don’t have to show the users every change CloudKit fetches. Keeping the current data view isolated from the remote one is necessary. You should pin a context:
try? persistentContainer.viewContext.setQueryGenerationFrom(.current)