mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2024-12-24 00:30:51 +01:00
More infrastructure work on CloudKit.
This commit is contained in:
parent
53b00c5414
commit
f288e3d5d8
@ -56,6 +56,8 @@
|
||||
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; };
|
||||
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; };
|
||||
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */; };
|
||||
51C034DF242D65D20014DC71 /* CloudKitResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C034DE242D65D20014DC71 /* CloudKitResult.swift */; };
|
||||
51C034E1242D660D0014DC71 /* CKError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C034E0242D660D0014DC71 /* CKError+Extensions.swift */; };
|
||||
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
|
||||
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
|
||||
51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; };
|
||||
@ -64,7 +66,6 @@
|
||||
51E148EC234B8FFC0004F7A5 /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E148EB234B8FFC0004F7A5 /* SyncDatabase.framework */; };
|
||||
51E3EB41229AF61B00645299 /* AccountError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB40229AF61B00645299 /* AccountError.swift */; };
|
||||
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E490352288C37100C791F0 /* FeedbinDate.swift */; };
|
||||
51E4DB2C242632DC0091EB5B /* CloudKitErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4DB2B242632DC0091EB5B /* CloudKitErrorHandler.swift */; };
|
||||
51E4DB2E242633ED0091EB5B /* CloudKitZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */; };
|
||||
51E4DB302426353D0091EB5B /* CloudKitAccountZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */; };
|
||||
51E4DB3224264B470091EB5B /* WebFeed+CloudKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4DB3124264B470091EB5B /* WebFeed+CloudKit.swift */; };
|
||||
@ -289,6 +290,8 @@
|
||||
51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = "<group>"; };
|
||||
51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
|
||||
51BFDECD238B508D00216323 /* ContainerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerIdentifier.swift; sourceTree = "<group>"; };
|
||||
51C034DE242D65D20014DC71 /* CloudKitResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitResult.swift; sourceTree = "<group>"; };
|
||||
51C034E0242D660D0014DC71 /* CKError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKError+Extensions.swift"; sourceTree = "<group>"; };
|
||||
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
|
||||
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
|
||||
51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; };
|
||||
@ -297,7 +300,6 @@
|
||||
51E148EB234B8FFC0004F7A5 /* SyncDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SyncDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
51E3EB40229AF61B00645299 /* AccountError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountError.swift; sourceTree = "<group>"; };
|
||||
51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = "<group>"; };
|
||||
51E4DB2B242632DC0091EB5B /* CloudKitErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitErrorHandler.swift; sourceTree = "<group>"; };
|
||||
51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitZone.swift; sourceTree = "<group>"; };
|
||||
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountZone.swift; sourceTree = "<group>"; };
|
||||
51E4DB3124264B470091EB5B /* WebFeed+CloudKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebFeed+CloudKit.swift"; sourceTree = "<group>"; };
|
||||
@ -515,10 +517,11 @@
|
||||
5103A9D7242253DC00410853 /* CloudKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
51C034E0242D660D0014DC71 /* CKError+Extensions.swift */,
|
||||
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */,
|
||||
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */,
|
||||
51E4DB2B242632DC0091EB5B /* CloudKitErrorHandler.swift */,
|
||||
51E4DB352426693F0091EB5B /* CloudKitRecordConvertable.swift */,
|
||||
51C034DE242D65D20014DC71 /* CloudKitResult.swift */,
|
||||
51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */,
|
||||
51E4DB3324264CD50091EB5B /* Folder+CloudKit.swift */,
|
||||
51E4DB3124264B470091EB5B /* WebFeed+CloudKit.swift */,
|
||||
@ -1171,6 +1174,7 @@
|
||||
9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */,
|
||||
3B3A33E7238D3D6800314204 /* Secrets.swift in Sources */,
|
||||
9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */,
|
||||
51C034E1242D660D0014DC71 /* CKError+Extensions.swift in Sources */,
|
||||
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
|
||||
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,
|
||||
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */,
|
||||
@ -1185,7 +1189,6 @@
|
||||
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */,
|
||||
9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */,
|
||||
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
|
||||
51E4DB2C242632DC0091EB5B /* CloudKitErrorHandler.swift in Sources */,
|
||||
9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */,
|
||||
84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */,
|
||||
9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */,
|
||||
@ -1193,6 +1196,7 @@
|
||||
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */,
|
||||
769F295938E5A30D03DFF88F /* NewsBlurAccountDelegate.swift in Sources */,
|
||||
769F2BA02EF5F329CDE45F5A /* NewsBlurAPICaller.swift in Sources */,
|
||||
51C034DF242D65D20014DC71 /* CloudKitResult.swift in Sources */,
|
||||
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */,
|
||||
179DBF4DE2562D4C532F6008 /* NewsBlurFeed.swift in Sources */,
|
||||
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */,
|
||||
|
87
Frameworks/Account/CloudKit/CKError+Extensions.swift
Normal file
87
Frameworks/Account/CloudKit/CKError+Extensions.swift
Normal file
@ -0,0 +1,87 @@
|
||||
//
|
||||
// CKError+Extensions.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 3/26/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CloudKit
|
||||
|
||||
extension CKError: LocalizedError {
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch code {
|
||||
case .alreadyShared:
|
||||
return NSLocalizedString("Already Shared: a record or share cannot be saved because doing so would cause the same hierarchy of records to exist in multiple shares.", comment: "Known iCloud Error")
|
||||
case .assetFileModified:
|
||||
return NSLocalizedString("Asset File Modified: the content of the specified asset file was modified while being saved.", comment: "Known iCloud Error")
|
||||
case .assetFileNotFound:
|
||||
return NSLocalizedString("Asset File Not Found: the specified asset file is not found.", comment: "Known iCloud Error")
|
||||
case .badContainer:
|
||||
return NSLocalizedString("Bad Container: the specified container is unknown or unauthorized.", comment: "Known iCloud Error")
|
||||
case .badDatabase:
|
||||
return NSLocalizedString("Bad Database: the operation could not be completed on the given database.", comment: "Known iCloud Error")
|
||||
case .batchRequestFailed:
|
||||
return NSLocalizedString("Batch Request Failed: the entire batch was rejected.", comment: "Known iCloud Error")
|
||||
case .changeTokenExpired:
|
||||
return NSLocalizedString("Change Token Expired: the previous server change token is too old.", comment: "Known iCloud Error")
|
||||
case .constraintViolation:
|
||||
return NSLocalizedString("Constraint Violation: the server rejected the request because of a conflict with a unique field.", comment: "Known iCloud Error")
|
||||
case .incompatibleVersion:
|
||||
return NSLocalizedString("Incompatible Version: your app version is older than the oldest version allowed.", comment: "Known iCloud Error")
|
||||
case .internalError:
|
||||
return NSLocalizedString("Internal Error: a nonrecoverable error was encountered by CloudKit.", comment: "Known iCloud Error")
|
||||
case .invalidArguments:
|
||||
return NSLocalizedString("Invalid Arguments: the specified request contains bad information.", comment: "Known iCloud Error")
|
||||
case .limitExceeded:
|
||||
return NSLocalizedString("Limit Exceeded: the request to the server is too large.", comment: "Known iCloud Error")
|
||||
case .managedAccountRestricted:
|
||||
return NSLocalizedString("Managed Account Restricted: the request was rejected due to a managed-account restriction.", comment: "Known iCloud Error")
|
||||
case .missingEntitlement:
|
||||
return NSLocalizedString("Missing Entitlement: the app is missing a required entitlement.", comment: "Known iCloud Error")
|
||||
case .networkUnavailable:
|
||||
return NSLocalizedString("Network Unavailable: the internet connection appears to be offline.", comment: "Known iCloud Error")
|
||||
case .networkFailure:
|
||||
return NSLocalizedString("Network Failure: the internet connection appears to be offline.", comment: "Known iCloud Error")
|
||||
case .notAuthenticated:
|
||||
return NSLocalizedString("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.", comment: "Known iCloud Error")
|
||||
case .operationCancelled:
|
||||
return NSLocalizedString("Operation Cancelled: the operation was explicitly canceled.", comment: "Known iCloud Error")
|
||||
case .partialFailure:
|
||||
return NSLocalizedString("Partial Failure: some items failed, but the operation succeeded overall.", comment: "Known iCloud Error")
|
||||
case .participantMayNeedVerification:
|
||||
return NSLocalizedString("Participant May Need Verification: you are not a member of the share.", comment: "Known iCloud Error")
|
||||
case .permissionFailure:
|
||||
return NSLocalizedString("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.", comment: "Known iCloud Error")
|
||||
case .quotaExceeded:
|
||||
return NSLocalizedString("Quota Exceeded: saving would exceed your current iCloud storage quota.", comment: "Known iCloud Error")
|
||||
case .referenceViolation:
|
||||
return NSLocalizedString("Reference Violation: the target of a record's parent or share reference was not found.", comment: "Known iCloud Error")
|
||||
case .requestRateLimited:
|
||||
return NSLocalizedString("Request Rate Limited: transfers to and from the server are being rate limited at this time.", comment: "Known iCloud Error")
|
||||
case .serverRecordChanged:
|
||||
return NSLocalizedString("Server Record Changed: the record was rejected because the version on the server is different.", comment: "Known iCloud Error")
|
||||
case .serverRejectedRequest:
|
||||
return NSLocalizedString("Server Rejected Request", comment: "Known iCloud Error")
|
||||
case .serverResponseLost:
|
||||
return NSLocalizedString("Server Response Lost", comment: "Known iCloud Error")
|
||||
case .serviceUnavailable:
|
||||
return NSLocalizedString("Service Unavailable: Please try again.", comment: "Known iCloud Error")
|
||||
case .tooManyParticipants:
|
||||
return NSLocalizedString("Too Many Participants: a share cannot be saved because too many participants are attached to the share.", comment: "Known iCloud Error")
|
||||
case .unknownItem:
|
||||
return NSLocalizedString("Unknown Item: the specified record does not exist.", comment: "Known iCloud Error")
|
||||
case .userDeletedZone:
|
||||
return NSLocalizedString("User Deleted Zone: the user has deleted this zone from the settings UI.", comment: "Known iCloud Error")
|
||||
case .zoneBusy:
|
||||
return NSLocalizedString("Zone Busy: the server is too busy to handle the zone operation.", comment: "Known iCloud Error")
|
||||
case .zoneNotFound:
|
||||
return NSLocalizedString("Zone Not Found: the specified record zone does not exist on the server.", comment: "Known iCloud Error")
|
||||
default:
|
||||
return NSLocalizedString("Unhandled Error.", comment: "Unknown iCloud Error")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -23,46 +23,17 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
self.database = container.privateCloudDatabase
|
||||
}
|
||||
|
||||
|
||||
/// Persist a feed record to iCloud and return the external key
|
||||
func createFeed(url: String, editedName: String?, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let record = CKRecord(recordType: "Feed", recordID: generateRecordID())
|
||||
record["url"] = url
|
||||
if let editedName = editedName {
|
||||
record["editedName"] = editedName
|
||||
}
|
||||
|
||||
save(record: record, completion: completion)
|
||||
}
|
||||
|
||||
// func fetchChangesInDatabase(_ callback: ((Error?) -> Void)?) {
|
||||
// let changesOperation = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken)
|
||||
//
|
||||
// /// Only update the changeToken when fetch process completes
|
||||
// changesOperation.changeTokenUpdatedBlock = { [weak self] newToken in
|
||||
// self?.databaseChangeToken = newToken
|
||||
// }
|
||||
//
|
||||
// changesOperation.fetchDatabaseChangesCompletionBlock = {
|
||||
// [weak self]
|
||||
// newToken, _, error in
|
||||
// guard let self = self else { return }
|
||||
// switch CloudKitErrorHandler.shared.resultType(with: error) {
|
||||
// case .success:
|
||||
// self.databaseChangeToken = newToken
|
||||
// // Fetch the changes in zone level
|
||||
// self.fetchChangesInZones(callback)
|
||||
// case .retry(let timeToWait, _):
|
||||
// CloudKitErrorHandler.shared.retryOperationIfPossible(retryAfter: timeToWait, block: {
|
||||
// self.fetchChangesInDatabase(callback)
|
||||
// })
|
||||
// case .recoverableError(let reason, _):
|
||||
// switch reason {
|
||||
// case .changeTokenExpired:
|
||||
// /// The previousServerChangeToken value is too old and the client must re-sync from scratch
|
||||
// self.databaseChangeToken = nil
|
||||
// self.fetchChangesInDatabase(callback)
|
||||
// default:
|
||||
// return
|
||||
// }
|
||||
// default:
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// database.add(changesOperation)
|
||||
// }
|
||||
|
||||
// private func fetchChangesInZones(_ callback: ((Error?) -> Void)? = nil) {
|
||||
// let changesOp = CKFetchRecordZoneChangesOperation(recordZoneIDs: zoneIds, optionsByRecordZoneID: zoneIdOptions)
|
||||
// changesOp.fetchAllChanges = true
|
||||
|
@ -1,194 +0,0 @@
|
||||
//
|
||||
// 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)"
|
||||
}
|
||||
|
||||
}
|
53
Frameworks/Account/CloudKit/CloudKitResult.swift
Normal file
53
Frameworks/Account/CloudKit/CloudKitResult.swift
Normal file
@ -0,0 +1,53 @@
|
||||
//
|
||||
// CloudKitResult.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 3/26/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CloudKit
|
||||
|
||||
enum CloudKitResult {
|
||||
case success
|
||||
case retry(afterSeconds: Double)
|
||||
case chunk
|
||||
case changeTokenExpired
|
||||
case partialFailure
|
||||
case serverRecordChanged
|
||||
case noZone
|
||||
case failure(error: Error)
|
||||
|
||||
static func resolve(_ error: Error?) -> CloudKitResult {
|
||||
|
||||
guard error != nil else { return .success }
|
||||
|
||||
guard let ckError = error as? CKError else {
|
||||
return .failure(error: error!)
|
||||
}
|
||||
|
||||
switch ckError.code {
|
||||
case .serviceUnavailable, .requestRateLimited, .zoneBusy:
|
||||
if let retry = ckError.userInfo[CKErrorRetryAfterKey] as? Double {
|
||||
return .retry(afterSeconds: retry)
|
||||
} else {
|
||||
return .failure(error: error!)
|
||||
}
|
||||
case .changeTokenExpired:
|
||||
return .changeTokenExpired
|
||||
case .serverRecordChanged:
|
||||
return .serverRecordChanged
|
||||
case .partialFailure:
|
||||
return .partialFailure
|
||||
case .limitExceeded:
|
||||
return .chunk
|
||||
case .zoneNotFound, .userDeletedZone:
|
||||
return .noZone
|
||||
default:
|
||||
return .failure(error: error!)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -8,13 +8,16 @@
|
||||
|
||||
import CloudKit
|
||||
|
||||
public enum CloudKitZoneError: Error {
|
||||
case unknown
|
||||
}
|
||||
|
||||
public protocol CloudKitZone: class {
|
||||
|
||||
static var zoneID: CKRecordZone.ID { get }
|
||||
|
||||
var container: CKContainer { get }
|
||||
var database: CKDatabase { get }
|
||||
static var zoneID: CKRecordZone.ID { get }
|
||||
|
||||
func startUp(completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
// func prepare()
|
||||
|
||||
@ -33,12 +36,44 @@ public protocol CloudKitZone: class {
|
||||
|
||||
extension CloudKitZone {
|
||||
|
||||
func startUp(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
var changeTokenKey: String {
|
||||
return "cloudkit.server.token.\(Self.zoneID.zoneName)"
|
||||
}
|
||||
|
||||
var changeToken: CKServerChangeToken? {
|
||||
get {
|
||||
guard let tokenData = UserDefaults.standard.object(forKey: changeTokenKey) as? Data else { return nil }
|
||||
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: CKServerChangeToken.self, from: tokenData)
|
||||
}
|
||||
set {
|
||||
guard let token = newValue, let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: false) else {
|
||||
UserDefaults.standard.removeObject(forKey: changeTokenKey)
|
||||
return
|
||||
}
|
||||
UserDefaults.standard.set(data, forKey: changeTokenKey)
|
||||
}
|
||||
}
|
||||
|
||||
var zoneConfiguration: CKFetchRecordZoneChangesOperation.ZoneConfiguration {
|
||||
let config = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
|
||||
config.previousServerChangeToken = changeToken
|
||||
return config
|
||||
}
|
||||
|
||||
func generateRecordID() -> CKRecord.ID {
|
||||
return CKRecord.ID(recordName: UUID().uuidString, zoneID: Self.zoneID)
|
||||
}
|
||||
|
||||
func createZoneRecord(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
database.save(CKRecordZone(zoneID: Self.zoneID)) { (recordZone, error) in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
} else {
|
||||
completion(.success(()))
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,37 +113,62 @@ extension CloudKitZone {
|
||||
// })
|
||||
// }
|
||||
|
||||
public func save(record: CKRecord, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
database.save(record) {(savedRecord, error) in
|
||||
|
||||
switch CloudKitResult.resolve(error) {
|
||||
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
if let savedRecord = savedRecord {
|
||||
completion(.success(savedRecord.recordID.recordName))
|
||||
} else {
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
}
|
||||
}
|
||||
|
||||
case .retry(let timeToWait):
|
||||
self.retryOperationIfPossible(retryAfter: timeToWait) {
|
||||
self.save(record: record, completion: completion)
|
||||
}
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Sync local data to CloudKit
|
||||
/// For more about the savePolicy: https://developer.apple.com/documentation/cloudkit/ckrecordsavepolicy
|
||||
public func syncRecordsToCloudKit(recordsToStore: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: ((Error?) -> ())? = nil) {
|
||||
let modifyOpe = CKModifyRecordsOperation(recordsToSave: recordsToStore, recordIDsToDelete: recordIDsToDelete)
|
||||
public func syncRecordsToCloudKit(recordsToStore: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let op = CKModifyRecordsOperation(recordsToSave: recordsToStore, recordIDsToDelete: recordIDsToDelete)
|
||||
|
||||
let config = CKOperation.Configuration()
|
||||
config.isLongLived = true
|
||||
modifyOpe.configuration = config
|
||||
op.configuration = config
|
||||
|
||||
// We use .changedKeys savePolicy to do unlocked changes here cause my app is contentious and off-line first
|
||||
// Apple suggests using .ifServerRecordUnchanged save policy
|
||||
// For more, see Advanced CloudKit(https://developer.apple.com/videos/play/wwdc2014/231/)
|
||||
modifyOpe.savePolicy = .changedKeys
|
||||
op.savePolicy = .changedKeys
|
||||
|
||||
// To avoid CKError.partialFailure, make the operation atomic (if one record fails to get modified, they all fail)
|
||||
// If you want to handle partial failures, set .isAtomic to false and implement CKOperationResultType .fail(reason: .partialFailure) where appropriate
|
||||
modifyOpe.isAtomic = true
|
||||
op.isAtomic = true
|
||||
|
||||
modifyOpe.modifyRecordsCompletionBlock = {
|
||||
[weak self]
|
||||
(_, _, error) in
|
||||
op.modifyRecordsCompletionBlock = { [weak self] (_, _, error) in
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
switch CloudKitErrorHandler.shared.resultType(with: error) {
|
||||
switch CloudKitResult.resolve(error) {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
completion?(nil)
|
||||
completion(.success(()))
|
||||
}
|
||||
case .retry(let timeToWait, _):
|
||||
CloudKitErrorHandler.shared.retryOperationIfPossible(retryAfter: timeToWait) {
|
||||
case .retry(let timeToWait):
|
||||
self.retryOperationIfPossible(retryAfter: timeToWait) {
|
||||
self.syncRecordsToCloudKit(recordsToStore: recordsToStore, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
||||
}
|
||||
case .chunk:
|
||||
@ -123,7 +183,14 @@ extension CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
database.add(modifyOpe)
|
||||
database.add(op)
|
||||
}
|
||||
|
||||
func retryOperationIfPossible(retryAfter: Double, block: @escaping () -> ()) {
|
||||
let delayTime = DispatchTime.now() + retryAfter
|
||||
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: {
|
||||
block()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,14 +13,13 @@ extension WebFeed: CloudKitRecordConvertible {
|
||||
|
||||
enum CloudKitKey: String {
|
||||
case url
|
||||
case homePageURL
|
||||
case editedName
|
||||
}
|
||||
|
||||
static var cloudKitZoneID: CKRecordZone.ID {
|
||||
return CloudKitAccountZone.zoneID
|
||||
}
|
||||
|
||||
|
||||
var cloudKitPrimaryKey: String {
|
||||
return externalID!
|
||||
}
|
||||
@ -28,7 +27,6 @@ extension WebFeed: CloudKitRecordConvertible {
|
||||
var cloudKitRecord: CKRecord {
|
||||
let record = CKRecord(recordType: Self.cloudKitRecordType)
|
||||
record[.url] = url
|
||||
record[.homePageURL] = homePageURL
|
||||
record[.editedName] = editedName
|
||||
return record
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user