Episode #276

CloudKit Notes Manager

Series: Hello CloudKit

11 minutes
Published on June 9, 2017

This video is only available to subscribers. Get access to this video and 585 others.

In this episode we implement a CloudKit version of our NotesManager protocol. Along the way we'll implement a reusable query function and run into a limitation with Swift generics that we will have to work around.

Episode Links

  • Type Variance in Swift - If you were a bit confused by why we couldn't reuse our completion blocks in the CloudKitNotesManager implementation, this article explains it pretty well. It's a complex topic, but the reasons make sense. Essentially Swift lack's covariance with method parameters, so we cannot pass a block that accepts a type B in place of an argument that uses type A, even if B : A.

Creating the CloudKitNotesManager

We want our notes manager to work on any database and zone we pass in, so we declare an initializer and some readonly properties:

class CloudKitNotesManager : NotesManager {
    static var sharedInstance: NotesManager = CloudKitNotesManager(database: CKContainer.default().privateCloudDatabase)

    private let database: CKDatabase
    private let zoneID: CKRecordZoneID

    init(database: CKDatabase) {
        self.database = database
        self.zoneID = CKRecordZone.default().zoneID
    }

}

We also declared a quick sharedInstance static property with some sensible defaults.

Creating the Default Folder

Next we want to allow creating the default folder. We'll allow the client to blindly attempt to create a new default folder and we will just swallow and ignore the error if the record was already created on the server.

    func createDefaultFolder(completion: @escaping OperationCompletionBlock<Folder>) {
        let folder = CloudKitFolder.defaultFolder(inZone: zoneID)

        database.save(folder.record) { (record, error) in
            if let e = error as? CKError {
                if e.code == CKError.Code.serverRecordChanged {
                    // silently fail, it already exists...
                    let serverFolder = CloudKitFolder(record: e.serverRecord!)
                    completion(.success(serverFolder))
                } else {
                    completion(.error(e))
                }
            } else if let e = error {
                completion(.error(e))
            } else if let record = record {
                let folder = CloudKitFolder(record: record)
                completion(.success(folder))
            }
        }
    }

Note here that we return the serverRecord instance, which is a common pattern we'll use everywhere. This will ensure that when we save records the returned records will have the updated metadata fields that the server will provide.

Fetching the Folders

We'll start by creating a reusable function to query for records.

private func query<R, T>(predicate: NSPredicate, sortDescriptors: [NSSortDescriptor], conversion: @escaping (R) -> T, completion: @escaping OperationCompletionBlock<[T]>) where R : CKRecordWrapper {
    let query = CKQuery(recordType: R.RecordType, predicate: predicate)
    query.sortDescriptors = sortDescriptors

    let queryOperation = CKQueryOperation(query: query)
    var results: [R] = []
    queryOperation.recordFetchedBlock = { record in
        results.append(R(record: record))
    }
    queryOperation.queryCompletionBlock = { cursor, error in
        // ignore cursor for now

        if let e = error as? CKError, e.code == CKError.Code.unknownItem {
            // we'll let the first save define it, for now just return an empty collection
            completion(.success([]))
        } else if let e = error {
            completion(.error(e))
        } else {
            completion(.success(results.map(conversion)))
        }
    }

    database.add(queryOperation)
}

The conversion block here is to satisfy the compiler because we cannot reuse the completion block with type R for one that uses type T. See the link above for an explanation.

Once this is in place, our query methods become simple:

func fetchFolders(completion: @escaping (Result<[Folder]>) -> Void) {
    let all = NSPredicate(value: true)
    let sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]

    query(predicate: all,
          sortDescriptors: sortDescriptors,
          conversion: { (folder: CloudKitFolder) -> Folder in folder },
          completion: completion)
}

This episode uses Ios 10.3, Xcode 8.3.