Move common CloudKit classes to RSCore

This commit is contained in:
Maurice Parker 2020-11-20 10:17:40 -06:00
parent 7ccc0e6b17
commit 1c23f02803
7 changed files with 5 additions and 868 deletions

View File

@ -8,6 +8,7 @@
import Foundation
import os.log
import RSCore
import RSWeb
import RSParser
import CloudKit

View File

@ -8,6 +8,7 @@
import Foundation
import os.log
import RSCore
import RSParser
import RSWeb
import CloudKit

View File

@ -8,6 +8,7 @@
import Foundation
import os.log
import RSCore
import RSParser
import RSWeb
import CloudKit

View File

@ -1,98 +0,0 @@
//
// CloudKitError.swift
// Account
//
// Created by Maurice Parker on 3/26/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
// Derived from https://github.com/caiyue1993/IceCream
import Foundation
import CloudKit
class CloudKitError: LocalizedError {
let error: Error
init(_ error: Error) {
self.error = error
}
public var errorDescription: String? {
guard let ckError = error as? CKError else {
return error.localizedDescription
}
switch ckError.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 the iCloud account, 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")
}
}
}

View File

@ -1,687 +0,0 @@
//
// CloudKitZone.swift
// Account
//
// Created by Maurice Parker on 3/21/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import CloudKit
import os.log
import RSWeb
enum CloudKitZoneError: LocalizedError {
case userDeletedZone
case invalidParameter
case unknown
var errorDescription: String? {
if case .userDeletedZone = self {
return NSLocalizedString("The iCloud data was deleted. Please delete the NetNewsWire iCloud account and add it again to continue using NetNewsWire's iCloud support.", comment: "User deleted zone.")
} else {
return NSLocalizedString("An unexpected CloudKit error occurred.", comment: "An unexpected CloudKit error occurred.")
}
}
}
protocol CloudKitZoneDelegate: class {
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void);
}
typealias CloudKitRecordKey = (recordType: CKRecord.RecordType, recordID: CKRecord.ID)
protocol CloudKitZone: class {
static var zoneID: CKRecordZone.ID { get }
static var qualityOfService: QualityOfService { get }
var log: OSLog { get }
var container: CKContainer? { get }
var database: CKDatabase? { get }
var delegate: CloudKitZoneDelegate? { get set }
/// Reset the change token used to determine what point in time we are doing changes fetches
func resetChangeToken()
/// Generates a new CKRecord.ID using a UUID for the record's name
func generateRecordID() -> CKRecord.ID
/// Subscribe to changes at a zone level
func subscribeToZoneChanges()
/// Process a remove notification
func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void)
}
extension CloudKitZone {
// My observation has been that QoS is treated differently for CloudKit operations on macOS vs iOS.
// .userInitiated is too aggressive on iOS and can lead the UI slowing down and appearing to block.
// .default (or lower) on macOS will sometimes hang for extended periods of time and appear to hang.
static var qualityOfService: QualityOfService {
#if os(macOS)
return .userInitiated
#else
return .default
#endif
}
/// Reset the change token used to determine what point in time we are doing changes fetches
func resetChangeToken() {
changeToken = nil
}
func generateRecordID() -> CKRecord.ID {
return CKRecord.ID(recordName: UUID().uuidString, zoneID: Self.zoneID)
}
func retryIfPossible(after: Double, block: @escaping () -> ()) {
let delayTime = DispatchTime.now() + after
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: {
block()
})
}
func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
let note = CKRecordZoneNotification(fromRemoteNotificationDictionary: userInfo)
guard note?.recordZoneID?.zoneName == Self.zoneID.zoneName else {
completion()
return
}
fetchChangesInZone() { result in
if case .failure(let error) = result {
os_log(.error, log: self.log, "%@ zone remote notification fetch error: %@", Self.zoneID.zoneName, error.localizedDescription)
}
completion()
}
}
/// Creates the zone record
func createZoneRecord(completion: @escaping (Result<Void, Error>) -> Void) {
guard let database = database else {
completion(.failure(CloudKitZoneError.unknown))
return
}
database.save(CKRecordZone(zoneID: Self.zoneID)) { (recordZone, error) in
if let error = error {
DispatchQueue.main.async {
completion(.failure(CloudKitError(error)))
}
} else {
DispatchQueue.main.async {
completion(.success(()))
}
}
}
}
/// Subscribes to zone changes
func subscribeToZoneChanges() {
let subscription = CKRecordZoneSubscription(zoneID: Self.zoneID)
let info = CKSubscription.NotificationInfo()
info.shouldSendContentAvailable = true
subscription.notificationInfo = info
save(subscription) { result in
if case .failure(let error) = result {
os_log(.error, log: self.log, "%@ zone subscribe to changes error: %@", Self.zoneID.zoneName, error.localizedDescription)
}
}
}
/// Issue a CKQuery and return the resulting CKRecords.s
func query(_ query: CKQuery, completion: @escaping (Result<[CKRecord], Error>) -> Void) {
guard let database = database else {
completion(.failure(CloudKitZoneError.unknown))
return
}
database.perform(query, inZoneWith: Self.zoneID) { [weak self] records, error in
guard let self = self else {
completion(.failure(CloudKitZoneError.unknown))
return
}
switch CloudKitZoneResult.resolve(error) {
case .success:
DispatchQueue.main.async {
if let records = records {
completion(.success(records))
} else {
completion(.failure(CloudKitZoneError.unknown))
}
}
case .zoneNotFound:
self.createZoneRecord() { result in
switch result {
case .success:
self.query(query, completion: completion)
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone query retry in %f seconds.", Self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) {
self.query(query, completion: completion)
}
case .userDeletedZone:
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone))
}
default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!)))
}
}
}
}
/// Fetch a CKRecord by using its externalID
func fetch(externalID: String?, completion: @escaping (Result<CKRecord, Error>) -> Void) {
guard let externalID = externalID else {
completion(.failure(CloudKitZoneError.invalidParameter))
return
}
let recordID = CKRecord.ID(recordName: externalID, zoneID: Self.zoneID)
database?.fetch(withRecordID: recordID) { [weak self] record, error in
guard let self = self else {
completion(.failure(CloudKitZoneError.unknown))
return
}
switch CloudKitZoneResult.resolve(error) {
case .success:
DispatchQueue.main.async {
if let record = record {
completion(.success(record))
} else {
completion(.failure(CloudKitZoneError.unknown))
}
}
case .zoneNotFound:
self.createZoneRecord() { result in
switch result {
case .success:
self.fetch(externalID: externalID, completion: completion)
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone fetch retry in %f seconds.", Self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) {
self.fetch(externalID: externalID, completion: completion)
}
case .userDeletedZone:
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone))
}
default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!)))
}
}
}
}
/// Save the CKRecord
func save(_ record: CKRecord, completion: @escaping (Result<Void, Error>) -> Void) {
modify(recordsToSave: [record], recordIDsToDelete: [], completion: completion)
}
/// Save the CKRecords
func save(_ records: [CKRecord], completion: @escaping (Result<Void, Error>) -> Void) {
modify(recordsToSave: records, recordIDsToDelete: [], completion: completion)
}
/// Saves or modifies the records as long as they are unchanged relative to the local version
func saveIfNew(_ records: [CKRecord], completion: @escaping (Result<Void, Error>) -> Void) {
let op = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: [CKRecord.ID]())
op.savePolicy = .ifServerRecordUnchanged
op.isAtomic = false
op.qualityOfService = Self.qualityOfService
op.modifyRecordsCompletionBlock = { [weak self] (_, _, error) in
guard let self = self else { return }
switch CloudKitZoneResult.resolve(error) {
case .success, .partialFailure:
DispatchQueue.main.async {
completion(.success(()))
}
case .zoneNotFound:
self.createZoneRecord() { result in
switch result {
case .success:
self.saveIfNew(records, completion: completion)
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
case .userDeletedZone:
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone))
}
case .retry(let timeToWait):
self.retryIfPossible(after: timeToWait) {
self.saveIfNew(records, completion: completion)
}
case .limitExceeded:
let chunkedRecords = records.chunked(into: 300)
let group = DispatchGroup()
var errorOccurred = false
for chunk in chunkedRecords {
group.enter()
self.saveIfNew(chunk) { result in
if case .failure(let error) = result {
os_log(.error, log: self.log, "%@ zone modify records error: %@", Self.zoneID.zoneName, error.localizedDescription)
errorOccurred = true
}
group.leave()
}
}
group.notify(queue: DispatchQueue.main) {
if errorOccurred {
completion(.failure(CloudKitZoneError.unknown))
} else {
completion(.success(()))
}
}
default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!)))
}
}
}
database?.add(op)
}
/// Save the CKSubscription
func save(_ subscription: CKSubscription, completion: @escaping (Result<CKSubscription, Error>) -> Void) {
database?.save(subscription) { [weak self] savedSubscription, error in
guard let self = self else {
completion(.failure(CloudKitZoneError.unknown))
return
}
switch CloudKitZoneResult.resolve(error) {
case .success:
DispatchQueue.main.async {
completion(.success((savedSubscription!)))
}
case .zoneNotFound:
self.createZoneRecord() { result in
switch result {
case .success:
self.save(subscription, completion: completion)
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone save subscription retry in %f seconds.", Self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) {
self.save(subscription, completion: completion)
}
default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!)))
}
}
}
}
/// Delete CKRecords using a CKQuery
func delete(ckQuery: CKQuery, completion: @escaping (Result<Void, Error>) -> Void) {
var records = [CKRecord]()
let op = CKQueryOperation(query: ckQuery)
op.qualityOfService = Self.qualityOfService
op.recordFetchedBlock = { record in
records.append(record)
}
op.queryCompletionBlock = { [weak self] (cursor, error) in
guard let self = self else {
completion(.failure(CloudKitZoneError.unknown))
return
}
if let cursor = cursor {
self.delete(cursor: cursor, carriedRecords: records, completion: completion)
} else {
guard !records.isEmpty else {
DispatchQueue.main.async {
completion(.success(()))
}
return
}
let recordIDs = records.map { $0.recordID }
self.modify(recordsToSave: [], recordIDsToDelete: recordIDs, completion: completion)
}
}
database?.add(op)
}
/// Delete CKRecords using a CKQuery
func delete(cursor: CKQueryOperation.Cursor, carriedRecords: [CKRecord], completion: @escaping (Result<Void, Error>) -> Void) {
var records = [CKRecord]()
let op = CKQueryOperation(cursor: cursor)
op.qualityOfService = Self.qualityOfService
op.recordFetchedBlock = { record in
records.append(record)
}
op.queryCompletionBlock = { [weak self] (cursor, error) in
guard let self = self else {
completion(.failure(CloudKitZoneError.unknown))
return
}
records.append(contentsOf: carriedRecords)
if let cursor = cursor {
self.delete(cursor: cursor, carriedRecords: records, completion: completion)
} else {
let recordIDs = records.map { $0.recordID }
self.modify(recordsToSave: [], recordIDsToDelete: recordIDs, completion: completion)
}
}
database?.add(op)
}
/// Delete a CKRecord using its recordID
func delete(recordID: CKRecord.ID, completion: @escaping (Result<Void, Error>) -> Void) {
modify(recordsToSave: [], recordIDsToDelete: [recordID], completion: completion)
}
/// Delete CKRecords
func delete(recordIDs: [CKRecord.ID], completion: @escaping (Result<Void, Error>) -> Void) {
modify(recordsToSave: [], recordIDsToDelete: recordIDs, completion: completion)
}
/// Delete a CKRecord using its externalID
func delete(externalID: String?, completion: @escaping (Result<Void, Error>) -> Void) {
guard let externalID = externalID else {
completion(.failure(CloudKitZoneError.invalidParameter))
return
}
let recordID = CKRecord.ID(recordName: externalID, zoneID: Self.zoneID)
modify(recordsToSave: [], recordIDsToDelete: [recordID], completion: completion)
}
/// Delete a CKSubscription
func delete(subscriptionID: String, completion: @escaping (Result<Void, Error>) -> Void) {
database?.delete(withSubscriptionID: subscriptionID) { [weak self] _, error in
guard let self = self else {
completion(.failure(CloudKitZoneError.unknown))
return
}
switch CloudKitZoneResult.resolve(error) {
case .success:
DispatchQueue.main.async {
completion(.success(()))
}
case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone delete subscription retry in %f seconds.", Self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) {
self.delete(subscriptionID: subscriptionID, completion: completion)
}
default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!)))
}
}
}
}
/// Modify and delete the supplied CKRecords and CKRecord.IDs
func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping (Result<Void, Error>) -> Void) {
guard !(recordsToSave.isEmpty && recordIDsToDelete.isEmpty) else {
completion(.success(()))
return
}
let op = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete)
op.savePolicy = .changedKeys
op.isAtomic = true
op.qualityOfService = Self.qualityOfService
op.modifyRecordsCompletionBlock = { [weak self] (_, _, error) in
guard let self = self else {
completion(.failure(CloudKitZoneError.unknown))
return
}
switch CloudKitZoneResult.resolve(error) {
case .success:
DispatchQueue.main.async {
completion(.success(()))
}
case .zoneNotFound:
self.createZoneRecord() { result in
switch result {
case .success:
self.modify(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete, completion: completion)
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
case .userDeletedZone:
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone))
}
case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone modify retry in %f seconds.", Self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) {
self.modify(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete, completion: completion)
}
case .limitExceeded:
let recordToSaveChunks = recordsToSave.chunked(into: 300)
let recordIDsToDeleteChunks = recordIDsToDelete.chunked(into: 300)
let group = DispatchGroup()
var errorOccurred = false
for chunk in recordToSaveChunks {
group.enter()
self.modify(recordsToSave: chunk, recordIDsToDelete: []) { result in
if case .failure(let error) = result {
os_log(.error, log: self.log, "%@ zone modify records error: %@", Self.zoneID.zoneName, error.localizedDescription)
errorOccurred = true
}
group.leave()
}
}
for chunk in recordIDsToDeleteChunks {
group.enter()
self.modify(recordsToSave: [], recordIDsToDelete: chunk) { result in
if case .failure(let error) = result {
os_log(.error, log: self.log, "%@ zone modify records error: %@", Self.zoneID.zoneName, error.localizedDescription)
errorOccurred = true
}
group.leave()
}
}
group.notify(queue: DispatchQueue.global(qos: .background)) {
if errorOccurred {
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.unknown))
}
} else {
DispatchQueue.main.async {
completion(.success(()))
}
}
}
default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!)))
}
}
}
database?.add(op)
}
/// Fetch all the changes in the CKZone since the last time we checked
func fetchChangesInZone(completion: @escaping (Result<Void, Error>) -> Void) {
var savedChangeToken = changeToken
var changedRecords = [CKRecord]()
var deletedRecordKeys = [CloudKitRecordKey]()
let zoneConfig = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
zoneConfig.previousServerChangeToken = changeToken
let op = CKFetchRecordZoneChangesOperation(recordZoneIDs: [Self.zoneID], configurationsByRecordZoneID: [Self.zoneID: zoneConfig])
op.fetchAllChanges = true
op.qualityOfService = Self.qualityOfService
op.recordZoneChangeTokensUpdatedBlock = { zoneID, token, _ in
savedChangeToken = token
}
op.recordChangedBlock = { record in
changedRecords.append(record)
}
op.recordWithIDWasDeletedBlock = { recordID, recordType in
let recordKey = CloudKitRecordKey(recordType: recordType, recordID: recordID)
deletedRecordKeys.append(recordKey)
}
op.recordZoneFetchCompletionBlock = { zoneID ,token, _, _, error in
if case .success = CloudKitZoneResult.resolve(error) {
savedChangeToken = token
}
}
op.fetchRecordZoneChangesCompletionBlock = { [weak self] error in
guard let self = self else {
completion(.failure(CloudKitZoneError.unknown))
return
}
switch CloudKitZoneResult.resolve(error) {
case .success:
DispatchQueue.main.async {
self.delegate?.cloudKitDidModify(changed: changedRecords, deleted: deletedRecordKeys) { result in
switch result {
case .success:
self.changeToken = savedChangeToken
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
}
case .zoneNotFound:
self.createZoneRecord() { result in
switch result {
case .success:
self.fetchChangesInZone(completion: completion)
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
case .userDeletedZone:
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone))
}
case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone fetch changes retry in %f seconds.", Self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) {
self.fetchChangesInZone(completion: completion)
}
case .changeTokenExpired:
DispatchQueue.main.async {
self.changeToken = nil
self.fetchChangesInZone(completion: completion)
}
default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!)))
}
}
}
database?.add(op)
}
}
private extension CloudKitZone {
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
}
}

View File

@ -1,81 +0,0 @@
//
// CloudKitResult.swift
// Account
//
// Created by Maurice Parker on 3/26/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import CloudKit
enum CloudKitZoneResult {
case success
case retry(afterSeconds: Double)
case limitExceeded
case changeTokenExpired
case partialFailure(errors: [AnyHashable: CKError])
case serverRecordChanged
case zoneNotFound
case userDeletedZone
case failure(error: Error)
static func resolve(_ error: Error?) -> CloudKitZoneResult {
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? NSNumber {
return .retry(afterSeconds: retry.doubleValue)
} else {
return .failure(error: CloudKitError(ckError))
}
case .zoneNotFound:
return .zoneNotFound
case .userDeletedZone:
return .userDeletedZone
case .changeTokenExpired:
return .changeTokenExpired
case .serverRecordChanged:
return .serverRecordChanged
case .partialFailure:
if let partialErrors = ckError.userInfo[CKPartialErrorsByItemIDKey] as? [AnyHashable: CKError] {
if let zoneResult = anyRequestErrors(partialErrors) {
return zoneResult
} else {
return .partialFailure(errors: partialErrors)
}
} else {
return .failure(error: CloudKitError(ckError))
}
case .limitExceeded:
return .limitExceeded
default:
return .failure(error: CloudKitError(ckError))
}
}
}
private extension CloudKitZoneResult {
static func anyRequestErrors(_ errors: [AnyHashable: CKError]) -> CloudKitZoneResult? {
if errors.values.contains(where: { $0.code == .changeTokenExpired } ) {
return .changeTokenExpired
}
if errors.values.contains(where: { $0.code == .zoneNotFound } ) {
return .zoneNotFound
}
if errors.values.contains(where: { $0.code == .userDeletedZone } ) {
return .userDeletedZone
}
return nil
}
}

View File

@ -51,8 +51,8 @@
"repositoryURL": "https://github.com/Ranchero-Software/RSCore.git",
"state": {
"branch": null,
"revision": "1f72115989c05ca1e79fe694f25a17b9b731d0df",
"version": "1.0.0-beta3"
"revision": "3aa7710dda2b540834c05b9bb544a93e1166e25e",
"version": "1.0.0-beta5"
}
},
{