From 766eb507bfd78c9d39a342ef2b01ac527ae31111 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 30 Mar 2020 15:15:45 -0500 Subject: [PATCH] Add container handling code --- Frameworks/Account/Account.swift | 9 +++ Frameworks/Account/AccountMetadata.swift | 9 +++ .../CloudKit/CloudKitAccountDelegate.swift | 66 +++++++++++++++--- .../CloudKit/CloudKitAccountZone.swift | 69 ++++++++++++++++++- .../CloudKitAccountZoneDelegate.swift | 20 ++++++ .../Account/CloudKit/CloudKitZone.swift | 38 +++++++++- Frameworks/Account/Container.swift | 3 +- 7 files changed, 199 insertions(+), 15 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 17704e763..c3ad99300 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -136,6 +136,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public var topLevelWebFeeds = Set() public var folders: Set? = Set() + public var externalID: String? { + get { + return metadata.externalID + } + set { + metadata.externalID = newValue + } + } + public var sortedFolders: [Folder]? { if let folders = folders { return Array(folders).sorted(by: { $0.nameForDisplay < $1.nameForDisplay }) diff --git a/Frameworks/Account/AccountMetadata.swift b/Frameworks/Account/AccountMetadata.swift index 7c5f378f9..705424ca4 100644 --- a/Frameworks/Account/AccountMetadata.swift +++ b/Frameworks/Account/AccountMetadata.swift @@ -23,6 +23,7 @@ final class AccountMetadata: Codable { case lastArticleFetchStartTime = "lastArticleFetch" case lastArticleFetchEndTime case endpointURL + case externalID } var name: String? { @@ -81,6 +82,14 @@ final class AccountMetadata: Codable { } } + var externalID: String? { + didSet { + if externalID != oldValue { + valueDidChange(.externalID) + } + } + } + weak var delegate: AccountMetadataDelegate? func valueDidChange(_ key: CodingKeys) { diff --git a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift index b197431a0..64b98b0e4 100644 --- a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift @@ -235,26 +235,60 @@ final class CloudKitAccountDelegate: AccountDelegate { } func createFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { - if let folder = account.ensureFolder(with: name) { - completion(.success(folder)) - } else { - completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + accountZone.createFolder(name: name) { result in + switch result { + case .success(let externalID): + if let folder = account.ensureFolder(with: name) { + folder.externalID = externalID + completion(.success(folder)) + } else { + completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + } + case .failure(let error): + completion(.failure(error)) + } } } func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { - folder.name = name - completion(.success(())) + accountZone.renameFolder(folder, to: name) { result in + switch result { + case .success: + folder.name = name + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } } func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { - account.removeFolder(folder) - completion(.success(())) + accountZone.removeFolder(folder) { result in + switch result { + case .success: + account.removeFolder(folder) + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } } func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { - account.addFolder(folder) - completion(.success(())) + guard let name = folder.name else { + completion(.failure(LocalAccountDelegateError.invalidParameter)) + return + } + + accountZone.createFolder(name: name) { result in + switch result { + case .success(let externalID): + folder.externalID = externalID + account.addFolder(folder) + case .failure(let error): + completion(.failure(error)) + } + } } func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set
? { @@ -263,6 +297,18 @@ final class CloudKitAccountDelegate: AccountDelegate { func accountDidInitialize(_ account: Account) { accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress) + + if account.externalID == nil { + accountZone.findOrCreateAccount() { result in + switch result { + case .success(let externalID): + account.externalID = externalID + case .failure(let error): + os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription) + } + } + } + zones.forEach { zone in zone.resumeLongLivedOperationIfPossible() zone.subscribe() diff --git a/Frameworks/Account/CloudKit/CloudKitAccountZone.swift b/Frameworks/Account/CloudKit/CloudKitAccountZone.swift index 70b1b9aac..9c8fc173b 100644 --- a/Frameworks/Account/CloudKit/CloudKitAccountZone.swift +++ b/Frameworks/Account/CloudKit/CloudKitAccountZone.swift @@ -32,6 +32,14 @@ final class CloudKitAccountZone: CloudKitZone { } } + struct CloudKitContainer { + static let recordType = "Container" + struct Fields { + static let isAccount = "isAccount" + static let name = "name" + } + } + init(container: CKContainer) { self.container = container self.database = container.privateCloudDatabase @@ -77,11 +85,68 @@ final class CloudKitAccountZone: CloudKitZone { /// Deletes a web feed from iCloud func removeWebFeed(_ webFeed: WebFeed, completion: @escaping (Result) -> Void) { - guard let externalID = webFeed.externalID else { + delete(externalID: webFeed.externalID , completion: completion) + } + + func findOrCreateAccount(completion: @escaping (Result) -> Void) { + let predicate = NSPredicate(format: "isAccount = true") + let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate) + + query(ckQuery) { result in + switch result { + case .success(let records): + completion(.success(records[0].externalID)) + case .failure: + self.createContainer(name: "Account", isAccount: true, completion: completion) + } + } + } + + func createFolder(name: String, completion: @escaping (Result) -> Void) { + createContainer(name: name, isAccount: false, completion: completion) + } + + func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result) -> Void) { + guard let externalID = folder.externalID else { completion(.failure(CloudKitZoneError.invalidParameter)) return } - delete(externalID: externalID, completion: completion) + + let recordID = CKRecord.ID(recordName: externalID, zoneID: Self.zoneID) + let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: recordID) + record[CloudKitContainer.Fields.name] = name + + save(record: record) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + } + + func removeFolder(_ folder: Folder, completion: @escaping (Result) -> Void) { + delete(externalID: folder.externalID, completion: completion) + } + +} + +private extension CloudKitAccountZone { + + func createContainer(name: String, isAccount: Bool, completion: @escaping (Result) -> Void) { + let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID()) + record[CloudKitContainer.Fields.name] = name + record[CloudKitContainer.Fields.isAccount] = isAccount + + save(record: record) { result in + switch result { + case .success: + completion(.success(record.externalID)) + case .failure(let error): + completion(.failure(error)) + } + } } } diff --git a/Frameworks/Account/CloudKit/CloudKitAccountZoneDelegate.swift b/Frameworks/Account/CloudKit/CloudKitAccountZoneDelegate.swift index 7ba75fa8b..e758421e9 100644 --- a/Frameworks/Account/CloudKit/CloudKitAccountZoneDelegate.swift +++ b/Frameworks/Account/CloudKit/CloudKitAccountZoneDelegate.swift @@ -27,6 +27,8 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { switch record.recordType { case CloudKitAccountZone.CloudKitWebFeed.recordType: addOrUpdateWebFeed(record) + case CloudKitAccountZone.CloudKitContainer.recordType: + addOrUpdateContainer(record) default: assertionFailure("Unknown record type: \(record.recordType)") } @@ -36,6 +38,8 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { switch recordType { case CloudKitAccountZone.CloudKitWebFeed.recordType: removeWebFeed(recordID.externalID) + case CloudKitAccountZone.CloudKitContainer.recordType: + removeContainer(recordID.externalID) default: assertionFailure("Unknown record type: \(recordID.externalID)") } @@ -63,6 +67,22 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { } } + func addOrUpdateContainer(_ record: CKRecord) { + guard let account = account, let name = record[CloudKitAccountZone.CloudKitContainer.Fields.name] as? String else { return } + + if let folder = account.existingFolder(withExternalID: record.externalID) { + folder.name = name + } else { + account.ensureFolder(with: name) + } + } + + func removeContainer(_ externalID: String) { + if let folder = account?.existingFolder(withExternalID: externalID) { + account?.removeFolder(folder) + } + } + } private extension CloudKitAcountZoneDelegate { diff --git a/Frameworks/Account/CloudKit/CloudKitZone.swift b/Frameworks/Account/CloudKit/CloudKitZone.swift index 062f24607..5c53edbb2 100644 --- a/Frameworks/Account/CloudKit/CloudKitZone.swift +++ b/Frameworks/Account/CloudKit/CloudKitZone.swift @@ -100,11 +100,40 @@ extension CloudKitZone { modify(recordsToSave: [record], recordIDsToDelete: [], completion: completion) } - func delete(externalID: String, completion: @escaping (Result) -> Void) { + 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)) + return + } + + database.perform(query, inZoneWith: Self.zoneID) { records, error in + switch CloudKitZoneResult.resolve(error) { + case .success: + if let records = records { + completion(.success(records)) + } else { + completion(.failure(CloudKitZoneError.unknown)) + } + case .retry(let timeToWait): + self.retryOperationIfPossible(retryAfter: timeToWait) { + self.query(query, completion: completion) + } + default: + completion(.failure(error!)) + } + } + } + func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping (Result) -> Void) { let op = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete) @@ -252,7 +281,12 @@ private extension CloudKitZone { } func createZoneRecord(completion: @escaping (Result) -> Void) { - database?.save(CKRecordZone(zoneID: Self.zoneID)) { (recordZone, error) in + 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(error)) diff --git a/Frameworks/Account/Container.swift b/Frameworks/Account/Container.swift index f9dbaacb1..4405b0439 100644 --- a/Frameworks/Account/Container.swift +++ b/Frameworks/Account/Container.swift @@ -21,7 +21,8 @@ public protocol Container: class, ContainerIdentifiable { var account: Account? { get } var topLevelWebFeeds: Set { get set } var folders: Set? { get set } - + var externalID: String? { get set } + func hasAtLeastOneWebFeed() -> Bool func objectIsChild(_ object: AnyObject) -> Bool