How to Make a Custom Control Reactive With RxSwift

Earlier this week, I showed you how easy it is to work with bitmasks using Swift and the Swift standard library. In today's tutorial, I'd like to show you how to create a custom control that uses the Schedule structure we created earlier. This is what the result will look like when we're finished.

How to Create a Custom Control Using a Bitmask

Setting Up the Project In Xcode

Fire up Xcode, create a new project, and choose the Single View Application template from the iOS > Application section.

Setting Up the Project In Xcode

Name the project Schedules and set Language to Swift.

Configuring the Project In Xcode

Creating a UIControl Subclass

The custom control we're about to create is going to be a UIControl subclass. That's a good starting point since we get several pieces of functionality for free. Create a new file and choose the Cocoa Touch Class template from the iOS > Source section.

Creating a UIControl Subclass

Name the class SchedulePicker and set Subclass of to UIControl.

Creating a UIControl Subclass

Open SchedulePicker.swift and, below the import statement for the UIKit framework, add the definition of the Schedule structure we created in the previous tutorial.

import UIKit

struct Schedule: OptionSet {

    let rawValue: Int

    static let monday       = Schedule(rawValue: 1 << 0)
    static let tuesday      = Schedule(rawValue: 1 << 1)
    static let wednesday    = Schedule(rawValue: 1 << 2)
    static let thursday     = Schedule(rawValue: 1 << 3)
    static let friday       = Schedule(rawValue: 1 << 4)
    static let saturday     = Schedule(rawValue: 1 << 5)
    static let sunday       = Schedule(rawValue: 1 << 6)

    static let weekend: Schedule    = [.saturday, .sunday]
    static let weekdays: Schedule   = [.monday, .tuesday, .wednesday, .thursday, .friday]

}

class SchedulePicker: UIControl {

}

The implementation of the SchedulePicker class is no rocket science. Let's take a look at the details.

Defining Properties

The value of the UIControl subclass is of type Schedule. This shouldn't be a surprise since the purpose of the control is to allow users to choose a schedule. We define a property, schedule, of type Schedule. The property's default value is an empty array. We covered this in the previous tutorial.

We also implement a didSet property observer. In the property observer, we invoke a helper method, updateView(), which we implement shortly.

class SchedulePicker: UIControl {

    // MARK: - Properties

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

}

The schedule picker displays seven buttons, a button for each day of the week. We store references to these buttons in a private helper property, buttons, an array of UIButton instances.

class SchedulePicker: UIControl {

    // MARK: - Properties

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

    // MARK: -

    private var buttons: [UIButton] = []

}

Before we continue, I'd like to show you a neat trick I frequently use to namespace constants. I declare an enumeration without cases, Color, and define three static constant properties. In these properties, I store the colors used by the SchedulePicker class. Notice that the Color enum is private and nested within the SchedulePicker class.

class SchedulePicker: UIControl {

    // MARK: - Properties

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

    // MARK: -

    private var buttons: [UIButton] = []

    // MARK: -

    private enum Color {

        static let selected = UIColor.white
        static let tint = UIColor(red:1.0, green:0.37, blue:0.36, alpha:1.0)
        static let normal = UIColor(red:0.2, green:0.25, blue:0.3, alpha:1.0)

    }

}

Setting Up the View

It's time to set up the schedule picker. We override the awakeFromNib() method and invoke a helper method, setupView().

// MARK: - Overrides

override func awakeFromNib() {
    super.awakeFromNib()

    // Setup View
    setupView()
}

In setupView(), we invoke another helper method, setupButtons(). You may have noticed that I prefer to keep methods short and simple.

// MARK: - View Methods

private func setupView() {
    setupButtons()
}

We also implement another helper method, updateView(), in which we invoke updateButtons(). Remember that updateView() is invoked in the didSet property observer of the schedule property.

private func updateView() {
    updateButtons()
}

The implementation of setupButtons() should look familiar if you've worked with Auto Layout in code. We create a button for each day of the week, configure it, and add it to a stack view. When the user taps one of the buttons, the toggleSchedule(_:) function is triggered.

// MARK: -

private func setupButtons() {
    // Create Button
    for title in [ "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" ] {
        // Initialize Button
        let button = UIButton(type: .system)

        // Configure Button
        button.setTitle(title, for: .normal)

        button.tintColor = Color.tint
        button.setTitleColor(Color.normal, for: .normal)
        button.setTitleColor(Color.selected, for: .selected)

        button.addTarget(self, action: #selector(toggleSchedule(_:)), for: .touchUpInside)

        // Add to Buttons
        buttons.append(button)
    }

    // Initialize Stack View
    let stackView = UIStackView(arrangedSubviews: buttons)

    // Add as Subview
    addSubview(stackView)

    // Configure Stack View
    stackView.spacing = 8.0
    stackView.axis = .horizontal
    stackView.alignment = .center
    stackView.distribution = .fillEqually
    stackView.translatesAutoresizingMaskIntoConstraints = false

    // Add Constraints
    topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
    bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
    leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
    trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true
}

The implementation of updateButtons() is more interesting. In this method, we update the buttons based on the value stored in the schedule property.

private func updateButtons() {
    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)
}

Implementing the Action

The last piece of the puzzle is implementing the toggleSchedule(_:) action. The sender that is passed to the method is the button the user has tapped. We ask the buttons array for the index of the sender object. We need the index to figure out which button was tapped and how to update the schedule. We could store the index in the tag property of each button, but this works just fine.

// MARK: - Actions

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

We declare a temporary constant, element, of type Schedule.Element and use a switch statement to assign a value to the element constant based on the button the user tapped.

// Helpers
let element: Schedule.Element

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
}

We update the schedule using the value stored in element. If the schedule property already contains element, we remove element from schedule. If the schedule property doesn't contain element, we insert it. This means the user can toggle a day in the schedule by tapping one of the buttons.

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

We update the buttons to reflect the new state of the schedule picker by invoking updateButtons().

// Update Buttons
updateButtons()

Last but not least, we invoke sendActions(for:) to notify any registered targets that the value of the schedule picker has changed. We pass .valueChanged as the argument of sendActions(for:).

// Send Actions
sendActions(for: .valueChanged)

Putting the Schedule Picker to Work

Open ViewController.swift and declare an outlet, schedulePicker, of type SchedulePicker.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var schedulePicker: SchedulePicker!

}

In viewDidLoad(), we invoke a helper method, setupView(), in which we invoke another helper method, setupSchedulePicker().

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Setup View
    setupView()
}

// MARK: - View Methods

fileprivate func setupView() {
    // Setup Schedule Picker
    setupSchedulePicker()
}

I'd like to show you how to easily store a bitmask as an integer in the user defaults database. In setupSchedulePicker(), we fetched the stored schedule as an integer from the user defaults database. We use the value to create a Schedule instance and set the schedule property of the schedulePicker outlet.

// MARK: - 

fileprivate func setupSchedulePicker() {
    // Fetch Stored Value
    let scheduleRawValue = UserDefaults.standard.integer(forKey: UserDefaults.Keys.schedule)

    // Configure Schedule Picker
    schedulePicker.schedule = Schedule(rawValue: scheduleRawValue)
}

I use namespacing to clean up the constants we use. Add this extension below the implementation of the ViewController class. In a larger project, I'd put the extension in a file of its own.

extension UserDefaults {

    enum Keys {

        static let schedule = "schedule"

    }

}

Because the SchedulePicker class is a UIControl class, we can easily wire an action to the .valueChanaged event. We do this in Interface Builder. The action triggered by the .valueChanged event is scheduleDidChange(_:). We ask the sender, an instance of the SchedulePicker class, for its schedule and store the raw value in the user defaults database.

// MARK: - Actions

@IBAction func scheduleDidChange(_ sender: SchedulePicker) {
    // Helpers
    let userDefaults = UserDefaults.standard

    // Store Value
    let scheduleRawValue = sender.schedule.rawValue
    userDefaults.set(scheduleRawValue, forKey: UserDefaults.Keys.schedule)
    userDefaults.synchronize()
}

The synchronize() call is optional. This can be useful during development for reasons I won't go into in this tutorial.

Creating the User Interface

Open Main.storyboard, open the Object Library on the right, and add an empty view to the view of the ViewController instance. With the view selected, open the Identity Inspector on the right and set Class to SchedulePicker.

Creating the User Interface

Select the view controller of the View Controller Scene, open the Connections Inspector on the right, and connect the schedulePicker outlet to the SchedulePicker instance you added a moment ago.

Creating the User Interface

With the view controller selected, connect the scheduleDidChange(_:) action to the schedule picker, choosing Value Changed from the menu that pops up.

Creating the User Interface

Creating the User Interface

Creating the User Interface

Build and Run

Build and run the application to give the schedule picker a try. This tutorial not only taught you how to create a custom control by subclassing the UIControl class. It also taught you how to use bitmasks and store them, for example, in the user defaults database.

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

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By

About Bart Jacobs

About bart jacobs

My name is Bart Jacobs and I run a mobile development company, Code Foundry. I've been programming for more than fifteen years, focusing on Cocoa development soon after the introduction of the iPhone in 2007.

Stop Writing Swift That Sucks

In my free book, you learn the four patterns I use in every Swift project I work on. You learn how easy it is to integrate these patterns in any Swift project.