Teabyte

Mobile app development for iOS and Swift

A simple generic UIPickerDataSource

2019-07-07

Recently I had to deal with a lot of UIPicker views in an app for a customer. In order to reduce the amount of code to write I tried to create a generic way of setting up a picker view which can represent any kind of data. There are several posts which pop up when searching for a generic UIPickerDataSource but I wanted to mix it up with something on my own. So let's start!

The Generic Data Source

At first create a GenericRow<T> struct which represents one row inside the UIPickerView.

struct GenericRow<T> {
    let type: T
    let title: String
    public init(type: T, title: String) {
        self.type = type
        self.title = title
    }
}

It owns type, the data for one row of the picker and has a title, the text to be displayed inside the picker view to represent the type.

Let us utilize the GenericRow<T> inside a GenericPickerDataSource<T> class.

class GenericPickerDataSource<T>: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
 
    public var originalItems: [T]
    public var items: [GenericRow<T>]
    public var selected: (T) -> Void
 
  // snip
}

Declare three attributes:

  • originalItems, which stores a list of the original data items this data source will represent
  • items, which stores a list of GenericRow<T>
  • selected, a callback which is executed when the user selects one item from the picker

Now we implement the initializer our generic data source.

public init(withItems originalItems: [T], withRowTitle generateRowTitle: (T) -> String, didSelect selected: @escaping (T) -> Void) {
    self.originalItems = originalItems
    self.selected = selected
 
    self.items = originalItems.map {
        GenericRow<T>(type: $0, title: generateRowTitle($0))
    }
}

It takes three arguments:

  • the original data items to be selectable
  • a function which generates a string based on the type of the data items
  • a callback function which is executed when the user selects an item

generateRowTitle() is used to create GenericRow<T> instances. It uses the result of the method as it's title, which will later be displayed to the user inside the picker view.

The last step for the implementation is to make GenericPickerDataSource<T> conform to the protocols.

func numberOfComponents(in pickerView: UIPickerView) -> Int {
    return 1
}
 
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    return items.count
}
 
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
    return items[row].title
}
 
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    self.selected(items[row].type)
}

UITextField Extensions

In order to further simplify the creation of a picker view, declare two extensions on UITextField.

extension UITextField {
 
    func setupPickerField<T>(withDataSource dataSource: GenericPickerDataSource<T>) {
 
        let pickerView = UIPickerView()
        self.inputView = pickerView
        self.addDoneToolbar()
 
        pickerView.delegate = dataSource
        pickerView.dataSource = dataSource
    }
 
    func addDoneToolbar(onDone: (target: Any, action: Selector)? = nil) {
 
        let onDone = onDone ?? (target: self, action: #selector(doneButtonTapped))
 
        let toolbar: UIToolbar = UIToolbar()
        toolbar.barStyle = .default
        toolbar.tintColor = UIColor.blue
 
        toolbar.items = [
            UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: self, action: nil),
            UIBarButtonItem(title: "Done", style: .done, target: onDone.target, action: onDone.action)
        ]
        toolbar.sizeToFit()
        self.inputAccessoryView = toolbar
    }
 
    // Default actions:
    @objc private func doneButtonTapped() { self.resignFirstResponder() }
}

setupPickerField takes a GenericPickerDataSource and sets up the picker view for us. addDoneToolbar creates a toolbar above the input view of the text field and adds a "done" button to it which will close the view on click.

Use it

Now everything is ready to be used. Assume you want to have a picker where users need to select a country. First create a data object which holds the data:

struct Country {
    var id: Int
    var name: String
}

Create a GenericPickerDataSource<Country> from a list of countries. Specify how the text, which is displayed inside the picker view, is generated and the callback which is executed when the user selects a country.

class ViewController: UIViewController {
 
    @IBOutlet weak var countryTextField: UITextField!
 
    var dataSource: GenericPickerDataSource<Country>?
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
        let countries =  [Country(id: 0, name: "Norway"),
                          Country(id:1, name: "Sweden"),
                          Country(id: 2, name: "Finland")]
 
        self.dataSource = GenericPickerDataSource<Country>(
            withItems: countries,
            withRowTitle: { (country) -> String in
                return country.name
            },
            didSelect: { (country) in
                print("Country \(country) was selected !")
                self.countryTextField.text = country.name
            }
        )
        countryTextField.setupPickerField(withDataSource: dataSource!)
    }
}

When you run the code and tap on the text field you can select the country of your choice.

Define File's Owner class

Conclusion

With an initial set up of a generic data source we have created an easy way of declaring picker views which can accept any kind of data throughout our application. This is especially helpful if your app is dealing with a lot of UIPickerViews.

One thing to note here, is that the current implementation only supports one section inside the picker. We can think about improving the implementation by supporting more than one section. But this is something we could further elaborate in the future.