I created a reusable control to be used in a project I'm working on. It's just a UITextField
which shows a UIPickerView
as its inputView
.
class InputPickerView: UIView {
@IBOutlet private var view: UIView!
@IBOutlet weak private var titleLabel: UILabel!
@IBOutlet weak private var textField: UITextField!
private(set) var pickerView = UIPickerView()
var options: [String] = []
var option: String {
get {
return textField.text ?? ""
}
set {
textField.text = newValue
}
}
var title: String = "" {
didSet {
titleLabel.text = title
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("InputPickerView", owner: self, options: nil)
addSubview(view)
view.frame = bounds
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
pickerView.dataSource = self
pickerView.delegate = self
textField.inputView = pickerView
}
}
extension InputPickerView: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return false
}
}
extension InputPickerView: UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return options.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return options[row]
}
}
extension InputPickerView: UIPickerViewDelegate {
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
textField.text = options[row]
}
}
Currently it only accepts an array of strings and returns a string. I'm trying to make it even more reusable by make it accept/return any types such as structs and enums with the help of generics. I was hoping to make the structs/enums conform to CustomStringConvertible
and use the description property value as the display value for the picker view options.
But I'm having trouble figuring out how to do that. All the articles, questions, tutorials I came across have protocols involved in them. So I'm a little confused.
How can I make the options
and option
variables accept/return any type with generics?
By which I mean, say I create an object called State
.
struct State {
let id: Int
let title: String
}
extension State: CustomStringConvertible {
var description: String {
return title
}
}
Instead of passing in strings to the view, I'm trying to make it accept instances of State
objects in the options
property and have the view use the description
value as the display value. And when the user selects one, it returns the selected State
object via the option
property.
First you need a protocol that extracts a string from your conforming types to display in the picker:
protocol Presentable {
var title: String { get }
}
Make your State
struct conform to Presentable
:
struct State: Presentable {
let id: Int
let title: String
}
Add some generic constraints to your InputPickerView
and whenever you need the text from your model just reference the title
property. Note that if you use generics you can no longer create extensions for your UIPickerViewDataSource
and UIPickerViewDelegate
methods.
class InputPickerView<OptionType: Presentable>: UIView, UIPickerViewDataSource, UIPickerViewDelegate {
private var titleLabel: UILabel!
private var textField: UITextField!
var options: [OptionType] = []
var selectedOption: OptionType?
var title: String = "" {
didSet {
titleLabel.text = title
}
}
// ... Other stuff you need to add ...
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return options.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return options[row].title
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
textField.text = options[row].title
selectedOption = options[row]
}
}
You create your InputPickerView
like this:
let pickerView = InputPickerView<State>()
pickerView.options = [
State(id: 1, title: "First"),
State(id: 2, title: "Second"),
State(id: 3, title: "Third"),
]