How to handle first time launch experience when iCloud is required?
Asked Answered
H

3

7

I am using CloudKit to store publicly available data and the new NSPersistentCloudKitContainer as part of my Core Data stack to store/sync private data.

When a user opens my app, they are in 1 of 4 states:

  1. They are a new user with access to iCloud
  2. They are a returning user with access to iCloud
  3. They are a new user but do not have access to iCloud for some reason
  4. They are a returning user but do not have access to iCloud for some reason

States 1 and 2 represent my happy paths. If they are a new user, I'd like to seed the user's private store with some data before showing the initial view. If they are a returning user, I'd like to fetch data from Core Data to pass to the initial view.

Determining new/old user: My plan is to use NSUbiquitousKeyValueStore. My concern with this is handling the case where they: download the app -> are recorded as having launched the app before -> delete and reinstall/install the app on a new device I assume NSUbiquitousKeyValueStore will take some time to receive updates so I need to wait until it has finished synchronizing before moving onto the initial view. Then there's the question of what happens if they don't have access to iCloud? How can NSUbiquitousKeyValueStore tell me if they are a returning user if it can't receive the updates?

Determining iCloud access: Based on the research I've done, I can check if FileManager.default.ubiquityIdentityToken is nil to see if iCloud is available, but this will not tell me why. I would have to use CKContainer.default().accountStatus to learn why iCloud is not available. The issue is that is an asynchronous call and my app would have moved on before learning what their account status is.

I'm really scratching my head on this one. What is the best way to gracefully make sure all of these states are handled?

Herra answered 20/4, 2020 at 5:32 Comment(2)
Did you end up resolving this? How did you solve points 3 and 4? I am currently dealing with the same issues and haven't found a super elegant solution to the user not being connected to iCloud.Montagna
At the moment, I have not found a completely satisfying answer to this problem. As some have suggested, there isn't one singular solution. For the time being, I'm going to approach my app as if the user has access to iCloud.Herra
H
3

to improve whistler's solution on point 3 and 4,

  • They are a new user but do not have access to iCloud for some reason
  • They are a returning user but do not have access to iCloud for some reason

one should use UserDefaults as well, so that it covers offline users and to have better performance by skipping network connections when not needed, which is every time after the first time.

solution

func isFirstTimeUser() async -> Bool {
    if UserDefaults.shared.bool(forKey: "hasSeenTutorial") { return false }
    let db = CKContainer.default().privateCloudDatabase
    let predicate = NSPredicate(format: "CD_entityName = 'Item'")
    let query = CKQuery(recordType: "CD_Container", predicate: predicate)
    do {
        let items = (try await db.records(matching: query)).matchResults
        return items.isEmpty
    } catch {
        return false
        // this is for the answer's simplicity,
        // but obviously you should handle errors accordingly.
    }
}

func showTutorial() {
    print("showing tutorial")
    UserDefaults.shared.set(true, forKey: "hasSeenTutorial")
}

As it shows, after the first time user task showTutorial(), UserDefaults's bool value for key "hasSeenTutorial" is set to true, so no more calling expensive CK... after.

usage

if await isFirstTimeUser() {
    showTutorial()
}
Hyperbole answered 21/6, 2022 at 1:44 Comment(0)
A
2

There's no "correct" answer here, but I don't see NSUbiquitiousKeyValueStore being a win in any way - like you said if they're not logged into iCloud or don't have network access it's not going to work for them anyway. I've got some sharing related stuff done using NSUbiquitiousKeyValueStore currently and wouldn't do it that way next time. I'm really hoping NSPersistentCloudKitContainer supports sharing in iOS 14 and I can just wipe out most of my CloudKit code in one fell swoop.

If your app isn't functional without cloud access then you can probably just put up a screen saying that, although in general that's not a very satisfying user experience. The way I do it is to think of the iCloud sync as truly asynchronous (which it is). So I allow the user to start using the app. Then you can make your call to accountStatus to see if it's available in the background. If it is, start a sync, if it's not, then wait until it is and then start the process.

So the user can use the app indefinitely standalone on the device, and at such time as they connect to the internet everything they've done on any other device gets merged into what they've done on this new device.

Armenta answered 28/4, 2020 at 16:37 Comment(1)
I appreciate the response. I guess this comes down to being okay with the 'eventual consistency' that NSUbiquitousKeyValueStore promises. From my understanding, even if the user doesn't ever have access to iCloud, their preferences will still be stored on disk waiting to sync. Could you share some more details with the problems you've had with using NSUbiquitousKeyValueStore?Herra
I
2

I struggled with this problem as well just recently. The solution I came up with was to query iCloud directly with CloudKit and see if it has been initialized. It's actually very simple:

public func checkRemoteData(completion: @escaping (Bool) -> ()) {
    let db = CKContainer.default().privateCloudDatabase
    let predicate = NSPredicate(format: "CD_entityName = 'Root'")
    let query = CKQuery(recordType: .init("CD_Container"), predicate: predicate)
    db.perform(query, inZoneWith: nil) { result, error in
        if error == nil {
            if let records = result, !records.isEmpty {
                completion(true)
            } else {
                completion(false)
            }
        } else {
            print(error as Any)
            completion(false)
        }
    }
}

This code illustrates a more complex case, where you have instances of a Container entity with a derived model, in this case called Root. I had something similar, and could use the existence of a root as proof that the data had been set up.

See here for first hand documentation on how Core Data information is brought over to iCloud: https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/reading_cloudkit_records_for_core_data

Imperial answered 27/5, 2020 at 13:19 Comment(1)
It is a workable suggestion and helped me to solve a similar issue, thank youPlod

© 2022 - 2024 — McMap. All rights reserved.