Since our model objects will be backed by a CKRecord, we will leverage computed properties to marshal values back and forth to the record. Doing so in a type safe way gets pretty redundant, so we can reuse a lot of this boilerplate code by extracting a protocol we’ll call CKRecordWrapper. We can leverage this protocol to give us type-safe access to record keys and to provide default implementations of identifier, modifiedAt, and createdAt fields.
Episode Links Episode Source Code Extracting a Protocol We can extract our common structure into a protocol: protocol CKRecordWrapper { static var RecordType: String { get } var record: CKRecord { get } associatedtype FieldKey : RawRepresentable var identifier: String? { get } var createdAt: Date? { get } var modifiedAt: Date? { get } init(record: CKRecord) } This ensures that conforming types have a backing record, have a way to define strongly typed field keys (that have a rawValue) and that can initialize from an existing CKRecord. Providing Helpful Default Implementations We can go one step further and provide helpful default implementations for many of the protocol's requirements. For instance, the metadata properties can simply come from the backing record: extension CKRecordWrapper { var identifier: String? { return record.recordID.recordName } var createdAt: Date? { return record.creationDate } var modifiedAt: Date? { return record.modificationDate } } Strongly Typed Reader/Writer methods We can further extend the protocol in the specific case where the FieldKey type has a string raw value. To express this, we can add a where clause to the extension: extension CKRecordWrapper where FieldKey.RawValue == String { // ... } Then we can move our helper functions into the extension: func getField<T>(_ key: FieldKey) -> T? { return record[key.rawValue] as? T } func setField<T>(_ key: FieldKey, value: T?) { return record[key.rawValue] = value as? CKRecordValue } func getReference(_ key: FieldKey) -> String? { let ref: CKReference? = getField(key) return ref?.recordID.recordName } func setReference(_ key: FieldKey, referenceIdentifier: String?) { let ref = referenceIdentifier.flatMap { (id: String) -> CKReference in let rid = CKRecordID(recordName: id) return CKReference(recordID: rid, action: .deleteSelf) } setField(key, value: ref) } As long as our model types provide a string-backed enum, we'll get this implementation for free. Updating CloudKitNote The models get a lot cleaner: class CloudKitNote : Note, CKRecordWrapper { static let RecordType = "Note" enum FieldKey : String { case content case folder } let record: CKRecord // ... other initializers required init(record: CKRecord) { self.record = record } var content: String { get { return getField(.content) ?? "" } set { setField(.content, value: newValue) } } var folderIdentifier: String? { get { return getReference(.folder) } set { setReference(.folder, referenceIdentifier: newValue) } } Updating CloudKitFolder Similarly, the folder type gets cleaner: class CloudKitFolder : Folder, CKRecordWrapper { static let RecordType = "Folder" enum FieldKey : String { case name } let record: CKRecord var name: String { get { return getField(.name) ?? "" } set { setField(.name, value: newValue) } } required init(name: String) { self.record = CKRecord(recordType: CloudKitFolder.RecordType) self.name = name } init(folder: Folder) { self.record = CKRecord(recordType: CloudKitFolder.RecordType) self.name = folder.name } required init(record: CKRecord) { self.record = record } } With this wrapper protocol and extension, we have provided some very helpful default behavior we can leverage when making CloudKit based model types.