Episode #275

Extracting CKRecordWrapper

Series: Hello CloudKit

7 minutes
Published on June 8, 2017

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

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

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.

This episode uses Ios 10.3, Xcode 8.3.