How to get notified on CKError.quotaExceeded

Hi all,

I have an iOS app which uses CloudKit and the standard NSPersistentCloudKitContainer, which I rely on for syncing app data between the user's devices. If the user's iCloud account is full I can see a log message while debugging in Xcode shortly after startup which looks something like this:

error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _requestAbortedNotInitialized:](2183): <NSCloudKitMirroringDelegate: 0x281ddc1e0> - Never successfully initialized and cannot execute request '<NSCloudKitMirroringExportRequest: 0x2841e00f0> 51383346-87BA-44D8-B527-A0B1EE35A0EF' due to error: <CKError 0x282c50db0: "Partial Failure" (2/1011); "Failed to modify some records"; uuid = 7BA17495-4F05-4AF4-A463-C0DF5A823B2E; container ID = "iCloud.com.neufsters.pangram"; partial errors: {
    E30B2972-FD4B-4D2A-BD1C-EB6F33F5367D:(com.apple.coredata.cloudkit.zone:__defaultOwner__) = <CKError 0x282c155f0: "Quota Exceeded" (25/2035); server message = "Quota exceeded"; op = FC4D3188D0A46ABC; uuid = 7BA17495-4F05-4AF4-A463-C0DF5A823B2E; Retry after 315.0 seconds>
    2FC9A487-D630-444D-B7F4-27A0F3A6B46E:(com.apple.coredata.cloudkit.zone:__defaultOwner__) = <CKError 0x282c52820: "Quota Exceeded" (25/2035); server message = "Quota exceeded"; op = FC4D3188D0A46ABC; uuid = 7BA17495-4F05-4AF4-A463-C0DF5A823B2E; Retry after 315.0 seconds>
    903DD6A0-0BD8-46C0-84FB-E89797514D9F:(com.apple.coredata.cloudkit.zone:__defaultOwner__) = <CKError 0x282c513e0: "Quota Exceeded" (25/2035); server message = "Quota exceeded"; op = FC4D3188D0A46ABC; uuid = 7BA17495-4F05-4AF4-A463-C0DF5A823B2E; Retry after 315.0 seconds>
}>

I would like to know how I can get a callback of some sort so I can run code if this CloudKit/CoreData error happens. In particular I'd like to put up some sort of warning to the user letting them know their data isn't going to sync.

Please note that I'm not looking for how to do error handling as a result of a user-initiated CloudKit API call. I'm looking for how to get notified when the background syncing logs errors like the above.

Thanks,

Russ

We would expect you to use NSPersistentCloudKitContainerEvent for this. You would observe event change notifications and see that the event failed with an error.

Thank you for your reply. I have tried something like the following, however the "quotaExceeded" never gets printed even though the above error message does. I can see also that the code enters the .partialFailure section of the if, however ckerror.partialErrorsByItemID is always nil or an empty list. Any suggests on how to unpack the error and get to the underlying .quotaExceeded?

class SyncMonitor {
    /// Where we store Combine cancellables for publishers we're listening to, e.g. NSPersistentCloudKitContainer's notifications.
    fileprivate var disposables = Set<AnyCancellable>()

    init() {
        NotificationCenter.default.publisher(for: NSPersistentCloudKitContainer.eventChangedNotification)
            .sink(receiveValue: { notification in
                
                print("notification: \(notification)")
                
                if let cloudEvent = notification.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey]
                    as? NSPersistentCloudKitContainer.Event {
                    // NSPersistentCloudKitContainer sends a notification when an event starts, and another when it
                    // ends. If it has an endDate, it means the event finished.
                    if cloudEvent.endDate == nil {
                        print("Starting an event...") // You could check the type, but I'm trying to keep this brief.
                    } else {
                        switch cloudEvent.type {
                        case .setup:
                            print("Setup finished!")
                        case .import:
                            print("An import finished!")
                        case .export:
                            print("An export finished!")
                        @unknown default:
                            assertionFailure("NSPersistentCloudKitContainer added a new event type.")
                        }

                        if cloudEvent.succeeded {
                            print("And it succeeded!")
                        } else {
                            print("But it failed!")
                        }

                        if let error = cloudEvent.error {
                            print("Error: \(error.localizedDescription)")
                            
                            guard let ckerror = error as? CKError else {
                                return
                            }

                            print("Error: code: \(ckerror.code), \(ckerror.localizedDescription)")
                            if ckerror.code == .partialFailure {
                                guard let errors = ckerror.partialErrorsByItemID else {
                                    return
                                }
                            
                                for (_, error) in errors {
                                    if let currentError = error as? CKError {
                                        print(currentError.localizedDescription)
                                    }
                                }
                            } else if ckerror.code == .quotaExceeded {
                                print("quotaExceeded")
                            }
                        }
                    }
                }
            })
            .store(in: &disposables)
    }
}

I am testing the same thing with a user that has a maxed out iCloud storage.

The eventChangedNotification received only mentions a partialFailure without any CKPartialErrorsByItemIDKey so there doesn't seem to be a way to come to the conclusion that the issue is with the storage quota being exceeded just by listening those notifications, is there? Is the only way trying to manually write to cloudkit and analysing the error received? If so, why is there a .quotaExceeded code for the notifications?

*** debugDescription print of the notification: NSPersistentCloudKitContainer.eventChangedNotification received: name = NSPersistentCloudKitContainerEventChangedNotification, object = Optional(<***>), userInfo = Optional([AnyHashable("event"): <NSPersistentCloudKitContainerEvent: 0x300d186e0> { type: Export store: C947EFBD-BE17-49EF-807B-087A62EBF0DE started: 2024-06-13 01:39:23 +0000 ended: 2024-06-13 01:39:28 +0000 succeeded: NO error: CKErrorDomain:2 }])

*** debug output from console: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _recoverFromPartialError:forStore:inMonitor:] 2812: <NSCloudKitMirroringDelegate: 0x301114870>: Error recovery failed because the following fatal errors were found: { "<CKRecordID: 0x302e99dc0; recordName=61920C45-377E-4E1F-BD51-8E4E0D290B59, zoneID=com.apple.coredata.cloudkit.share.3DA846A5-15EC-409D-9160-E1764FFA74BC:defaultOwner>" = "<CKError 0x302090f90: "Quota Exceeded" (25/2035); server message = "Quota exceeded"; op = ADC86A29FF3777F8; uuid = 7C016545-FF26-4B11-B475-A15B770DDC25; Retry after 344.0 seconds; container ID = "xxxx">"; [...]

How to get notified on CKError.quotaExceeded
 
 
Q