Disable SwiftData CloudKit sync when iCloud account is unavailable

I have a widely-used app that lets users keep track of personal data. This data is persisted with SwiftData, and synced with CloudKit.

I understand that if the user's iCloud account changes on a device (for example, user logs out or toggles off an app's access to iCloud), then NSPersistentCloudKitContainer will erase the local data records on app launch. This is intentional behavior, intended as a privacy feature.

However, we are receiving regular reports from users for whom the system has incorrectly indicated that the app's access to iCloud is unavailable, even when the user hasn't logged out or toggled off permission to access iCloud. This triggers the behavior to clear the local records, and even though the data is still available in iCloud, to the user, it looks like their data has disappeared for no reason. Helping the user find and troubleshoot their iCloud app data settings can be very difficult, since in many cases the user has no idea what iCloud is, and we can't link them directly to the correct settings screen.

We seem to get these reports most frequently from users whose iCloud storage is full (which feels like punishment for not paying for additional storage), but we've also received reports from users who have enough storage space available (and are logged in and have the app's iCloud data permissions toggled on). It appears to happen randomly, as far as we can tell.

I found a blog post from two years ago from another app developer who encountered the same issue: https://crunchybagel.com/nspersistentcloudkitcontainer/#:~:text=The%20problem%20we%20were%20experiencing

To work around this and improve the user experience, we want to use CKContainer.accountStatus to check if the user has an available iCloud account, and if not, disable the CloudKit sync before it erases the local data.

I've found steps to accomplish this workaround using CoreData, but I'm not sure how to best modify the ModelContainer's configuration after receiving the CKAccountStatus when using SwiftData. I've put together this approach so far; is this the right way to handle disabling/enabling sync based on account status?

import SwiftUI
import SwiftData
import CloudKit

@main
struct AccountStatusTestApp: App {
    @State private var modelContainer: ModelContainer?

    var body: some Scene {
        WindowGroup {
            if let modelContainer {
                ContentView()
                    .modelContainer(modelContainer)
            } else {
                ProgressView("Loading...")
                    .task {
                        await initializeModelContainer()
                    }
            }
        }
    }

    func initializeModelContainer() async {
        let schema = Schema([
            Item.self,
        ])
        
        do {
            let accountStatus = try await CKContainer.default().accountStatus()
            let modelConfiguration = ModelConfiguration(
                schema: schema,
                cloudKitDatabase: accountStatus == .available ? .private("iCloud.com.AccountStatusTestApp") : .none
            )
            
            do {
                let container = try ModelContainer(for: schema, configurations: [modelConfiguration])
                modelContainer = container
            } catch {
                print("Could not create ModelContainer: \(error)")
            }
        } catch {
            print("Could not determine iCloud account status: \(error)")
        }
    }
}

I understand that bypassing the clearing of local data when the iCloud account is "unavailable" introduces possible issues with data being mingled on shared devices, but I plan to mitigate that with warning messages when users are in this state. This would be a far more preferable user experience than what's happening now.

To work around this and improve the user experience, we want to use CKContainer.accountStatus to check if the user has an available iCloud account, and if not, disable the CloudKit sync before it erases the local data.

This is fine but doesn't completely solve your problem: While your app is running, a user can launch Settings.app and log out their iCloud account or turn off iCloud for your app, which still triggers an account change, but your app can't respond because it only checks the account availability at the beginning of a launch session.

You can probably observe the CKAccountChanged notification, but then there may have a race – NSPersistentCloudKitContainer may still erase the data before you release the current model container.

However, we are receiving regular reports from users for whom the system has incorrectly indicated that the app's access to iCloud is unavailable, even when the user hasn't logged out or toggled off permission to access iCloud. This triggers the behavior to clear the local records, and even though the data is still available in iCloud, to the user, it looks like their data has disappeared for no reason. ... We seem to get these reports most frequently from users whose iCloud storage is full (which feels like punishment for not paying for additional storage), but we've also received reports from users who have enough storage space available (and are logged in and have the app's iCloud data permissions toggled on). It appears to happen randomly, as far as we can tell.

This sounds like an issue worth further investigations. Next time when seeing the issue, you might try to capture and analyze a sysdiagnose to figure out the root cause. Capture and analyze a sysdiagnose covers how to do that.

If you do confirm that NSPersistentCloudKitContainer erases the data without a good reason, I’d suggest that you file a feedback report with the sysdiagnose and your analysis. See Provide actionable feedback for the information we need for our investigation.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Disable SwiftData CloudKit sync when iCloud account is unavailable
 
 
Q