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.
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.
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.