CloudKit Zone 刪除的誤解:為什麼我清一個 zone 就把 985 筆資料弄消失
起因
MeowCare 的 CKShare 陷入 recovery loop,Xcode Console 狂噴:
1
2
3
4
CoreData: fault: Zone metadata is missing it's encoded share data
but is marked for a mutation:
<CKRecordZoneID: zoneName=com.apple.coredata.cloudkit.share.71E08173-55A9-426C-B389-F6D47157E6BB,
ownerName=__defaultOwner__>
encodedShareAsset = nil 但 needsShareUpdate = 1。Mirror delegate 一直想修,修不好,container.share 呼叫全部 hang。
我以為這個 share zone 只是壞掉的 metadata,從 CloudKit Console 手動刪掉應該就能讓 mirror 重新走一遍健康流程。
實際動手:Console → Private Database → Data → Zones → 找到那個 com.apple.coredata.cloudkit.share.71E08173-... → 刪除。
結果 app 回報:
1
2
Private — Event: 0, Session: 1
Shared — Event: 0, Session: 0
985 筆 events 全部消失。
為什麼
這是我對 Core Data + CloudKit 共享機制的誤解。正確的模型:
container.share(_:to:) 是「搬移」不是「複製」
一開始,所有 record 住在 owner 的 private database 的 com.apple.coredata.cloudkit.zone(Core Data 自己建的 default custom zone,以下簡稱「主 zone」)。
呼叫 container.share([session], to: nil) 時,NSPersistentCloudKitContainer 做的事:
- 建立一個新 zone,名字類似
com.apple.coredata.cloudkit.share.<UUID>(以下簡稱「share zone」) - 把傳入的 session record 從主 zone 搬到 share zone
- 把 session relationship 可達的所有 CareEvent record 一起搬到 share zone
- 在 share zone 裡放一個
cloudkit.sharerecord(CKShare 本體) - 把 share URL 回傳給我
重點在「搬」。share zone 建立之後,那 985 筆 events 的實體就住在 share zone,主 zone 裡沒了。這是為什麼 partner 接受 share 後,他的 shared database 能讀到這些 records——他的 shared database 就是在 read owner 的 share zone。
刪 share zone = 刪整個 zone 裡的所有 records
CloudKit 的 zone 刪除語意是 cascade:delete zone = delete every record in that zone。
我從 Console 刪 share zone 後:
- Zone 本身從 CloudKit server 消失
- Zone 裡的所有 record(CareSession + 985 CareEvent + CKShare)一併 server-side 刪除
- Owner 的本地 SQLite 透過 CloudKit import notification 收到「zone deleted」
- Core Data 把對應 record 從本地 store 也刪掉
- UI 看到 985 筆變 0 筆
備份 JSON 救了我。
正確的 recovery 流程(如果再遇到)
Zombie share zone 的症狀就真的只是 metadata 有問題,這時候該做的不是刪 zone,而是:
Option A:在 app 裡 unshare
1
2
3
4
5
6
7
8
9
let privateDB = CKContainer(identifier: "...").privateCloudDatabase
let shares = try await container.fetchShares(in: privateStore)
for share in shares {
// 透過 Core Data 的 API unshare
try await container.purgeObjectsAndRecordsInZone(
with: share.recordID.zoneID,
in: privateStore
)
}
purgeObjectsAndRecordsInZone 是 NSPersistentCloudKitContainer 提供的 API,會正確解除 share 並把 record 搬回主 zone(不是刪 zone)。
Option B:用 CKModifyRecordsOperation 刪 cloudkit.share record
只刪 CKShare record 本身(它住在 share zone 的根部,system recordName 叫 cloudkit.zoneshare 或 share 的固定 ID),保留 zone 跟裡面的資料 record。
1
2
3
4
5
6
let shareID = CKRecord.ID(recordName: "cloudkit.zoneshare", zoneID: shareZoneID)
let op = CKModifyRecordsOperation(
recordsToSave: nil,
recordIDsToDelete: [shareID]
)
privateDB.add(op)
刪完之後,zone 還在、records 還在,但 share 沒了——partner 的 access 失效。然後再重新建新 share。
Option C:換 container identifier
如果真的狀態爛到救不回,最乾淨的就是換 iCloud container(iCloud.your.bundle.v2)。要改:
- Apple Developer portal 加新 container
- App 的 entitlements 加新 container
Config.containerIdentifier跟程式碼更新- App 使用者全部從 0 重來(如果 app 還沒多少使用者就無痛)
為什麼 Console 提供刪 zone 按鈕
Console 的 Zones 頁面本來就是給 CloudKit 管理員用的 power tool,刪 zone 在某些情境下是合理動作(例如真的想放棄整個 zone 的所有資料)。錯的是我以為 share zone 只有 metadata、刪掉不會動到業務資料。
正確的心智模型是:
| Zone | 用途 | 資料在哪 |
|---|---|---|
_defaultZone | CloudKit 系統 zone | 沒用 Core Data 時才會放東西 |
com.apple.coredata.cloudkit.zone | Core Data 主 zone | 沒 share 的所有資料 |
com.apple.coredata.cloudkit.share.<UUID> | 某個 CKShare 的 zone | 該 share 底下所有 record 的實體 |
share zone 不是 metadata-only,它裝著完整的資料子樹。
JSON 備份的價值
MeowCare 一開始就做了全資料 JSON export/import 功能,單純是因為我想要「換 app 時能搬家」。沒想到這次救命了。
如果你的 CloudKit app 沒做 JSON export:
- 做一個吧。Core Data
NSManagedObject可以很輕易包個Codable的 Export 結構,JSONEncoder +.iso8601日期策略,50 行內搞定 - 使用者也喜歡這個功能,因為他們知道資料可以自己帶走
心得
- CloudKit Sharing 的 share zone 不是 metadata-only zone,它是真實資料 zone,刪了就是 cascade delete 所有 record
- Console 的 Zones 頁面操作要非常小心,跟刪 S3 bucket 等級的操作
- Zombie zone metadata 的正確修法是 unshare(
purgeObjectsAndRecordsInZone)或刪 CKShare record,不是刪 zone - JSON export/import 是 CloudKit app 的最後一道防線,做好做滿
- 任何「我以為只是 metadata、應該可以放心清」的動作,在 cloud infra 上都要先假設會 cascade,確認完再下手
Apple 的文件對 share zone 的 lifecycle 描述得不夠清楚,這個坑需要自己踩過才知道。希望這篇可以讓下一個人少賠一次資料。