Reactifying iCloud Account Status Changes With RxSwift

Reactifying iCloud Account Status Changes With RxSwift

In last week's tutorial, I showed you how your application can respond to account status changes. While this isn't rocket science, it's important for any application that takes advantage of Apple's iCloud services.

If you're familiar with reactive programming, then you may have noticed that there's room for improvement. The CloudKitManager class stores the current account status in a property, accountStatus, of type CKAccountStatus. This isn't very reactive.

One of the joys of reactive programming is ridding a codebase of state. Since the account status can change over time it makes sense to turn the accountStatus property into an observable sequence other objects can subscribe to. An observable sequence is nothing more than a stream of values ordered in time.

Adding Dependencies

Before we can start working with RxSwift and RxCocoa, we need to add these libraries as dependencies. My dependency manager of choice is CocoaPods, but feel free to use Carthage or the Swift Package Manager.

Open Terminal, navigate to the root of the project we created in the previous tutorial, and execute the pod init command. Open the Podfile CocoaPods has created for us and add RxSwift and RxCocoa as dependencies.

target 'Scribbles' do
  platform :ios, '10.0'
  use_frameworks!

  pod 'RxSwift'
  pod 'RxCocoa'
end

Head back to Terminal and execute the pod install command. For this tutorial, I'll be using RxSwift 4.0.0 and RxCocoa 4.0.0. Open the workspace CocoaPods has created for us in Xcode. Build the project to make sure no errors or warnings pop up. We're ready to reactify the CloudKitManager class.

Updating the CloudKit Manager

Open the CloudKitManager class and add import statements for the RxSwift and RxCocoa libraries.

import RxSwift
import RxCocoa
import CloudKit

class CloudKitManager {

    ...

}

Locate the accountStatus property of the CloudKitManager class. The approach I usually take is simple. I define a private property of type BehaviorRelay and an internal or public property of type Observable. This means that the CloudKit manager can manipulate the account status while other objects can only subscribe to the internal or pubic observable sequence. This is what that looks like.

import RxSwift
import RxCocoa
import CloudKit

class CloudKitManager {

    // MARK: - Properties

    private let container = CKContainer.default()

    // MARK: -

    private let _accountStatus = BehaviorRelay<CKAccountStatus>(value: .couldNotDetermine)

    var accountStatus: Observable<CKAccountStatus> { return _accountStatus.asObservable() }

    ...

}

The BehaviorRelay instance is initialized with a default value of CKAccountStatus.couldNotDetermine. Notice that I prefix the private _accountStatus property with an underscore. This may look like an ancient habit, but it allows me to use the same property name for the private and the internal or public property. I find it adds clarity, showing that the properties are linked to one another.

Updating the Account Status

We need to make another small change in the requestAccountStatus() method. We can no longer assign the current account status to the accountStatus property. We emit a new value by passing the current account status to the accept(_:) method of the BehaviorRelay instance.

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

        // Update Account Status
        print(accountStatus.rawValue)
        self._accountStatus.accept(accountStatus)
    }
}

That's it. Because the CloudKit framework isn't reactive, we still need to add the CloudKit manager as an observer of CKAccountChanged notifications. The interface of the CloudKitManager class, however, has become more reactive. Let's see what we gained.

Subscribing to Account Status Changes

Open RootViewController.swift and define an outlet, accountStatusLabel, of type UILabel!.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var accountStatusLabel: UILabel!

    // MARK: -

    private let cloudKitManager = CloudKitManager()

    ...

}

Open Main.storyboard, add a label to the root view controller's view, and define the necessary constraints to center it in its superview. Don't forget to connect the label to the accountStatusLabel outlet we declared earlier.

Adding a Label to the Root View Controller

Revisit the RootViewController class and add import statements for the RxSwift and RxCocoa libraries.

import UIKit
import RxSwift
import RxCocoa

class RootViewController: UIViewController {

    ...

}

Define a private constant property, disposeBag, of type DisposeBag. If you're not familiar with dispose bags, then you need to read up on the fundamentals of RxSwift first.

import UIKit
import RxSwift
import RxCocoa

class RootViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var accountStatusLabel: UILabel!

    // MARK: -

    private let cloudKitManager = CloudKitManager()

    // MARK: -

    private let disposeBag = DisposeBag()

    ...

}

The magic takes place in the next step. In the viewDidLoad() method, the view controller subscribes to the accountStatus observable sequence, converts it to a driver, maps the CKAccountStatus values to String values, and drives the account status label.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Subscribe to Account Status
    cloudKitManager
        .accountStatus
        .asDriver(onErrorJustReturn: .couldNotDetermine)
        .map { (accountStatus) -> String in
            switch accountStatus {
            case .couldNotDetermine: return "Unable to Determine iCloud Account Status"
            case .available: return "User Signed in to iCloud"
            case .restricted: return "Not Permitted to Access iCloud Account"
            case .noAccount: return "User Not Signed in to iCloud"
            }
        }
        .drive(accountStatusLabel.rx.text)
        .disposed(by: disposeBag)
}

Run the application to see the result.

Build and Run

Drivers and Observables

You may be wondering why I didn't define the accountStatus property as a driver. That's a fair question. I tend to use drivers only when they're directly tied to the user interface. That's the reason we convert the observable sequence in the root view controller's viewDidLoad() method to a driver.

It's very likely, though, that other objects are going to be interested in changes of the account status. Those objects may not be tied to the user interface, for example, an object that synchronizes the data stored in iCloud with a local cache.

Don't worry about this too much, though. I showed you how easy it is to convert an observable sequence to a driver and it's just as easy to ask a driver for the observable sequence that powers it.

Transparency

Without reactive programming, we'd have to use another mechanism to keep the user interface of the root view controller synchronized with the current account status, such as delegation, notifications, or key-value observing. RxSwift and RxCocoa make this task almost trivial. You can find the source files of this tutorial on GitHub. Look for the reactify branch.