jazzedge
11/29/2017 - 8:23 PM

Swift - CloudKit - Saving Records

Saving records is, perhaps, the most complicated operation.

See: https://www.whatmatrix.com/blog/a-guide-to-cloudkit-how-to-sync-user-data-across-ios-devices/

Saving records is, perhaps, the most complicated operation. The simple act of writing a record to the database is straightforward enough, but in my example, with multiple clients, this is where you’ll face the potential issue of handling a conflict when multiple clients attempt to write to the server concurrently. Thankfully, CloudKit is explicitly designed to handle this condition. It rejects specific requests with enough error context in the response to allow each client to make a local, enlightened decision about how to resolve the conflict.

Although this adds complexity to the client, it’s ultimately a far better solution than having Apple come up with one of a few server-side mechanisms for conflict resolution.

The app designer is always in the best position to define rules for these situations, which can include everything from context-aware automatic merging to user-directed resolution instructions. I am not going to get very fancy in my example; I am using the modified field to declare that the most recent update wins. This might not always be the best outcome for professional apps, but it’s not bad for a first rule and, for this purpose, serves to illustrate the mechanism by which CloudKit passes conflict information back to the client

Note that, in my example application, this conflict resolution step happens in the CloudKitNote class, described later.

// Save a record to the iCloud database
public func saveRecord(record: CKRecord, completion: @escaping (Error?) -> Void) {
	let operation = CKModifyRecordsOperation(recordsToSave: [record], recordIDsToDelete: [])
	operation.modifyRecordsCompletionBlock = { _, _, error in
		guard error == nil else {
			guard let ckerror = error as? CKError else {
				completion(error)
				return
			}
			guard ckerror.isZoneNotFound() else {
				completion(error)
				return
			}
			// ZoneNotFound is the one error we can reasonably expect & handle here, since
			// the zone isn't created automatically for us until we've saved one record.
			// create the zone and, if successful, try again
			self.createZone() { error in
				guard error == nil else {
					completion(error)
					return
				}
				self.saveRecord(record: record, completion: completion)
			}
			return
		}

		// Lazy save the subscription upon first record write
		// (saveSubscription is internally defensive against trying to save it more than once)
		self.saveSubscription()
		completion(nil)
	}
	operation.qualityOfService = .utility

	let container = CKContainer.default()
	let db = container.privateCloudDatabase
	db.add(operation)
}