Earlier this week, I showed you how to create a custom control using a bitmask. In this episode, we take it a step further by making the custom control we built reactive using RxSwift.

Adding Dependencies Using the Swift Package Manager

Fire up Xcode and open the starter project of this episode if you want to follow along with me. You can add RxSwift one of several ways. I prefer the Swift Package Manager. Select the project in the Project Navigator. Select the project in the Project section and open the Package Dependencies tab at the top.

Adding Dependencies Using the Swift Package Manager

Click the + button below the table view that lists the project's dependencies. Enter the URL of the RxSwift repository on GitHub. Set Dependency Rule to Up to Next Major Version with the lower bound set to 6.0.0. Click the Add Package button to continue.

Adding Dependencies Using the Swift Package Manager

Check the checkboxes of RxCocoa, RxRelay, and RxSwift. Click Add Package to add the package products to the project.

Adding Dependencies Using the Swift Package Manager

You should see RxSwift listed in the Package Dependencies section in the Project Navigator on the left.

Drivers and Behavior Relays

The technique I outline in this episode is a technique I picked up from Krunoslav Zaher, the creator of RxSwift. Open SchedulePicker.swift and add import statements for the RxSwift and RxCocoa libraries.

import UIKit
import RxSwift
import RxCocoa
import Foundation

struct Schedule: OptionSet {

    ...
    
}

Locate the schedule property we defined in the previous episode.

final class SchedulePicker: UIControl {

    // MARK: - Properties

    var schedule: Schedule = [] {
        didSet { updateView() }
    }

    ...
    
}

We replace the schedule property with a Driver, making the SchedulePicker class reactive. Subscribers can subscribe to the Driver and be notified when a change occurs.

But we don't simply use a Driver. The approach I have come to appreciate involves a private BehaviorRelay and a Driver the SchedulePicker class exposes. The BehaviorRelay is only used by the SchedulePicker class.

This is what that looks like. The scheduleRelay property is of type BehaviorRelay<Schedule> and it is initialized with the same value the schedule property was initialized with. The computed scheduleDriver property is of type Driver<Schedule> and returns the BehaviorRelay as a driver.

final class SchedulePicker: UIControl {

    // MARK: - Properties

    private let scheduleRelay = BehaviorRelay<Schedule>(value: [])

    var scheduleDriver: Driver<Schedule> {
        scheduleRelay.asDriver()
    }

    ...

}

Under the hood, a BehaviorRelay instance manages a BehaviorSubject instance. The asDriver() method provides read access to the BehaviorSubject. This is what the implementation of the asDriver() method looks like in the RxCocoa library.

import RxSwift
import RxRelay

extension BehaviorRelay {
    /// Converts `BehaviorRelay` to `Driver`.
    ///
    /// - returns: Observable sequence.
    public func asDriver() -> Driver<Element> {
        let source = self.asObservable()
            .observe(on:DriverSharingStrategy.scheduler)
        return SharedSequence(source)
    }
}

Refactoring the SchedulePicker Class

The compiler has generated a few errors we need to take care of. We need to refactor the SchedulePicker class to make it work with the scheduleRelay property. The changes are minor and easy to understand.

In updateButtons(), we ask scheduleRelay for its value and use the Schedule instance to update the buttons of the SchedulePicker instance.

private func updateButtons() {
    let schedule = scheduleRelay.value

    buttons[0].isSelected = schedule.contains(.monday)
    buttons[1].isSelected = schedule.contains(.tuesday)
    buttons[2].isSelected = schedule.contains(.wednesday)
    buttons[3].isSelected = schedule.contains(.thursday)
    buttons[4].isSelected = schedule.contains(.friday)
    buttons[5].isSelected = schedule.contains(.saturday)
    buttons[6].isSelected = schedule.contains(.sunday)
}

In toggleSchedule(_:), we make a similar change. We ask scheduleRelay for its value and update the Schedule instance. Instead of invoking sendActions(for:), we update the value of scheduleRelay by passing the Schedule object to its accept(_:) method.

@IBAction func toggleSchedule(_ sender: UIButton) {
    guard let index = buttons.firstIndex(of: sender) else {
        return
    }

    let element: Schedule.Element
    var schedule = scheduleRelay.value

    switch index {
    case 0: element = .monday
    case 1: element = .tuesday
    case 2: element = .wednesday
    case 3: element = .thursday
    case 4: element = .friday
    case 5: element = .saturday
    default: element = .sunday
    }

    if schedule.contains(element) {
        schedule.remove(element)
    } else {
        schedule.insert(element)
    }

    updateButtons()

    scheduleRelay.accept(schedule)
}

Under the hood, a next event is emitted by the BehaviorSubject instance of the BehaviorRelay instance. This is what the implementation of the accept(_:) method looks like in the RxRelay library.

import RxSwift

/// BehaviorRelay is a wrapper for `BehaviorSubject`.
///
/// Unlike `BehaviorSubject` it can't terminate with error or completed.
public final class BehaviorRelay<Element>: ObservableType {
    private let subject: BehaviorSubject<Element>

    /// Accepts `event` and emits it to subscribers
    public func accept(_ event: Element) {
        self.subject.onNext(event)
    }

	...

}

We also need to refactor the setupButtons() method. After setting up the user interface, the SchedulePicker instance subscribes to the scheduleRelay property. Every time the BehaviorRelay instance emits a value, updateButtons() is invoked.

private func setupButtons() {
    ...
    
    scheduleDriver.drive(
        onNext: { [weak self] schedule in
            self?.updateButtons()
        }
    ).disposed(by: disposeBag)
}

To keep the compiler happy, we declare a private, constant property named disposeBag and use it to store a reference to a DisposeBag instance.

// MARK: -

private let disposeBag = DisposeBag()

This also means that we can remove updateView(). We no longer need it.

Refactoring the ViewController Class

We need to make a few changes to the ViewController class. But before we do, we need to implement a setter method to set the schedule of the SchedulePicker instance. We can no longer set the value of the schedule property.

Add the following method to the SchedulePicker class. There are other solutions, but this works fine. I don't recommend exposing the scheduleRelay property. This solution allows us to keep the scheduleRelay property private.

// MARK: - Public API

func setSchedule(_ newSchedule: Schedule) {
    scheduleRelay.accept(newSchedule)
}

In the setupSchedulePicker() method of the ViewController class, we set the schedule using the setSchedule(_:) method we added to the SchedulePicker class.

private func setupSchedulePicker() {
    let scheduleRawValue = UserDefaults.standard.integer(
        forKey: UserDefaults.Keys.schedule
    )

    schedulePicker.setSchedule(.init(rawValue: scheduleRawValue))
}

Add an import statement for the RxSwift library and define a constant property named disposeBag. We use it to store a reference to a DisposeBag instance.

// MARK: -

private let disposeBag = DisposeBag()

Revisit the setupSchedulePicker() method and subscribe to the scheduleDriver property of the SchedulePicker instance. Every time the schedule is modified, we update the value stored in the user's defaults database.

private func setupSchedulePicker() {
    let scheduleRawValue = UserDefaults.standard.integer(
        forKey: UserDefaults.Keys.schedule
    )

    schedulePicker.setSchedule(.init(rawValue: scheduleRawValue))

    schedulePicker.scheduleDriver.drive(
        onNext: { schedule in
            UserDefaults.standard.set(
                schedule.rawValue,
                forKey: UserDefaults.Keys.schedule
            )
        }
    ).disposed(by: disposeBag)
}

We can now remove the scheduleDidChange(_:) action and unwire the action in Main.storyboard.

What's Next?

Build and run the application to give it a try. You shouldn't see any warnings or errors. It is important to note that RxCocoa uses a different approach to make UIKit components reactive. The advantage of this technique is that it is lightweight and requires very little setup.