swiftuipickerview

Formatting picker titles and columns in groups


I cannot find a way to create a picker where the components are grouped and aligned with titles. My picker needs to allow multiple types of information to be selected, some of it is composite numbers (i.e. select digits for thousands, hundreds, tens and units). I have mocked it up here.

enter image description here

I need a done button, the titles should be centred over the groups and I need a decent space (say 10 pixels) between the titles and the picker data. I have been doing lots of reading on pickers and understand how to use them, but I am really struggling to find a simple way to format them into groups.


Solution

  • The layout is basically a horizontal stack view with distribution = .fillEqually and alignment = .fill, containing 4 vertical stack views, each with distribution = .fill and alignment = .center. This is assuming that all the pickers have the same height, and all the texts have the same height.

    With this many picker views, it would be a pain to have their data sources and delegates all in one place. I'd write some convenient custom UIPickerViews first - one for selecting n digits, and one for selecting Yes/No.

    The code may be long, but most of it is boilerplate.

    class YesNoPicker: UIPickerView, UIPickerViewDataSource, UIPickerViewDelegate {
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            1
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            2
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            row == 0 ? "Yes" : "No"
        }
        
        var enableSelected: Bool {
            self.selectedRow(inComponent: 0) == 0
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        
        private func commonInit() {
            self.dataSource = self
            self.delegate = self
        }
    }
    
    class DigitPicker: UIPickerView, UIPickerViewDataSource, UIPickerViewDelegate {
    
        var numberOfDigits = 1
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            numberOfDigits
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            10
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            row.description
        }
        
        var selectedNumber: Int {
            var result = 0
            for i in 0..<numberOfDigits {
                result *= 10
                result += self.selectedRow(inComponent: i)
            }
            return result
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        
        private func commonInit() {
            self.dataSource = self
            self.delegate = self
        }
    }
    

    Then it's just composing these with stack views. You can tweak the constraints to give appropriate widths and heights for each picker.

    class MyView: UIView {
        var enabledPicker: YesNoPicker!
        var pressurePicker: DigitPicker!
        var hePicker: DigitPicker!
        var o2Picker: DigitPicker!
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        
        private func commonInit() {
            enabledPicker = .init(frame: .zero)
            pressurePicker = .init(frame: .zero)
            pressurePicker.numberOfDigits = 4
            hePicker = .init(frame: .zero)
            o2Picker = .init(frame: .zero)
            let enabledStack = makeVerticalStack(topText: "Enabled", bottomView: enabledPicker)
            let pressureStack = makeVerticalStack(topText: "Press.", bottomView: pressurePicker)
            let heStack = makeVerticalStack(topText: "He", bottomView: hePicker)
            let o2Stack = makeVerticalStack(topText: "O2", bottomView: o2Picker)
            let hStack = UIStackView(arrangedSubviews: [enabledStack, pressureStack, heStack, o2Stack])
            hStack.axis = .horizontal
            hStack.distribution = .fillEqually
            hStack.alignment = .fill
            hStack.translatesAutoresizingMaskIntoConstraints = false
            self.addSubview(hStack)
            NSLayoutConstraint.activate([
                hStack.topAnchor.constraint(equalTo: self.topAnchor),
                hStack.bottomAnchor.constraint(equalTo: self.bottomAnchor),
                hStack.leadingAnchor.constraint(equalTo: self.leadingAnchor),
                hStack.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            ])
        }
        
        private func makeVerticalStack(topText: String, bottomView: UIView) -> UIStackView {
            let label = UILabel()
            label.text = topText
            let vStack = UIStackView(arrangedSubviews: [label, bottomView])
            vStack.axis = .vertical
            vStack.distribution = .fill
            vStack.alignment = .center
            return vStack
        }
    }
    

    You would access hePicker.selectedNumber etc for each picker's selected number.

    Output:

    enter image description here