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