What is in a CKRecord, why archive its system fields, and what are the zone rules for references and parents?

17.11.1 · L5 · Interview

Predict first

What survives the system-fields archive?

import Foundation
import CloudKit

let zone = CKRecordZone.ID(zoneName: "Notes", ownerName: CKCurrentUserDefaultName)
let note = CKRecord(recordType: "Note", recordID: CKRecord.ID(recordName: "n1", zoneID: zone))
note["title"] = "Hello" as CKRecordValue
note["tags"] = ["a", "b"] as CKRecordValue
print("keys:", note.allKeys().sorted(), "changed:", note.changedKeys().sorted())
print("before any save: changeTag", note.recordChangeTag as Any, "created", note.creationDate as Any)

let archiver = NSKeyedArchiver(requiringSecureCoding: true)
note.encodeSystemFields(with: archiver); archiver.finishEncoding()
let unarchiver = try! NSKeyedUnarchiver(forReadingFrom: archiver.encodedData)
unarchiver.requiresSecureCoding = true
let restored = CKRecord(coder: unarchiver)!
print("system-fields copy: id", restored.recordID.recordName, "type", restored.recordType, "fields", restored.allKeys())

let folder = CKRecord(recordType: "Folder", recordID: CKRecord.ID(recordName: "f1", zoneID: zone))
note["folder"] = CKRecord.Reference(record: folder, action: .deleteSelf)
note.parent = CKRecord.Reference(record: folder, action: .none)
print("reference action .deleteSelf raw:", CKRecord.Reference.Action.deleteSelf.rawValue, "| parent set:", note.parent != nil)
print("default zone name:", CKRecordZone.default().zoneID.zoneName)
  • Only the id and type survive; fields are empty
  • All fields survive
  • Nothing survives

Predict first

What happens when a parent is in another zone?

import CloudKit

let zoneA = CKRecordZone.ID(zoneName: "Notes", ownerName: CKCurrentUserDefaultName)
let zoneB = CKRecordZone.ID(zoneName: "Elsewhere", ownerName: CKCurrentUserDefaultName)
let note = CKRecord(recordType: "Note", recordID: CKRecord.ID(recordName: "n1", zoneID: zoneA))
let folder = CKRecord(recordType: "Folder", recordID: CKRecord.ID(recordName: "f1", zoneID: zoneB))
note["folder"] = CKRecord.Reference(record: folder, action: .none)
print("an ordinary reference to another zone is accepted")
note.parent = CKRecord.Reference(record: folder, action: .none)
print("unreachable")
  • The process crashes with an exception
  • It works
  • The reference is silently dropped

Mental model. The record’s system fields are the library stamp on a book: your notes in the margins are yours, but the stamp says which edition you borrowed.

A record is a typed key-value dictionary (allKeys() shows the fields, changedKeys() the ones not yet sent) plus system fields managed by the server: record id, change tag, creation and modification dates. A new local record has no change tag or dates (the run prints nil). The recommended pattern is to store encodeSystemFields(with:) next to your own data: in the run the archive restores the id and type but no fields (fields []), and that stub, with your fields reapplied, lets a later save be an update with a change tag instead of a conflicting insert. References have an action (.deleteSelf means deleting the target deletes this record), and the default zone name is _defaultZone, which cannot be shared or use change tokens, so use a custom zone for synced data.

Zones matter for hierarchy: setting parent to a record in another zone raises NSInternalInconsistencyException (“Parent record must be in the same zone”), while an ordinary reference field to another zone is accepted at the call site. Parent hierarchies are what sharing (CKShare) follows, so design the zone and parent tree up front. Only the offline record API ran; no container, network or saves were involved.

Archived system fields restore the server identity, not your data.

Parent and child must share a zone.

Common traps

  • Saving a rebuilt record without system fields. It conflicts with the server copy.
  • Using the default zone for sync. Custom zones give change tokens and sharing.

They might follow up with

  • Why a parent? A share of the parent shares its children in the same zone.

Related questions

  • 17.4.1 How does CloudKit work: containers, databases, zones, records, subscriptions, CKShare and sync?
  • 17.11.2 How should a sync layer react to CKError codes: partial failures, retry-after, conflicts and expired tokens?

How should a sync layer react to CKError codes: partial failures… →