We can use swift enums, generics and protocols to easily let users select one value from many.
Consider allowing a user to select a colour from a predetermined list of options. We use an enum named Color
to represent the possible options.
enum Color {
case red
case orange
case yellow
case green
case blue
case indigo
case violet
}
To present this choice in a table view controller we need:
Translated into protocol form:
protocol UserSelectable: Equatable {
static var userSelectionTitle: String { get }
static var userSelectionOptions: [Self] { get }
var userSelectionName: String { get }
}
Now we can make our Color
enum conform to our UserSelectable
protocol. We also need to make Color
conform to equatable, which we can do easily by giving it a raw value.
enum Color: String {
case red
case orange
case yellow
case green
case blue
case indigo
case violet
}
extension Color: UserSelectable {
static var userSelectionTitle: String = "Colour"
static var userSelectionOptions: [Color] = [.red, .orange, .yellow, .green, .blue, .indigo, .violet]
var userSelectionName: String {
switch self {
case .red: return "Red"
case .orange: return "Orange"
case .yellow: return "Yellow"
case .green: return "Green"
case .blue: return "Blue"
case .indigo: return "Indigo"
case .violet: return "Violet"
}
}
}
Our table view controller should:
UserOptions
Using the power of generics and our newly defined protocol, we can meet the above criteria with a UserSelectionController
that only requires the current selection and a closure that will be called on selection.
class UserSelectionController<T: UserSelectable>: UITableViewController {
let reuseIdentifier: String = "optionCell"
var selection: T
let onSelect: (T) -> ()
required init?(coder aDecoder: NSCoder) { fatalError("Not implemented") }
init(selection: T, onSelect: @escaping (T) -> ()) {
self.selection = selection
self.onSelect = onSelect
super.init(style: .grouped)
self.title = T.userSelectionTitle
}
// MARK: table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return T.userSelectionOptions.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) {
return cell
} else {
return UITableViewCell(style: .default, reuseIdentifier: reuseIdentifier)
}
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let option = T.userSelectionOptions[indexPath.row]
cell.textLabel?.text = option.userSelectionName
cell.accessoryType = option == selection ? .checkmark : .none
}
// MARK: table view delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// remove checkmark for the old selected cell
if let oldSelectionIndex = T.userSelectionOptions.index(of: selection) {
let oldSelectionIndexPath = IndexPath(row: oldSelectionIndex, section: 0)
tableView.cellForRow(at: oldSelectionIndexPath)?.accessoryType = .none
}
// update the selection
let option = T.userSelectionOptions[indexPath.row]
selection = option
onSelect(selection)
// add a checkmark to the selected row and deselect it
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
tableView.deselectRow(at: indexPath, animated: true)
}
}
Presenting this view controller is straightforward.
let vc = UserSelectionController(selection: Color.blue, onSelect: { newColor in
print(newColor)
})
navigationController?.pushViewController(vc, animated: true)
This approach is great for settings screens and quick prototypes where fancy user interfaces are not needed.