diff --git a/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift b/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift index 8de2f22ef..5c43ef037 100644 --- a/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift +++ b/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift @@ -36,31 +36,31 @@ public typealias CloudKitRecordKey = (recordType: CKRecord.RecordType, recordID: public protocol CloudKitZone: AnyObject { - static var qualityOfService: QualityOfService { get } + @MainActor static var qualityOfService: QualityOfService { get } var zoneID: CKRecordZone.ID { get } - var log: OSLog { get } + @MainActor var log: OSLog { get } @MainActor var container: CKContainer? { get } @MainActor var database: CKDatabase? { get } @MainActor var delegate: CloudKitZoneDelegate? { get set } /// Reset the change token used to determine what point in time we are doing changes fetches - func resetChangeToken() + @MainActor func resetChangeToken() /// Generates a new CKRecord.ID using a UUID for the record's name - func generateRecordID() -> CKRecord.ID - + @MainActor func generateRecordID() -> CKRecord.ID + /// Subscribe to changes at a zone level - func subscribeToZoneChanges() + @MainActor func subscribeToZoneChanges() /// Process a remove notification - func receiveRemoteNotification(userInfo: [AnyHashable : Any]) async + @MainActor func receiveRemoteNotification(userInfo: [AnyHashable : Any]) async } @MainActor public 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. @@ -71,7 +71,7 @@ public protocol CloudKitZone: AnyObject { return .default #endif } - + var oldChangeTokenKey: String { return "cloudkit.server.token.\(zoneID.zoneName)" } @@ -130,29 +130,17 @@ public protocol CloudKitZone: AnyObject { }) } - func receiveRemoteNotification(userInfo: [AnyHashable : Any]) async { + @MainActor func receiveRemoteNotification(userInfo: [AnyHashable : Any]) async { - await withCheckedContinuation { continuation in - Task { @MainActor in - self.receiveRemoteNotification(userInfo: userInfo) { - continuation.resume() - } - } - } - } - - @MainActor func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping @Sendable () -> Void) { let note = CKRecordZoneNotification(fromRemoteNotificationDictionary: userInfo) guard note?.recordZoneID?.zoneName == 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() + do { + try await fetchChangesInZone() + } catch { + os_log(.error, log: log, "%@ zone remote notification fetch error: %@", zoneID.zoneName, error.localizedDescription) } } diff --git a/CloudKitSync/.swiftpm/xcode/xcshareddata/xcschemes/CloudKitSync.xcscheme b/CloudKitSync/.swiftpm/xcode/xcshareddata/xcschemes/CloudKitSync.xcscheme index 259c0830f..d16ad4575 100644 --- a/CloudKitSync/.swiftpm/xcode/xcshareddata/xcschemes/CloudKitSync.xcscheme +++ b/CloudKitSync/.swiftpm/xcode/xcshareddata/xcschemes/CloudKitSync.xcscheme @@ -29,6 +29,18 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + String { + func createFeed(url: String, name: String?, editedName: String?, homePageURL: String?, containerExternalID: String) async throws -> String { let recordID = CKRecord.ID(recordName: url.md5String, zoneID: zoneID) let record = CKRecord(recordType: CloudKitFeed.recordType, recordID: recordID) @@ -107,9 +107,6 @@ enum CloudKitAccountZoneError: LocalizedError { record[CloudKitFeed.Fields.homePageURL] = homePageURL } - guard let containerExternalID = container.externalID else { - throw CloudKitZoneError.corruptAccount - } record[CloudKitFeed.Fields.containerExternalIDs] = [containerExternalID] try await save(record) @@ -117,11 +114,7 @@ enum CloudKitAccountZoneError: LocalizedError { } /// Rename the given web feed - func renameFeed(_ feed: Feed, editedName: String?) async throws { - - guard let externalID = feed.externalID else { - throw CloudKitZoneError.corruptAccount - } + func renameFeed(externalID: String, editedName: String?) async throws { let recordID = CKRecord.ID(recordName: externalID, zoneID: zoneID) let record = CKRecord(recordType: CloudKitFeed.recordType, recordID: recordID) @@ -132,22 +125,18 @@ enum CloudKitAccountZoneError: LocalizedError { /// Removes a web feed from a container and optionally deletes it, returning true if deleted @discardableResult - func removeFeed(_ feed: Feed, from: Container) async throws -> Bool { - - guard let fromContainerExternalID = from.externalID else { - throw CloudKitZoneError.corruptAccount - } + func removeFeed(externalID: String, from containerExternalID: String) async throws -> Bool { do { - let record = try await fetch(externalID: feed.externalID) + let record = try await fetch(externalID: externalID) if let containerExternalIDs = record[CloudKitFeed.Fields.containerExternalIDs] as? [String] { var containerExternalIDSet = Set(containerExternalIDs) - containerExternalIDSet.remove(fromContainerExternalID) + containerExternalIDSet.remove(containerExternalID) if containerExternalIDSet.isEmpty { - try await delete(externalID: feed.externalID) + try await delete(externalID: externalID) return true } else { record[CloudKitFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet) @@ -166,44 +155,32 @@ enum CloudKitAccountZoneError: LocalizedError { } } - func moveFeed(_ feed: Feed, from: Container, to: Container) async throws { + func moveFeed(externalID: String, from sourceContainerExternalID: String, to destinationContainerExternalID: String) async throws { - guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else { - throw CloudKitZoneError.corruptAccount - } - - let record = try await fetch(externalID: feed.externalID) + let record = try await fetch(externalID: externalID) if let containerExternalIDs = record[CloudKitFeed.Fields.containerExternalIDs] as? [String] { var containerExternalIDSet = Set(containerExternalIDs) - containerExternalIDSet.remove(fromContainerExternalID) - containerExternalIDSet.insert(toContainerExternalID) + containerExternalIDSet.remove(sourceContainerExternalID) + containerExternalIDSet.insert(destinationContainerExternalID) record[CloudKitFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet) try await save(record) } } - func addFeed(_ feed: Feed, to: Container) async throws { + func addFeed(externalID: String, to containerExternalID: String) async throws { - guard let toContainerExternalID = to.externalID else { - throw CloudKitZoneError.corruptAccount - } - - let record = try await fetch(externalID: feed.externalID) + let record = try await fetch(externalID: externalID) if let containerExternalIDs = record[CloudKitFeed.Fields.containerExternalIDs] as? [String] { var containerExternalIDSet = Set(containerExternalIDs) - containerExternalIDSet.insert(toContainerExternalID) + containerExternalIDSet.insert(containerExternalID) record[CloudKitFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet) try await save(record) } } - func findFeedExternalIDs(for folder: Folder) async throws -> [String] { - - guard let folderExternalID = folder.externalID else { - throw CloudKitAccountZoneError.unknown - } + func findFeedExternalIDs(for folderExternalID: String) async throws -> [String] { let predicate = NSPredicate(format: "containerExternalIDs CONTAINS %@", folderExternalID) let ckQuery = CKQuery(recordType: CloudKitFeed.recordType, predicate: predicate) @@ -216,9 +193,7 @@ enum CloudKitAccountZoneError: LocalizedError { func findOrCreateAccount() async throws -> String { - guard let database else { - throw CloudKitAccountZoneError.unknown - } + guard let database else { throw CloudKitAccountZoneError.unknown } let predicate = NSPredicate(format: "isAccount = \"1\"") let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate) @@ -254,11 +229,7 @@ enum CloudKitAccountZoneError: LocalizedError { return try await createContainer(name: name, isAccount: false) } - func renameFolder(_ folder: Folder, to name: String) async throws { - - guard let externalID = folder.externalID else { - throw CloudKitZoneError.corruptAccount - } + func renameFolder(externalID: String, to name: String) async throws { let recordID = CKRecord.ID(recordName: externalID, zoneID: zoneID) let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: recordID) @@ -267,9 +238,9 @@ enum CloudKitAccountZoneError: LocalizedError { try await save(record) } - func removeFolder(_ folder: Folder) async throws { + func removeFolder(externalID: String) async throws { - try await delete(externalID: folder.externalID) + try await delete(externalID: externalID) } }