NetNewsWire/Frameworks/Account/CloudKit/CloudKitErrorHandler.swift
2020-03-22 16:35:03 -05:00

195 lines
7.9 KiB
Swift

//
// CloudKitErrorHandler.swift
// Account
//
// Created by @randycarney on 12/12/17.
// Derived from https://github.com/caiyue1993/IceCream
//
import Foundation
import CloudKit
/// This struct helps you handle all the CKErrors and has been updated to the current Apple documentation(12/15/2017):
/// https://developer.apple.com/documentation/cloudkit/ckerror.code
struct CloudKitErrorHandler {
static let shared = CloudKitErrorHandler()
/// We could classify all the results that CKOperation returns into the following five CKOperationResultTypes
enum CloudKitOperationResultType {
case success
case retry(afterSeconds: Double, message: String)
case chunk
case recoverableError(reason: CloudKitOperationFailReason, message: String)
case fail(reason: CloudKitOperationFailReason, message: String)
}
/// The reason of CloudKit failure could be classified into following 8 cases
enum CloudKitOperationFailReason {
case changeTokenExpired
case network
case quotaExceeded
case partialFailure
case serverRecordChanged
case shareRelated
case unhandledErrorCode
case unknown
}
func resultType(with error: Error?) -> CloudKitOperationResultType {
guard error != nil else { return .success }
guard let e = error as? CKError else {
return .fail(reason: .unknown, message: "The error returned is not a CKError")
}
let message = returnErrorMessage(for: e.code)
switch e.code {
// SHOULD RETRY
case .serviceUnavailable,
.requestRateLimited,
.zoneBusy:
// If there is a retry delay specified in the error, then use that.
let userInfo = e.userInfo
if let retry = userInfo[CKErrorRetryAfterKey] as? Double {
print("ErrorHandler - \(message). Should retry in \(retry) seconds.")
return .retry(afterSeconds: retry, message: message)
} else {
return .fail(reason: .unknown, message: message)
}
// RECOVERABLE ERROR
case .networkUnavailable,
.networkFailure:
print("ErrorHandler.recoverableError: \(message)")
return .recoverableError(reason: .network, message: message)
case .changeTokenExpired:
print("ErrorHandler.recoverableError: \(message)")
return .recoverableError(reason: .changeTokenExpired, message: message)
case .serverRecordChanged:
print("ErrorHandler.recoverableError: \(message)")
return .recoverableError(reason: .serverRecordChanged, message: message)
case .partialFailure:
// Normally it shouldn't happen since if CKOperation `isAtomic` set to true
if let dictionary = e.userInfo[CKPartialErrorsByItemIDKey] as? NSDictionary {
print("ErrorHandler.partialFailure for \(dictionary.count) items; CKPartialErrorsByItemIDKey: \(dictionary)")
}
return .recoverableError(reason: .partialFailure, message: message)
// SHOULD CHUNK IT UP
case .limitExceeded:
print("ErrorHandler.Chunk: \(message)")
return .chunk
// SHARE DATABASE RELATED
case .alreadyShared,
.participantMayNeedVerification,
.referenceViolation,
.tooManyParticipants:
print("ErrorHandler.Fail: \(message)")
return .fail(reason: .shareRelated, message: message)
// quota exceeded is sort of a special case where the user has to take action(like spare more room in iCloud) before retry
case .quotaExceeded:
print("ErrorHandler.Fail: \(message)")
return .fail(reason: .quotaExceeded, message: message)
// FAIL IS THE FINAL, WE REALLY CAN'T DO MORE
default:
print("ErrorHandler.Fail: \(message)")
return .fail(reason: .unknown, message: message)
}
}
func retryOperationIfPossible(retryAfter: Double, block: @escaping () -> ()) {
let delayTime = DispatchTime.now() + retryAfter
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: {
block()
})
}
private func returnErrorMessage(for code: CKError.Code) -> String {
var returnMessage = ""
switch code {
case .alreadyShared:
returnMessage = "Already Shared: a record or share cannot be saved because doing so would cause the same hierarchy of records to exist in multiple shares."
case .assetFileModified:
returnMessage = "Asset File Modified: the content of the specified asset file was modified while being saved."
case .assetFileNotFound:
returnMessage = "Asset File Not Found: the specified asset file is not found."
case .badContainer:
returnMessage = "Bad Container: the specified container is unknown or unauthorized."
case .badDatabase:
returnMessage = "Bad Database: the operation could not be completed on the given database."
case .batchRequestFailed:
returnMessage = "Batch Request Failed: the entire batch was rejected."
case .changeTokenExpired:
returnMessage = "Change Token Expired: the previous server change token is too old."
case .constraintViolation:
returnMessage = "Constraint Violation: the server rejected the request because of a conflict with a unique field."
case .incompatibleVersion:
returnMessage = "Incompatible Version: your app version is older than the oldest version allowed."
case .internalError:
returnMessage = "Internal Error: a nonrecoverable error was encountered by CloudKit."
case .invalidArguments:
returnMessage = "Invalid Arguments: the specified request contains bad information."
case .limitExceeded:
returnMessage = "Limit Exceeded: the request to the server is too large."
case .managedAccountRestricted:
returnMessage = "Managed Account Restricted: the request was rejected due to a managed-account restriction."
case .missingEntitlement:
returnMessage = "Missing Entitlement: the app is missing a required entitlement."
case .networkUnavailable:
returnMessage = "Network Unavailable: the internet connection appears to be offline."
case .networkFailure:
returnMessage = "Network Failure: the internet connection appears to be offline."
case .notAuthenticated:
returnMessage = "Not Authenticated: to use this app, you must enable iCloud syncing. Go to device Settings, sign in to iCloud, then in the app settings, be sure the iCloud feature is enabled."
case .operationCancelled:
returnMessage = "Operation Cancelled: the operation was explicitly canceled."
case .partialFailure:
returnMessage = "Partial Failure: some items failed, but the operation succeeded overall."
case .participantMayNeedVerification:
returnMessage = "Participant May Need Verification: you are not a member of the share."
case .permissionFailure:
returnMessage = "Permission Failure: to use this app, you must enable iCloud syncing. Go to device Settings, sign in to iCloud, then in the app settings, be sure the iCloud feature is enabled."
case .quotaExceeded:
returnMessage = "Quota Exceeded: saving would exceed your current iCloud storage quota."
case .referenceViolation:
returnMessage = "Reference Violation: the target of a record's parent or share reference was not found."
case .requestRateLimited:
returnMessage = "Request Rate Limited: transfers to and from the server are being rate limited at this time."
case .serverRecordChanged:
returnMessage = "Server Record Changed: the record was rejected because the version on the server is different."
case .serverRejectedRequest:
returnMessage = "Server Rejected Request"
case .serverResponseLost:
returnMessage = "Server Response Lost"
case .serviceUnavailable:
returnMessage = "Service Unavailable: Please try again."
case .tooManyParticipants:
returnMessage = "Too Many Participants: a share cannot be saved because too many participants are attached to the share."
case .unknownItem:
returnMessage = "Unknown Item: the specified record does not exist."
case .userDeletedZone:
returnMessage = "User Deleted Zone: the user has deleted this zone from the settings UI."
case .zoneBusy:
returnMessage = "Zone Busy: the server is too busy to handle the zone operation."
case .zoneNotFound:
returnMessage = "Zone Not Found: the specified record zone does not exist on the server."
default:
returnMessage = "Unhandled Error."
}
return returnMessage + "CKError.Code: \(code.rawValue)"
}
}