iOS - CloudKit 错误处理

2019-05-19

列举一些常见的错误,以及处理方式。

1.致命错误,基本上无能为力

public enum CKErrorCode : Int {
    case internalError     		// 内容错误
    case serverRejectedRequest 	// 服务器拒绝
    case invalidArguments   	// 非法参数
    case permissionFailure  	// 权限失败
}

2.服务器让你过一会儿再重试

public enum CKErrorCode : Int {
    case zoneBusy     
    case serviceUnavailable 
    case requestRateLimited   
}

userInfo 中通过 CKErrorRetryAfterKey 获取需要等待的时间,并重新初始化一个相同的 CKOperation 并重试

一个例子

var error = ... // Error from the previous CKOperation
if let retryAfter = error.userInfo[CKErrorRetryAfterKey] as? Double {
    let delayTime = DispatchTime.now() + retryAfter
    DispatchQueue.main.after(when: delayTime) {
        // Initialize CKOperation for a retry
	} 
}

3.网络错误

public enum CKErrorCode : Int {
    case networkUnavailable   // 当前设备网络不能使用,通过监听网络状态之后重试
    case networkFailure		  // 网络可以使用但是连接失败,稍后重试
}

4.CKOperation 中出现部分错误

public enum CKErrorCode : Int {
    case partialFailure   
    case batchRequestFailed		  
}

因为一个 CKOperation 可以批量操作数据并且具有 原子性,其中一条操作失败会造成整个 operation 的失败。比如使用 CKModifyRecordsOperation 修改 RecordA、RecordB、RecordC。但是其中 RecordA 失败了,RecordB、RecordC 成功了那么就会报 partialFailure。在 operation.perRecordCompletionBlock 回调中RecordA 返回错误实际的错误,RecordB、RecordC 返回 batchRequestFailed

对于 partialFailure 可以通过 CKPartialErrorsByItemIDuserInfo 中找到具体哪条操作报错

一个例子

let recordsToSave = [RecordA, RecordB, RecordC]

let operation = CKModifyRecordsOperation(recordsToSave:recordsToSave, recordIDsToDelete: nil)
operation.perRecordCompletionBlock = { (record, error) in
    if let error = error as? CKError {
        // 这里的 error 分别为
        // RecordA -> serverRecordChanged
        // RecordB -> batchRequestFailed
        // RecordC -> batchRequestFailed
        print(error)
    }
}
operation.modifyRecordsCompletionBlock = { (records, recordID, error) in
    if let error = error as? CKError {
        // error 为 partialFailure
        print(error)
        // 这里 errorsByItemID 为 [RecordA.recordID: serverRecordChanged]
        let errorsByItemID = error.userInfo[CKPartialErrorsByItemID] as? [CKRecord.ID: CKError]
    }
}
self.privateDB.add(operation)

5.未登陆 iCloud

public enum CKErrorCode : Int {
    case notAuthenticated	  
}

如果用户未登陆 iCloud 但是又要使用 CloudKit 会报错,比较好的是在每次应用启动时检查 iCloud 登陆状态

6.用户在设置中手动删除 iCloud 中数据

public enum CKErrorCode : Int {
    case userDeletedZone	  
}

这个时候需要重新创建 zone 和 subscriptions

7.本地数据与服务器数据冲突

public enum CKErrorCode : Int {
    case serverRecordChanged	  
}

当有多个设备使用更新同一块数据的时候,本地数据及其容易与服务器数据冲突。比如设备 A 和 设备B 都缓存有相同的 RecordA-1,但是设备 A 在某个时候修改成了 RecordA-2 并同步到 iCloud。之后设备 B 也要修改 RecordA-1,设备B 将其修改为 RecordA-3,这个时候 设备B 同步到 iCloud 时就会报错。

对于错误 serverRecordChanged 可以通过以下 key 在 userInfo 中找到相应的数据

Key 备注
CKRecordChangedErrorClientRecordKey 获取本地尝试同步到服务器的 Record,是修改后的,即以上 RecordA-3
CKRecordChangedErrorServerRecordKey 获取服务器上已经存在的 Record,即以上 RecordA-2
CKRecordChangedErrorAncestorRecordKey 本地初始的数据,未进行任何修改的,即以上 RecordA-1

这个时候我们有两种解决方法

  1. 从服务器 fetch 最新的 Record,更新,并保存
  2. 修改 operation 的保存策略,operation 的 savePolicy 共有 3 种
Policy 备注
ifServerRecordUnchanged 如果服务器数据有修改就报错
changedKeys 本地数据覆盖服务器的数据,但是只修改有改动的 key
allKeys 本地数据覆盖服务器的数据,所有的 key 都同步为本地数据