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.
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.
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 UIPickerView
s 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: