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.