cloudkit-exchange

With CloudKit Web Services Apple gave access to CloudKit to non Apple devices / applications / services. Access to public CloudKit db as admin user ( read / write permissions ) is available with generated token and does not require any user login.

CloudKit could be used as passive backend that iOS apps interact with. Maintained by backend JVM service that process requests and updates data in streaming or batch manner.

Interesting to me was to develop simple exchange which will give iOS application possibility to write data which needs to be processed by backend service offline and as result of processing other CloudKit “table” should be updated.

First thing was to declare new RecordType, here called Exchange with following fields

type: String
timestamp: Date/Time
marked: Int(64)
data: Asset

type is intended for separation of different kinds of exchange messages, timestamp represents time when message was put in queue, marked should be used to mark processed messages, which later can be removed, data is Asset which will contain actual message. Message could be anything, in my case JSON.

Initialization of CloudKit exchange from iOS application

let container = CKContainer(identifier: containerName)
self.database = container.publicCloudDatabase
self.exchangeRecordType = exchangeRecordType // "Exchange"

Queue message to exchange, maybe queue is to strong word here

func put(
    _ type: String,
    dataUrl: URL,
    _ handler: @escaping (_ status: CloudKitExchangeRequestStatus) -> Void) -> Void {
    
    let exchangeMessage = CKRecord(recordType: self.exchangeRecordType)
    exchangeMessage.setObject(Date() as CKRecordValue?, forKey: "timestamp")
    exchangeMessage.setObject(0 as CKRecordValue?, forKey: "marked")
    exchangeMessage.setObject(type as CKRecordValue?, forKey: "type")
    
    let dataAsset = CKAsset(fileURL: dataUrl)
    exchangeMessage.setObject(dataAsset, forKey: "data")
    
    self.database.save(exchangeMessage) { (record, error) in
        if record != nil && error == nil {
            DispatchQueue.main.async {
                handler(.fail)
            }
        } else {
            DispatchQueue.main.async {
                handler(.fail)
            }
        }
    }
}

With CloudKitExchange in place posting of message is easy

cloudKitExchange.put("locations", dataUrl: FileUtils.getFullPathUrlInDocuments("locations.json")) { (status) in
    if status == .ok {
        self.statusLabel.text = "done"
    } else {
        self.statusLabel.text = "fail"
    }
}

Full code for CloudKitExchange can be found on github

To retrieve message from JVM backend app I was using simple CloudKit client, developed for Clojure ( clj-cloudkit  )

(defn pop [client type]
  (let [records (cloudkit/records-query
                  client
                  *exchange-record-type*
                  (list
                    (cloudkit-filter/equals :marked 0)
                    (cloudkit-filter/equals :type type))
                  (list
                    (cloudkit-sort/descending :timestamp)))]
    (if-let [record (first records)]
      (let [data-url (:downloadURL (:data record))]
        (if-let [data (cloudkit/assets-download
                        client
                        data-url)]
          (do
            (cloudkit/records-modify
              client
              (list
                (cloudkit-operation/update
                  (assoc (select-keys record [:marked]) :marked 1)
                  *exchange-record-type*)))
            data))))))

Full code for clj-client end exchange can be found under github

Advertisements