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.
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.

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

My complete CoreDataStack codes are shown below:

 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
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 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

1
try? persistentContainer.viewContext.setQueryGenerationFrom(.current)