diff --git a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift index 4b07d9cbf..c33330707 100644 --- a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift @@ -207,7 +207,7 @@ final class CloudKitAccountDelegate: AccountDelegate { } func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result) -> Void) { - accountZone.removeWebFeed(feed) { result in + accountZone.removeWebFeed(feed, from: container) { result in switch result { case .success: container.removeWebFeed(feed) @@ -218,20 +218,33 @@ final class CloudKitAccountDelegate: AccountDelegate { } } - func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result) -> Void) { - from.removeWebFeed(feed) - to.addWebFeed(feed) - completion(.success(())) + func moveWebFeed(for account: Account, with feed: WebFeed, from fromContainer: Container, to toContainer: Container, completion: @escaping (Result) -> Void) { + accountZone.moveWebFeed(feed, from: fromContainer, to: toContainer) { result in + switch result { + case .success: + fromContainer.removeWebFeed(feed) + toContainer.addWebFeed(feed) + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } } func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result) -> Void) { - container.addWebFeed(feed) - completion(.success(())) + accountZone.addWebFeed(feed, to: container) { result in + switch result { + case .success: + container.addWebFeed(feed) + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } } func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result) -> Void) { - container.addWebFeed(feed) - completion(.success(())) + addWebFeed(for: account, with: feed, to: container, completion: completion) } func createFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/CloudKit/CloudKitAccountZone.swift b/Frameworks/Account/CloudKit/CloudKitAccountZone.swift index 9f3db714a..ede217556 100644 --- a/Frameworks/Account/CloudKit/CloudKitAccountZone.swift +++ b/Frameworks/Account/CloudKit/CloudKitAccountZone.swift @@ -94,8 +94,72 @@ final class CloudKitAccountZone: CloudKitZone { } /// Deletes a web feed from iCloud - func removeWebFeed(_ webFeed: WebFeed, completion: @escaping (Result) -> Void) { - delete(externalID: webFeed.externalID , completion: completion) + func removeWebFeed(_ webFeed: WebFeed, from: Container, completion: @escaping (Result) -> Void) { + guard let fromContainerExternalID = from.externalID else { + completion(.failure(CloudKitZoneError.invalidParameter)) + return + } + + fetch(externalID: webFeed.externalID) { result in + switch result { + case .success(let record): + if let containerExternalIDs = record[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] { + var containerExternalIDSet = Set(containerExternalIDs) + containerExternalIDSet.remove(fromContainerExternalID) + if containerExternalIDSet.isEmpty { + self.delete(externalID: webFeed.externalID , completion: completion) + } else { + record[CloudKitWebFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet) + self.save(record, completion: completion) + } + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + func moveWebFeed(_ webFeed: WebFeed, from: Container, to: Container, completion: @escaping (Result) -> Void) { + guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else { + completion(.failure(CloudKitZoneError.invalidParameter)) + return + } + + fetch(externalID: webFeed.externalID) { result in + switch result { + case .success(let record): + if let containerExternalIDs = record[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] { + var containerExternalIDSet = Set(containerExternalIDs) + containerExternalIDSet.remove(fromContainerExternalID) + containerExternalIDSet.insert(toContainerExternalID) + record[CloudKitWebFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet) + self.save(record, completion: completion) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + func addWebFeed(_ webFeed: WebFeed, to: Container, completion: @escaping (Result) -> Void) { + guard let toContainerExternalID = to.externalID else { + completion(.failure(CloudKitZoneError.invalidParameter)) + return + } + + fetch(externalID: webFeed.externalID) { result in + switch result { + case .success(let record): + if let containerExternalIDs = record[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] { + var containerExternalIDSet = Set(containerExternalIDs) + containerExternalIDSet.insert(toContainerExternalID) + record[CloudKitWebFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet) + self.save(record, completion: completion) + } + case .failure(let error): + completion(.failure(error)) + } + } } func findOrCreateAccount(completion: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/CloudKit/CloudKitAccountZoneDelegate.swift b/Frameworks/Account/CloudKit/CloudKitAccountZoneDelegate.swift index 62dd94f5f..240050330 100644 --- a/Frameworks/Account/CloudKit/CloudKitAccountZoneDelegate.swift +++ b/Frameworks/Account/CloudKit/CloudKitAccountZoneDelegate.swift @@ -62,8 +62,8 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { } func removeWebFeed(_ externalID: String) { - if let webFeed = account?.existingWebFeed(withExternalID: externalID) { - account?.removeWebFeed(webFeed) + if let webFeed = account?.existingWebFeed(withExternalID: externalID), let containers = account?.existingContainers(withWebFeed: webFeed) { + containers.forEach { $0.removeWebFeed(webFeed) } } } @@ -107,7 +107,7 @@ private extension CloudKitAcountZoneDelegate { container.removeWebFeed(webFeed) } case .insert(_, let externalID, _): - if let container = existingContainers.first(where: { $0.externalID == externalID }) { + if let container = account.existingContainer(withExternalID: externalID) { container.addWebFeed(webFeed) } } diff --git a/Frameworks/Account/CloudKit/CloudKitZone.swift b/Frameworks/Account/CloudKit/CloudKitZone.swift index cf998ad9c..83dcff1a1 100644 --- a/Frameworks/Account/CloudKit/CloudKitZone.swift +++ b/Frameworks/Account/CloudKit/CloudKitZone.swift @@ -71,7 +71,7 @@ extension CloudKitZone { case .success: break case .retry(let timeToWait): - self.retryOperationIfPossible(retryAfter: timeToWait) { + self.retryIfPossible(after: timeToWait) { self.subscribe() } default: @@ -96,20 +96,6 @@ extension CloudKitZone { } } - func save(_ record: CKRecord, completion: @escaping (Result) -> Void) { - modify(recordsToSave: [record], recordIDsToDelete: [], completion: completion) - } - - func delete(externalID: String?, completion: @escaping (Result) -> 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) - } - func query(_ query: CKQuery, completion: @escaping (Result<[CKRecord], Error>) -> Void) { guard let database = database else { completion(.failure(CloudKitZoneError.unknown)) @@ -125,7 +111,7 @@ extension CloudKitZone { completion(.failure(CloudKitZoneError.unknown)) } case .retry(let timeToWait): - self.retryOperationIfPossible(retryAfter: timeToWait) { + self.retryIfPossible(after: timeToWait) { self.query(query, completion: completion) } default: @@ -134,6 +120,50 @@ extension CloudKitZone { } } + func fetch(externalID: String?, completion: @escaping (Result) -> Void) { + guard let externalID = externalID else { + completion(.failure(CloudKitZoneError.invalidParameter)) + return + } + + let recordID = CKRecord.ID(recordName: externalID, zoneID: Self.zoneID) + + database?.fetch(withRecordID: recordID) { record, error in + switch CloudKitZoneResult.resolve(error) { + case .success: + DispatchQueue.main.async { + if let record = record { + completion(.success(record)) + } else { + completion(.failure(CloudKitZoneError.unknown)) + } + } + case .retry(let timeToWait): + self.retryIfPossible(after: timeToWait) { + self.fetch(externalID: externalID, completion: completion) + } + default: + DispatchQueue.main.async { + completion(.failure(error!)) + } + } + } + } + + func save(_ record: CKRecord, completion: @escaping (Result) -> Void) { + modify(recordsToSave: [record], recordIDsToDelete: [], completion: completion) + } + + func delete(externalID: String?, completion: @escaping (Result) -> 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) + } + func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping (Result) -> Void) { let op = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete) @@ -173,7 +203,7 @@ extension CloudKitZone { completion(.failure(CloudKitZoneError.userDeletedZone)) } case .retry(let timeToWait): - self.retryOperationIfPossible(retryAfter: timeToWait) { + self.retryIfPossible(after: timeToWait) { self.modify(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete, completion: completion) } case .limitExceeded: @@ -230,7 +260,7 @@ extension CloudKitZone { self.changeToken = token } case .retry(let timeToWait): - self.retryOperationIfPossible(retryAfter: timeToWait) { + self.retryIfPossible(after: timeToWait) { self.fetchChangesInZone(completion: completion) } default: @@ -299,8 +329,8 @@ private extension CloudKitZone { } } - func retryOperationIfPossible(retryAfter: Double, block: @escaping () -> ()) { - let delayTime = DispatchTime.now() + retryAfter + func retryIfPossible(after: Double, block: @escaping () -> ()) { + let delayTime = DispatchTime.now() + after DispatchQueue.main.asyncAfter(deadline: delayTime, execute: { block() })