Handling Account Status Changes With CloudKit

Handling Account Status Changes With CloudKit

It's easy to forget that a user can only benefit from iCloud and its many features if they have an Apple ID and if they're signed in on their device. While both requirements are usually met, your application needs to be capable of handling scenarios in which the user isn't signed in.

A user who isn't signed in with their Apple ID won't be able to take advantage of every feature iCloud and CloudKit have to offer. The container of a CloudKit application contains three database types, a private database, a public database, and a shared database. A user that isn't signed in to iCloud is limited to reading the data stored in the public database.

That may be fine for some applications. However, if your application is only useful if the user is signed in to their iCloud account and, as a result, has access to its private (and shared) database, then it's key that your application knows whether the user is signed or not.

In this tutorial, I show you how to check the account status of the current user. We also explore what's needed to respond to account status changes.

First Things First

Before we can start playing with CloudKit, we need a project. I'll be using the project I created in the previous tutorial of this series. If you'd like to follow along, I recommend cloning the project from GitHub or following the steps I outlined in the previous tutorial.

If you decide to clone the project from GitHub, make sure to change the bundle identifier. Every project that uses CloudKit needs a unique App ID with a unique bundle identifier. App ID? Bundle identifier? You can read more about App IDs and bundle identifiers in this tutorial.

Requesting the User's Account Status

Requesting the user's account status is quite easy. Before I show you how this works, we need to do some housekeeping. We're going to create a class that handles the interactions with the CloudKit framework. The view controller shouldn't be burdened with such tasks.

Create a new group in the Project Navigator and name it Managers. Create a new Swift file in this group, name it CloudKitManager.swift, and define a class named CloudKitManager. Replace the import statement for Foundation with an import statement for the CloudKit framework.

import CloudKit

class CloudKitManager {

}

We create an instance of the CloudKitManager class in the view controller. Clean up the viewDidLoad() method and remove the import statement for the CloudKit framework.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    private let cloudKitManager = CloudKitManager()

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

Revisit the CloudKitManager class and define two stored properties. The first property, container, is of type CKContainer and it holds a reference to the default container of the application. The second property, accountStatus, is of type CKAccountStatus. This property stores the user's current account status. We initialize the accountStatus property with a default value of couldNotDetermine, which means that we don't know what the user's account status is. That seems appropriate.

Notice that the setter of the accountStatus property is declared private. Other objects can read the value of the accountStatus property, but only the CloudKit manager should be allowed to modify its value.

import CloudKit

class CloudKitManager {

    // MARK: - Properties

    private let container = CKContainer.default()

    // MARK: -

    private(set) var accountStatus: CKAccountStatus = .couldNotDetermine

}

In the initializer of the CloudKitManager class, we invoke a helper method, requestAccountStatus(). This is where the magic happens as you can see below.

import CloudKit

class CloudKitManager {

    // MARK: - Properties

    private let container = CKContainer.default()

    // MARK: -

    private(set) var accountStatus: CKAccountStatus = .couldNotDetermine

    // MARK: - Initialization

    init() {
        // Request Account Status
        requestAccountStatus()
    }

    // MARK: - Helper Methods

    private func requestAccountStatus() {
        // Request Account Status
        container.accountStatus { [unowned self] (accountStatus, error) in
            // Print Errors
            if let error = error { print(error) }

            // Update Account Status
            self.accountStatus = accountStatus
        }
    }

}

We request the user's account status by invoking accountStatus(_:) on the default container, which we access through the container property. The accountStatus(_:) method accepts one argument, a closure. The closure accepts two arguments, a CKAccountStatus instance and an optional error. The CKAccountStatus type is an enum that defines four cases:

  • couldNotDetermine
  • available
  • restricted
  • noAccount

Something went wrong if the value of accountStatus is equal to couldNotDetermine. The error that is passed to the closure tells us why we're not able to determine the user's account status. The available and noAccount cases are self-explanatory. The restricted case requires some clarification, though.

It's possible that the user is signed in with their Apple ID, but your application isn't granted access to the user's iCloud data. In the documentation, Apple explains that this can be the result of Parental Controls or Mobile Device Management restrictions.

In Scribbles, we store the account status in the accountStatus property. If you want to take it one step further, you could add a dash of reactive programming to the mix by creating an observable sequence of type CKAccountStatus. Any object interested in the user's account status can subscribe to this observable sequence.

Account Status Changes

It's possible for the account status to change, for example, when the user sign out. Your application needs to be able to respond to such an event. Don't worry. This doesn't mean that you need to periodically check the account status of the user.

When the account status changes, a CKAccountChanged notification is posted by an instance of the CKContainer class. If no instance is alive when the account status changes, no notification is posted. That's why I recommend that you keep a reference to the default container. In Scribbles, the CloudKitManager class keeps a reference to the default container in its container property.

Adding an Observer

In the initializer of the CloudKitManager class, we invoke a helper method, setupNotificationHandling().

// MARK: - Initialization

init() {
    // Request Account Status
    requestAccountStatus()

    // Setup Notification Handling
    setupNotificationHandling()
}

In setupNotificationHandling(), we add the CloudKit manager as an observer of CKAccountChanged notifications.

// MARK: -

fileprivate func setupNotificationHandling() {
    // Helpers
    let notificationCenter = NotificationCenter.default
    notificationCenter.addObserver(self, selector: #selector(accountDidChange(_:)), name: Notification.Name.CKAccountChanged, object: nil)
}

Notice that the last parameter of addObserver(_:selector:name:object:) is nil, which means that the CloudKit manager is interested in any CKAccountChanged notification that is posted. It may seem obvious to pass a reference to the default container as the last parameter. However, if you do, then you won't receive a notification when the user's account status changes.

Responding to Account Status Changes

In the accountDidChange(_:) method, we invoke requestAccountStatus() to ask the default container for the new account status of the user.

// MARK: - Notification Handling

@objc private func accountDidChange(_ notification: Notification) {
    // Request Account Status
    DispatchQueue.main.async { self.requestAccountStatus() }
}

Notice that we invoke requestAccountStatus() on the main queue since the CKAccountChanged notification isn't guaranteed to be posted on the main queue. This isn't strictly necessary in this example, but keep this detail in mind if you plan to update the user interface in response to an account status change.

Background and Foreground

You may be wondering how your application can respond to a CKAccountChanged notification, for example, if the user signs out and your application isn't in the foreground. If your application is running at the time the user signs out, then it receives the CKAccountChanged notification as soon as it enters the foreground. Your application can then request the account status and respond accordingly.

Adding a Dash of Reactive Programming

In the next tutorial, we take a detour and add reactive programming to the mix. Reactive programming makes it easier for your application to respond to changes, such as when the user sign in or signs out.

You can find the source files of this tutorial on GitHub.