Fix numerous concurrency warnings in CloudKit code.
This commit is contained in:
parent
88ec8b20c2
commit
254a02cd8e
@ -24,9 +24,9 @@ enum CloudKitAccountZoneError: LocalizedError {
|
|||||||
|
|
||||||
@MainActor final class CloudKitAccountZone: CloudKitZone {
|
@MainActor final class CloudKitAccountZone: CloudKitZone {
|
||||||
|
|
||||||
var zoneID: CKRecordZone.ID
|
let zoneID: CKRecordZone.ID
|
||||||
|
|
||||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||||
|
|
||||||
weak var container: CKContainer?
|
weak var container: CKContainer?
|
||||||
weak var database: CKDatabase?
|
weak var database: CKDatabase?
|
||||||
@ -240,7 +240,7 @@ enum CloudKitAccountZoneError: LocalizedError {
|
|||||||
return try await findOrCreateAccount()
|
return try await findOrCreateAccount()
|
||||||
|
|
||||||
case .zoneNotFound, .userDeletedZone:
|
case .zoneNotFound, .userDeletedZone:
|
||||||
try await createZoneRecord()
|
_ = try await createZoneRecord()
|
||||||
return try await findOrCreateAccount()
|
return try await findOrCreateAccount()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -59,7 +59,7 @@ final class CloudKitArticlesZone: CloudKitZone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(container: CKContainer) {
|
@MainActor init(container: CKContainer) {
|
||||||
self.container = container
|
self.container = container
|
||||||
self.database = container.privateCloudDatabase
|
self.database = container.privateCloudDatabase
|
||||||
self.zoneID = CKRecordZone.ID(zoneName: "Articles", ownerName: CKCurrentUserDefaultName)
|
self.zoneID = CKRecordZone.ID(zoneName: "Articles", ownerName: CKCurrentUserDefaultName)
|
||||||
@ -73,16 +73,16 @@ final class CloudKitArticlesZone: CloudKitZone {
|
|||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
if case CloudKitZoneError.userDeletedZone = error {
|
if case CloudKitZoneError.userDeletedZone = error {
|
||||||
self.createZoneRecord() { result in
|
|
||||||
switch result {
|
Task { @MainActor in
|
||||||
case .success:
|
do {
|
||||||
Task { @MainActor in
|
_ = try await self.createZoneRecord()
|
||||||
self.refreshArticles(completion: completion)
|
self.refreshArticles(completion: completion)
|
||||||
}
|
} catch {
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
@ -174,16 +174,16 @@ private extension CloudKitArticlesZone {
|
|||||||
|
|
||||||
@MainActor func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
@MainActor func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||||
if case CloudKitZoneError.userDeletedZone = error {
|
if case CloudKitZoneError.userDeletedZone = error {
|
||||||
self.createZoneRecord() { result in
|
|
||||||
switch result {
|
Task { @MainActor in
|
||||||
case .success:
|
do {
|
||||||
MainActor.assumeIsolated {
|
_ = try await self.createZoneRecord()
|
||||||
self.modifyArticles(statusUpdates, completion: completion)
|
self.modifyArticles(statusUpdates, completion: completion)
|
||||||
}
|
} catch {
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
|
@ -94,5 +94,4 @@ public final class CloudKitError: LocalizedError {
|
|||||||
return NSLocalizedString("Unhandled Error.", comment: "Unknown iCloud Error")
|
return NSLocalizedString("Unhandled Error.", comment: "Unknown iCloud Error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -42,9 +42,9 @@ public protocol CloudKitZone: AnyObject {
|
|||||||
|
|
||||||
var log: OSLog { get }
|
var log: OSLog { get }
|
||||||
|
|
||||||
var container: CKContainer? { get }
|
@MainActor var container: CKContainer? { get }
|
||||||
var database: CKDatabase? { get }
|
@MainActor var database: CKDatabase? { get }
|
||||||
var delegate: CloudKitZoneDelegate? { get set }
|
@MainActor var delegate: CloudKitZoneDelegate? { get set }
|
||||||
|
|
||||||
/// Reset the change token used to determine what point in time we are doing changes fetches
|
/// Reset the change token used to determine what point in time we are doing changes fetches
|
||||||
func resetChangeToken()
|
func resetChangeToken()
|
||||||
@ -59,7 +59,7 @@ public protocol CloudKitZone: AnyObject {
|
|||||||
func receiveRemoteNotification(userInfo: [AnyHashable : Any]) async
|
func receiveRemoteNotification(userInfo: [AnyHashable : Any]) async
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension CloudKitZone {
|
@MainActor public extension CloudKitZone {
|
||||||
|
|
||||||
// My observation has been that QoS is treated differently for CloudKit operations on macOS vs iOS.
|
// 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.
|
// .userInitiated is too aggressive on iOS and can lead the UI slowing down and appearing to block.
|
||||||
@ -172,7 +172,7 @@ public extension CloudKitZone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves the zone record for this zone only. If the record isn't found it will be created.
|
/// Retrieves the zone record for this zone only. If the record isn't found it will be created.
|
||||||
func fetchZoneRecord(completion: @escaping @Sendable (Result<CKRecordZone?, Error>) -> Void) {
|
@MainActor func fetchZoneRecord(completion: @escaping @Sendable (Result<CKRecordZone?, Error>) -> Void) {
|
||||||
|
|
||||||
let op = CKFetchRecordZonesOperation(recordZoneIDs: [zoneID])
|
let op = CKFetchRecordZonesOperation(recordZoneIDs: [zoneID])
|
||||||
op.qualityOfService = Self.qualityOfService
|
op.qualityOfService = Self.qualityOfService
|
||||||
@ -193,38 +193,38 @@ public extension CloudKitZone {
|
|||||||
|
|
||||||
op.fetchRecordZonesResultBlock = { [weak self] result in
|
op.fetchRecordZonesResultBlock = { [weak self] result in
|
||||||
|
|
||||||
guard let self else {
|
Task { @MainActor in
|
||||||
completion(.failure(CloudKitZoneError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
guard let self else {
|
||||||
|
completion(.failure(CloudKitZoneError.unknown))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
case .success:
|
switch result {
|
||||||
completion(.success(zoneRecords[self.zoneID]))
|
|
||||||
|
|
||||||
case .failure(let error):
|
case .success:
|
||||||
|
completion(.success(zoneRecords[self.zoneID]))
|
||||||
|
|
||||||
switch CloudKitZoneResult.resolve(error) {
|
case .failure(let error):
|
||||||
case .zoneNotFound, .userDeletedZone:
|
|
||||||
self.createZoneRecord() { result in
|
switch CloudKitZoneResult.resolve(error) {
|
||||||
switch result {
|
case .zoneNotFound, .userDeletedZone:
|
||||||
case .success:
|
|
||||||
self.fetchZoneRecord(completion: completion)
|
do {
|
||||||
case .failure(let error):
|
let recordZone = try await self.createZoneRecord()
|
||||||
DispatchQueue.main.async {
|
completion(.success(recordZone))
|
||||||
completion(.failure(error))
|
} catch {
|
||||||
}
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case .retry(let timeToWait):
|
case .retry(let timeToWait):
|
||||||
os_log(.error, log: self.log, "%@ zone fetch changes retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
os_log(.error, log: self.log, "%@ zone fetch changes retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
||||||
self.retryIfPossible(after: timeToWait) {
|
await self.delay(for: timeToWait)
|
||||||
self.fetchZoneRecord(completion: completion)
|
self.fetchZoneRecord(completion: completion)
|
||||||
}
|
default:
|
||||||
default:
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
completion(.failure(CloudKitError(error)))
|
||||||
completion(.failure(CloudKitError(error)))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,54 +234,34 @@ public extension CloudKitZone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the zone record
|
/// Creates the zone record
|
||||||
func createZoneRecord() async throws {
|
func createZoneRecord() async throws -> CKRecordZone {
|
||||||
|
|
||||||
try await withCheckedThrowingContinuation { continuation in
|
guard let database else { throw CloudKitZoneError.unknown }
|
||||||
self.createZoneRecord { result in
|
|
||||||
switch result {
|
|
||||||
case .success:
|
|
||||||
continuation.resume()
|
|
||||||
case .failure(let error):
|
|
||||||
continuation.resume(throwing: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates the zone record
|
do {
|
||||||
func createZoneRecord(completion: @escaping @Sendable (Result<Void, Error>) -> Void) {
|
return try await database.save(CKRecordZone(zoneID: zoneID))
|
||||||
guard let database = database else {
|
} catch {
|
||||||
completion(.failure(CloudKitZoneError.unknown))
|
throw CloudKitError(error)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
database.save(CKRecordZone(zoneID: 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
|
/// Subscribes to zone changes
|
||||||
func subscribeToZoneChanges() {
|
func subscribeToZoneChanges() {
|
||||||
let subscription = CKRecordZoneSubscription(zoneID: zoneID, subscriptionID: zoneID.zoneName)
|
|
||||||
|
|
||||||
let info = CKSubscription.NotificationInfo()
|
Task { @MainActor in
|
||||||
info.shouldSendContentAvailable = true
|
let subscription = CKRecordZoneSubscription(zoneID: zoneID, subscriptionID: zoneID.zoneName)
|
||||||
subscription.notificationInfo = info
|
|
||||||
|
|
||||||
save(subscription) { result in
|
let info = CKSubscription.NotificationInfo()
|
||||||
if case .failure(let error) = result {
|
info.shouldSendContentAvailable = true
|
||||||
|
subscription.notificationInfo = info
|
||||||
|
|
||||||
|
do {
|
||||||
|
_ = try await save(subscription)
|
||||||
|
} catch {
|
||||||
os_log(.error, log: self.log, "%@ zone subscribe to changes error: %@", self.zoneID.zoneName, error.localizedDescription)
|
os_log(.error, log: self.log, "%@ zone subscribe to changes error: %@", self.zoneID.zoneName, error.localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Issue a CKQuery and return the resulting CKRecords.
|
/// Issue a CKQuery and return the resulting CKRecords.
|
||||||
func query(_ ckQuery: CKQuery, desiredKeys: [String]? = nil) async throws -> [CKRecord] {
|
func query(_ ckQuery: CKQuery, desiredKeys: [String]? = nil) async throws -> [CKRecord] {
|
||||||
@ -322,55 +302,43 @@ public extension CloudKitZone {
|
|||||||
|
|
||||||
op.queryResultBlock = { [weak self] result in
|
op.queryResultBlock = { [weak self] result in
|
||||||
|
|
||||||
guard let self else {
|
Task { @MainActor in
|
||||||
completion(.failure(CloudKitZoneError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
guard let self else {
|
||||||
|
completion(.failure(CloudKitZoneError.unknown))
|
||||||
case .success(let cursor):
|
return
|
||||||
if let cursor {
|
|
||||||
Task { @MainActor in
|
|
||||||
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
completion(.success(records))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
switch result {
|
||||||
|
|
||||||
switch CloudKitZoneResult.resolve(error) {
|
case .success(let cursor):
|
||||||
|
if let cursor {
|
||||||
case .zoneNotFound:
|
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
|
||||||
self.createZoneRecord() { result in
|
} else {
|
||||||
switch result {
|
completion(.success(records))
|
||||||
case .success:
|
|
||||||
Task { @MainActor in
|
|
||||||
self.query(ckQuery, desiredKeys: desiredKeys, completion: completion)
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .retry(let timeToWait):
|
case .failure(let error):
|
||||||
os_log(.error, log: self.log, "%@ zone query retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
|
||||||
self.retryIfPossible(after: timeToWait) {
|
switch CloudKitZoneResult.resolve(error) {
|
||||||
Task { @MainActor in
|
|
||||||
|
case .zoneNotFound:
|
||||||
|
do {
|
||||||
|
_ = try await self.createZoneRecord()
|
||||||
self.query(ckQuery, desiredKeys: desiredKeys, completion: completion)
|
self.query(ckQuery, desiredKeys: desiredKeys, completion: completion)
|
||||||
|
} catch {
|
||||||
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case .userDeletedZone:
|
case .retry(let timeToWait):
|
||||||
DispatchQueue.main.async {
|
os_log(.error, log: self.log, "%@ zone query retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
||||||
|
await self.delay(for: timeToWait)
|
||||||
|
self.query(ckQuery, desiredKeys: desiredKeys, completion: completion)
|
||||||
|
|
||||||
|
case .userDeletedZone:
|
||||||
completion(.failure(CloudKitZoneError.userDeletedZone))
|
completion(.failure(CloudKitZoneError.userDeletedZone))
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(CloudKitError(error)))
|
completion(.failure(CloudKitError(error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -419,51 +387,44 @@ public extension CloudKitZone {
|
|||||||
|
|
||||||
op.queryResultBlock = { [weak self] result in
|
op.queryResultBlock = { [weak self] result in
|
||||||
|
|
||||||
guard let self else {
|
Task { @MainActor in
|
||||||
completion(.failure(CloudKitZoneError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
guard let self else {
|
||||||
|
completion(.failure(CloudKitZoneError.unknown))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
case .success(let newCursor):
|
switch result {
|
||||||
Task { @MainActor in
|
|
||||||
|
case .success(let newCursor):
|
||||||
if let newCursor = newCursor {
|
if let newCursor = newCursor {
|
||||||
self.query(cursor: newCursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
|
self.query(cursor: newCursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
|
||||||
} else {
|
} else {
|
||||||
completion(.success(records))
|
completion(.success(records))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
|
||||||
switch CloudKitZoneResult.resolve(error) {
|
switch CloudKitZoneResult.resolve(error) {
|
||||||
case .zoneNotFound:
|
|
||||||
self.createZoneRecord() { result in
|
case .zoneNotFound:
|
||||||
switch result {
|
|
||||||
case .success:
|
do {
|
||||||
Task { @MainActor in
|
_ = try await self.createZoneRecord()
|
||||||
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, 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) {
|
|
||||||
Task { @MainActor in
|
|
||||||
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
|
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
|
||||||
|
} catch {
|
||||||
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case .userDeletedZone:
|
case .retry(let timeToWait):
|
||||||
DispatchQueue.main.async {
|
os_log(.error, log: self.log, "%@ zone query retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
||||||
|
await self.delay(for: timeToWait)
|
||||||
|
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
|
||||||
|
|
||||||
|
case .userDeletedZone:
|
||||||
completion(.failure(CloudKitZoneError.userDeletedZone))
|
completion(.failure(CloudKitZoneError.userDeletedZone))
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(CloudKitError(error)))
|
completion(.failure(CloudKitError(error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,42 +459,41 @@ public extension CloudKitZone {
|
|||||||
let recordID = CKRecord.ID(recordName: externalID, zoneID: zoneID)
|
let recordID = CKRecord.ID(recordName: externalID, zoneID: zoneID)
|
||||||
|
|
||||||
database?.fetch(withRecordID: recordID) { [weak self] record, error in
|
database?.fetch(withRecordID: recordID) { [weak self] record, error in
|
||||||
guard let self = self else {
|
|
||||||
|
guard let self else {
|
||||||
completion(.failure(CloudKitZoneError.unknown))
|
completion(.failure(CloudKitZoneError.unknown))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch CloudKitZoneResult.resolve(error) {
|
Task { @MainActor in
|
||||||
case .success:
|
|
||||||
DispatchQueue.main.async {
|
switch CloudKitZoneResult.resolve(error) {
|
||||||
if let record = record {
|
|
||||||
|
case .success:
|
||||||
|
if let record {
|
||||||
completion(.success(record))
|
completion(.success(record))
|
||||||
} else {
|
} else {
|
||||||
completion(.failure(CloudKitZoneError.unknown))
|
completion(.failure(CloudKitZoneError.unknown))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case .zoneNotFound:
|
case .zoneNotFound:
|
||||||
self.createZoneRecord() { result in
|
|
||||||
switch result {
|
do {
|
||||||
case .success:
|
_ = try await self.createZoneRecord()
|
||||||
self.fetch(externalID: externalID, completion: completion)
|
self.fetch(externalID: externalID, completion: completion)
|
||||||
case .failure(let error):
|
} catch {
|
||||||
DispatchQueue.main.async {
|
completion(.failure(error))
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case .retry(let timeToWait):
|
case .retry(let timeToWait):
|
||||||
os_log(.error, log: self.log, "%@ zone fetch retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
os_log(.error, log: self.log, "%@ zone fetch retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
||||||
self.retryIfPossible(after: timeToWait) {
|
self.retryIfPossible(after: timeToWait) {
|
||||||
self.fetch(externalID: externalID, completion: completion)
|
self.fetch(externalID: externalID, completion: completion)
|
||||||
}
|
}
|
||||||
case .userDeletedZone:
|
case .userDeletedZone:
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(CloudKitZoneError.userDeletedZone))
|
completion(.failure(CloudKitZoneError.userDeletedZone))
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(CloudKitError(error!)))
|
completion(.failure(CloudKitError(error!)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -610,65 +570,59 @@ public extension CloudKitZone {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch result {
|
Task { @MainActor in
|
||||||
|
|
||||||
case .success:
|
switch result {
|
||||||
completion(.success(()))
|
|
||||||
|
|
||||||
case .failure(let error):
|
case .success:
|
||||||
|
completion(.success(()))
|
||||||
|
|
||||||
switch CloudKitZoneResult.resolve(error) {
|
case .failure(let error):
|
||||||
case .success, .partialFailure:
|
|
||||||
DispatchQueue.main.async {
|
switch CloudKitZoneResult.resolve(error) {
|
||||||
|
case .success, .partialFailure:
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
|
||||||
|
|
||||||
case .zoneNotFound:
|
case .zoneNotFound:
|
||||||
self.createZoneRecord() { result in
|
|
||||||
switch result {
|
do {
|
||||||
case .success:
|
_ = try await self.createZoneRecord()
|
||||||
self.saveIfNew(records, completion: completion)
|
self.saveIfNew(records, completion: completion)
|
||||||
case .failure(let error):
|
} catch {
|
||||||
DispatchQueue.main.async {
|
completion(.failure(error))
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case .userDeletedZone:
|
case .userDeletedZone:
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(CloudKitZoneError.userDeletedZone))
|
completion(.failure(CloudKitZoneError.userDeletedZone))
|
||||||
}
|
|
||||||
|
|
||||||
case .retry(let timeToWait):
|
case .retry(let timeToWait):
|
||||||
self.retryIfPossible(after: timeToWait) {
|
self.retryIfPossible(after: timeToWait) {
|
||||||
self.saveIfNew(records, completion: completion)
|
self.saveIfNew(records, completion: completion)
|
||||||
}
|
|
||||||
|
|
||||||
case .limitExceeded:
|
|
||||||
|
|
||||||
var chunkedRecords = records.chunked(into: 200)
|
|
||||||
|
|
||||||
func saveChunksIfNew() {
|
|
||||||
if let records = chunkedRecords.popLast() {
|
|
||||||
self.saveIfNew(records) { result in
|
|
||||||
switch result {
|
|
||||||
case .success:
|
|
||||||
os_log(.info, log: self.log, "Saved %d chunked new records.", records.count)
|
|
||||||
saveChunksIfNew()
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
saveChunksIfNew()
|
case .limitExceeded:
|
||||||
|
|
||||||
default:
|
var chunkedRecords = records.chunked(into: 200)
|
||||||
DispatchQueue.main.async {
|
|
||||||
|
@MainActor func saveChunksIfNew() {
|
||||||
|
if let records = chunkedRecords.popLast() {
|
||||||
|
self.saveIfNew(records) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
os_log(.info, log: self.log, "Saved %d chunked new records.", records.count)
|
||||||
|
saveChunksIfNew()
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveChunksIfNew()
|
||||||
|
|
||||||
|
default:
|
||||||
completion(.failure(CloudKitError(error)))
|
completion(.failure(CloudKitError(error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -681,52 +635,26 @@ public extension CloudKitZone {
|
|||||||
/// Save the CKSubscription
|
/// Save the CKSubscription
|
||||||
func save(_ subscription: CKSubscription) async throws -> CKSubscription {
|
func save(_ subscription: CKSubscription) async throws -> CKSubscription {
|
||||||
|
|
||||||
try await withCheckedThrowingContinuation { continuation in
|
guard let database else { throw CloudKitZoneError.unknown }
|
||||||
self.save(subscription) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let subscription):
|
|
||||||
continuation.resume(returning: subscription)
|
|
||||||
case .failure(let error):
|
|
||||||
continuation.resume(throwing: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save the CKSubscription
|
do {
|
||||||
func save(_ subscription: CKSubscription, completion: @escaping @Sendable (Result<CKSubscription, Error>) -> Void) {
|
return try await database.save(subscription)
|
||||||
database?.save(subscription) { [weak self] savedSubscription, error in
|
|
||||||
|
|
||||||
guard let self else {
|
} catch {
|
||||||
completion(.failure(CloudKitZoneError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch CloudKitZoneResult.resolve(error) {
|
switch CloudKitZoneResult.resolve(error) {
|
||||||
case .success:
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.success((savedSubscription!)))
|
|
||||||
}
|
|
||||||
case .zoneNotFound:
|
case .zoneNotFound:
|
||||||
self.createZoneRecord() { result in
|
_ = try await createZoneRecord()
|
||||||
switch result {
|
return try await save(subscription)
|
||||||
case .success:
|
|
||||||
self.save(subscription, completion: completion)
|
|
||||||
case .failure(let error):
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .retry(let timeToWait):
|
case .retry(let timeToWait):
|
||||||
os_log(.error, log: self.log, "%@ zone save subscription retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
os_log(.error, log: self.log, "%@ zone save subscription retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
||||||
self.retryIfPossible(after: timeToWait) {
|
await delay(for: timeToWait)
|
||||||
self.save(subscription, completion: completion)
|
return try await save(subscription)
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
DispatchQueue.main.async {
|
throw CloudKitError(error)
|
||||||
completion(.failure(CloudKitError(error!)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -938,24 +866,32 @@ public extension CloudKitZone {
|
|||||||
|
|
||||||
/// Delete a CKSubscription
|
/// Delete a CKSubscription
|
||||||
func delete(subscriptionID: String, completion: @escaping @Sendable (Result<Void, Error>) -> Void) {
|
func delete(subscriptionID: String, completion: @escaping @Sendable (Result<Void, Error>) -> Void) {
|
||||||
database?.delete(withSubscriptionID: subscriptionID) { [weak self] _, error in
|
|
||||||
guard let self = self else {
|
guard let database else {
|
||||||
|
completion(.failure(CloudKitZoneError.unknown))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
database.delete(withSubscriptionID: subscriptionID) { [weak self] _, error in
|
||||||
|
|
||||||
|
guard let self else {
|
||||||
completion(.failure(CloudKitZoneError.unknown))
|
completion(.failure(CloudKitZoneError.unknown))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch CloudKitZoneResult.resolve(error) {
|
Task { @MainActor in
|
||||||
case .success:
|
|
||||||
DispatchQueue.main.async {
|
switch CloudKitZoneResult.resolve(error) {
|
||||||
|
|
||||||
|
case .success:
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
|
||||||
case .retry(let timeToWait):
|
case .retry(let timeToWait):
|
||||||
os_log(.error, log: self.log, "%@ zone delete subscription retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
os_log(.error, log: self.log, "%@ zone delete subscription retry in %f seconds.", self.zoneID.zoneName, timeToWait)
|
||||||
self.retryIfPossible(after: timeToWait) {
|
await self.delay(for: timeToWait)
|
||||||
self.delete(subscriptionID: subscriptionID, completion: completion)
|
self.delete(subscriptionID: subscriptionID, completion: completion)
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(CloudKitError(error!)))
|
completion(.failure(CloudKitError(error!)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1014,16 +950,16 @@ public extension CloudKitZone {
|
|||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
case .zoneNotFound:
|
case .zoneNotFound:
|
||||||
self.createZoneRecord() { result in
|
|
||||||
switch result {
|
Task { @MainActor in
|
||||||
case .success:
|
do {
|
||||||
|
_ = try await self.createZoneRecord()
|
||||||
self.modify(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
self.modify(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
||||||
case .failure(let error):
|
} catch {
|
||||||
DispatchQueue.main.async {
|
completion(.failure(error))
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case .userDeletedZone:
|
case .userDeletedZone:
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
completion(.failure(CloudKitZoneError.userDeletedZone))
|
completion(.failure(CloudKitZoneError.userDeletedZone))
|
||||||
@ -1040,12 +976,13 @@ public extension CloudKitZone {
|
|||||||
func saveChunks(completion: @escaping (Result<Void, Error>) -> Void) {
|
func saveChunks(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
if !recordToSaveChunks.isEmpty {
|
if !recordToSaveChunks.isEmpty {
|
||||||
let records = recordToSaveChunks.removeFirst()
|
let records = recordToSaveChunks.removeFirst()
|
||||||
self.modify(recordsToSave: records, recordIDsToDelete: []) { result in
|
|
||||||
switch result {
|
Task { @MainActor in
|
||||||
case .success:
|
do {
|
||||||
|
try await self.modify(recordsToSave: records, recordIDsToDelete: [])
|
||||||
os_log(.info, log: self.log, "Saved %d chunked records.", records.count)
|
os_log(.info, log: self.log, "Saved %d chunked records.", records.count)
|
||||||
saveChunks(completion: completion)
|
saveChunks(completion: completion)
|
||||||
case .failure(let error):
|
} catch {
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1057,17 +994,18 @@ public extension CloudKitZone {
|
|||||||
func deleteChunks() {
|
func deleteChunks() {
|
||||||
if !recordIDsToDeleteChunks.isEmpty {
|
if !recordIDsToDeleteChunks.isEmpty {
|
||||||
let records = recordIDsToDeleteChunks.removeFirst()
|
let records = recordIDsToDeleteChunks.removeFirst()
|
||||||
self.modify(recordsToSave: [], recordIDsToDelete: records) { result in
|
|
||||||
switch result {
|
Task { @MainActor in
|
||||||
case .success:
|
|
||||||
|
do {
|
||||||
|
try await self.modify(recordsToSave: [], recordIDsToDelete: records)
|
||||||
os_log(.info, log: self.log, "Deleted %d chunked records.", records.count)
|
os_log(.info, log: self.log, "Deleted %d chunked records.", records.count)
|
||||||
deleteChunks()
|
deleteChunks()
|
||||||
case .failure(let error):
|
} catch {
|
||||||
DispatchQueue.main.async {
|
completion(.failure(error))
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
@ -1115,6 +1053,11 @@ public extension CloudKitZone {
|
|||||||
/// Fetch all the changes in the CKZone since the last time we checked
|
/// Fetch all the changes in the CKZone since the last time we checked
|
||||||
@MainActor func fetchChangesInZone(completion: @escaping (Result<Void, Error>) -> Void) {
|
@MainActor func fetchChangesInZone(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
|
guard let database else {
|
||||||
|
completion(.failure(CloudKitZoneError.unknown))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var changedRecords = [CKRecord]()
|
var changedRecords = [CKRecord]()
|
||||||
var deletedRecordKeys = [CloudKitRecordKey]()
|
var deletedRecordKeys = [CloudKitRecordKey]()
|
||||||
|
|
||||||
@ -1154,58 +1097,49 @@ public extension CloudKitZone {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch result {
|
Task { @MainActor in
|
||||||
|
|
||||||
case .success:
|
switch result {
|
||||||
Task { @MainActor in
|
|
||||||
|
case .success:
|
||||||
do {
|
do {
|
||||||
try await self.delegate?.cloudKitDidModify(changed: changedRecords, deleted: deletedRecordKeys)
|
try await self.delegate?.cloudKitDidModify(changed: changedRecords, deleted: deletedRecordKeys)
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
} catch {
|
} catch {
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
|
||||||
switch CloudKitZoneResult.resolve(error) {
|
switch CloudKitZoneResult.resolve(error) {
|
||||||
case .zoneNotFound:
|
case .zoneNotFound:
|
||||||
self.createZoneRecord() { result in
|
|
||||||
switch result {
|
do {
|
||||||
case .success:
|
_ = try await self.createZoneRecord()
|
||||||
Task { @MainActor in
|
|
||||||
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) {
|
|
||||||
Task { @MainActor in
|
|
||||||
self.fetchChangesInZone(completion: completion)
|
self.fetchChangesInZone(completion: completion)
|
||||||
|
} catch {
|
||||||
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case .changeTokenExpired:
|
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)
|
||||||
|
await self.delay(for: timeToWait)
|
||||||
|
self.fetchChangesInZone(completion: completion)
|
||||||
|
|
||||||
|
case .changeTokenExpired:
|
||||||
self.changeToken = nil
|
self.changeToken = nil
|
||||||
self.fetchChangesInZone(completion: completion)
|
self.fetchChangesInZone(completion: completion)
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(CloudKitError(error)))
|
completion(.failure(CloudKitError(error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
database?.add(op)
|
database.add(op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user