User Selections using Swift Enums

2018-02-01

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:

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.