CloudKit Records
CKRecord Basics
Supported field types:
String,NSNumber,Data,Date,CLLocationCKRecord.Reference- links to other recordsCKAsset- binary files (images, audio, documents)- Arrays of any above type (same-type elements only)
Size limits:
| Constraint | Limit |
|---|---|
| Single record (excluding assets) | 1 MB |
| Single asset | 250 MB (native) |
| Batch operations per request | ~400 records |
CKRecord.Reference
// Child points to parent with cascade delete
let parentRef = CKRecord.Reference(recordID: parentRecord.recordID, action: .deleteSelf)
childRecord["parentRef"] = parentRef
Actions:
.deleteSelf- Child deleted when parent deleted.none- Child becomes orphan when parent deleted
CKAsset
let fileURL = getLocalFileURL()
let asset = CKAsset(fileURL: fileURL)
record["attachment"] = asset
Assets stored separately, don't count toward 1MB record limit.
Critical Anti-Patterns
1. Storing Child Arrays in Parent
// BAD: Causes conflict resolution nightmares
let parentRecord = CKRecord(recordType: "Album")
parentRecord["photoIDs"] = photoIDs as CKRecordValue
// GOOD: Child references parent
let photoRecord = CKRecord(recordType: "Photo")
let albumRef = CKRecord.Reference(recordID: albumRecord.recordID, action: .deleteSelf)
photoRecord["album"] = albumRef
2. Ignoring Errors
// BAD
database.save(record) { _, error in
self.updateUI() // Ignores error!
}
// GOOD
database.save(record) { _, error in
if let error = error as? CKError {
switch error.code {
case .serverRecordChanged:
self.resolveConflict(error: error)
case .networkUnavailable, .networkFailure:
if let retry = error.userInfo[CKErrorRetryAfterKey] as? Double {
DispatchQueue.main.asyncAfter(deadline: .now() + retry) {
self.retrySave(record)
}
}
default:
self.handleError(error)
}
return
}
DispatchQueue.main.async { self.updateUI() }
}
3. String Literals for Keys
// BAD: Typos won't be caught
record["titel"] = title
// GOOD: Type-safe keys
enum RecordKeys: String {
case title, createdAt, category
}
record[RecordKeys.title.rawValue] = title
4. Individual Saves Instead of Batch
// BAD: Separate network call for each record
for record in records {
database.save(record) { _, _ in }
}
// GOOD: Single batch operation
let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
operation.modifyRecordsResultBlock = { result in }
database.add(operation)
5. Exceeding Record Size
// BAD: May exceed 1MB limit
record["imageData"] = largeImageData as CKRecordValue
// GOOD: Use CKAsset for binary data
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("temp.jpg")
try imageData.write(to: tempURL)
record["image"] = CKAsset(fileURL: tempURL)
6. UI Updates on Background Thread
// BAD: CloudKit callbacks are on background thread
database.fetch(withRecordID: recordID) { record, error in
self.titleLabel.text = record?["title"] as? String // Crash!
}
// GOOD
database.fetch(withRecordID: recordID) { record, error in
DispatchQueue.main.async {
self.titleLabel.text = record?["title"] as? String
}
}
7. Downloading All Fields
// BAD: Downloads everything including large assets
let query = CKQuery(recordType: "Photo", predicate: predicate)
database.perform(query, inZoneWith: nil) { records, error in }
// GOOD: Only fetch needed fields
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["title", "timestamp"]
database.add(operation)
Error Handling Table
| Error Code | Common Mistake | Correct Handling |
|---|---|---|
partialFailure |
Treat as complete failure | Parse partialErrorsByItemID |
serverRecordChanged |
Retry with client record | Merge using server record from error |
requestRateLimited |
Immediate retry | Use retryAfterSeconds |
limitExceeded |
Fail operation | Split batch and retry |
quotaExceeded |
Silent failure | Alert user |
Review Questions
- Are custom record IDs used that match local storage identifiers?
- Is record data under 1MB with large files as CKAssets?
- Are relationships using back-references (child->parent) not arrays?
- Is
.deleteSelfused appropriately for cascade delete needs? - Are all CloudKit callbacks dispatching UI updates to main thread?
- Is
desiredKeysspecified to avoid downloading unnecessary data?