Search code examples
iosswiftgenericsuiviewcustomstringconvertible

How to make a reusable view accept generic types


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.

enter image description here

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.

Demo project


Solution

  • 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"),
    ]