Earlier this week, I showed you how easy it is to work with bitmasks using Swift and the Swift standard library. In today's episode, I 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 are finished.

How to Create a Custom Control Using a Bitmask

Setting Up the Project In Xcode

Fire up Xcode and create a new project by choosing the App template from the iOS > Application section.

Setting Up the Project In Xcode

Name the project Schedules and set Interface to Storyboard.

Setting Up the Project In Xcode

Creating a UIControl Subclass

The custom control we are about to create is going to be a UIControl subclass. That is a good starting point since we get some 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 prefix the class declaration with the final keyword. Below the import statement for the UIKit framework, add the definition of the Schedule structure we created in the previous episode.

import UIKit

import Foundation

struct Schedule: OptionSet {

    // MARK: - Properties

    let rawValue: Int

    // MARK: - Options

    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]

}

final 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 named schedule of type Schedule. The property's initial value is empty. We covered this in the previous episode.

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

final 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, variable property, buttons, an array of UIButton instances.

final class SchedulePicker: UIControl {

    // MARK: - Properties

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

    // MARK: -

    private var buttons: [UIButton] = []

}

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

final 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 is 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()

    setupView()
}

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

// MARK: - View Methods

private func setupView() {
    setupButtons()
}

We implement one more 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 have 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(_:) method is invoked.

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

        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
        )

        buttons.append(button)
    }

    let stackView = UIStackView(arrangedSubviews: buttons)

    addSubview(stackView)

    stackView.spacing = 8.0
    stackView.axis = .horizontal
    stackView.alignment = .center
    stackView.distribution = .fillEqually
    stackView.translatesAutoresizingMaskIntoConstraints = false

    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 tapped. We ask the buttons array for the index of the sender object. We need the index to figure out which button the user 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.firstIndex(of: sender) else {
        return
    }
}

We declare a constant named 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.

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

    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.

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

    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
    }

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

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

    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
    }

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

    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 the sendActions(for:) method.

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

    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
    }

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

    updateButtons()

    sendActions(for: .valueChanged)
}

Putting the Schedule Picker to Work

Open ViewController.swift and declare an outlet named 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()

    setupView()
}

// MARK: - View Methods

private func setupView() {
    setupSchedulePicker()
}

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

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

    schedulePicker.schedule = Schedule(rawValue: scheduleRawValue)
}

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

fileprivate 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's defaults database.

// MARK: - Actions

@IBAction func scheduleDidChange(_ sender: SchedulePicker) {
    UserDefaults.standard.set(
        sender.schedule.rawValue,
        forKey: UserDefaults.Keys.schedule
    )
}

Creating the User Interface

Open Main.storyboard, open the Object Library on the right by clicking the + button, 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. With the view controller selected, connect the scheduleDidChange(_:) action to the schedule picker, choosing Value Changed from the contextual menu.

Creating the User Interface

Build and Run

Build and run the application to give the schedule picker a try. This episode 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's defaults database.