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.
Setting Up the Project In Xcode
Fire up Xcode and create a new project by choosing the App template from the iOS > Application section.
Name the project Schedules and set Interface to Storyboard.
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.
Name the class SchedulePicker and set Subclass of to UIControl.
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.
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.
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.