This is by no means a needed thing. It would be nice if possible. I am not sure if this can be done.
I have a UIPickerView
and it will have 41-42 options. Right now I have all of my options alphabetically. I want them to be broken into groups and before each group I want it to have a title. Similar to how a TableView has sections and you can give each section a title. I want the title of each section in the picker but I don't want it to be a selectable row. For example:
Picker options:
Core (not selecteable)
Barbarian (selectable)
Bard (selectable)
several more options
APG (not Selectable)
Alchemist (selectable)
Cavalier (selectable)
several more options
continue with several more
Is this even possible?
You don't need to subclass UIPickerView to achieve this, rather implement the datasource and delegate thoughtfully.
I advise you to have a class that implements both, as a test I did this like:
import UIKit
class PickerViewSource: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
init(pickerView: UIPickerView) {
super.init()
pickerView.dataSource = self
pickerView.delegate = self
}
var selected: (([String: Any]) -> Void)?
let data = [
["title": "Group a", "selectable": false],
["title": "title a1", "selectable": true],
["title": "title a2", "selectable": true],
["title": "title a3", "selectable": true],
["title": "Group b", "selectable": false],
["title": "title b1", "selectable": true],
["title": "title b2", "selectable": true],
["title": "Group c", "selectable": false],
["title": "title c1", "selectable": true],
["title": "title c2", "selectable": true],
]
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return data.count
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
let d = data[row]
if let selectable = d["selectable"] as? Bool, selectable == true {
if let view = view as? ItemView, let title = d["title"] as? String{
view.label.text = title
return view
}
let view = ItemView()
if let title = d["title"] as? String{
view.label.text = title
}
return view
}
if let selectable = d["selectable"] as? Bool, selectable == false {
if let view = view as? GroupView, let title = d["title"] as? String{
view.label.text = title
return view
}
let view = GroupView()
if let title = d["title"] as? String{
view.label.text = title
}
return view
}
return UIView()
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
var index = row
if let selectable = data[row]["selectable"] as? Bool, selectable == false {
index += 1
pickerView.selectRow(index, inComponent: 0, animated: true)
}
selected?(data[index])
}
}
As you see this class has a callback var selected: (([String: Any]) -> Void)?
. It will only be called for electable items.
The ViewController instantiate the source and set the callback:
class ViewController: UIViewController {
var pickerViewSource: PickerViewSource?
@IBOutlet weak var pickerView: UIPickerView! {
didSet{
pickerViewSource = PickerViewSource(pickerView: pickerView)
pickerViewSource?.selected = {
selected in
print(selected)
}
}
}
}
and for completness, the views:
class BaseView: UIView {
var label: UILabel = UILabel()
override func layoutSubviews() {
super.layoutSubviews()
self.addSubview(label)
label.frame = self.bounds
}
}
class GroupView: BaseView {
override func layoutSubviews() {
super.layoutSubviews()
label.backgroundColor = .orange
}
}
class ItemView: BaseView {
}
Instead of forcing a selection by selecting the next line, you could allow to select the group, but don't send a selection callback. But to make sure you trigger that nothing selectable is set, you should add a deselect callback.
var selectedElement: [String:Any]?
var deselected: (([String: Any]) -> Void)?
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if let element = selectedElement {
deselected?(element)
selectedElement = nil
}
if let selectable = data[row]["selectable"] as? Bool, selectable == true {
let element = data[row]
selected?(element)
selectedElement = element
}
}
Now you can use the callbacks selected
and deselected
to alter your user interface to i.e en- or disable buttons.
For a complete example see this example code I just published: https://github.com/vikingosegundo/PickerWithSectionTitlesExample
Edit: As @Duncan C mentions in his answer. There isn't a built in solution for this in iOS7+. My answer provides an alternative way to accomplish your goals.
Before iOS 7 you could use showsSelectionIndicator
to not show selected state on different objects in the picker view. But iOS7 + doesn't allow this:
On iOS 7 and later you cannot customzie the picker view’s selection indicator. The selection indicator is always shown, so setting this property to false has no effect.
Instead you can do a little nasty hack:
The delegate adopts the UIPickerViewDelegate protocol and provides the content for each component’s row, either as an attributed string, a plain string, or a view, and it typically responds to new selections or deselections.
This short example just shows how a solution might work. To make these not selectable picker objects you can use an attributed string as mentioned in the docs that looks different from the rest.
If a user will try to select one of these header objects you can just don't close the picker view. The user will have to choose another option, and by time, the user will learn that these objects with maybe a bold string is not selectable.
var datasource = ["Core", "content", "content", "content", "APG", "content", "content"]
func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
print(self.datasource[row])
switch row {
case 0:
// create attributed string
let myString = datasource[row]
let myAttribute: [String:Any] = [ NSForegroundColorAttributeName: UIColor.blue, NSUnderlineStyleAttributeName: NSUnderlineStyle.styleDouble.rawValue ]
let myAttrString = NSAttributedString(string: myString, attributes: myAttribute)
return myAttrString
case 15:
let myString = datasource[row]
let myAttribute: [String:Any] = [
NSForegroundColorAttributeName: UIColor.blue,
NSUnderlineStyleAttributeName: NSUnderlineStyle.styleDouble.rawValue ]
let myAttrString = NSAttributedString(string: myString, attributes: myAttribute)
return myAttrString
case 4:
let myString = datasource[row]
let myAttribute: [String:Any] = [
NSForegroundColorAttributeName: UIColor.blue,
NSUnderlineStyleAttributeName: NSUnderlineStyle.styleDouble.rawValue ]
let myAttrString = NSAttributedString(string: myString, attributes: myAttribute)
return myAttrString
default:
//It all depends on your datasource, but if you have 0-42 strings(core, APG included) you can just output the rest under default here
let myString = datasource[row]
let myAttribute: [String:Any] = [ NSForegroundColorAttributeName: UIColor.black]
let myAttrString = NSAttributedString(string: myString, attributes: myAttribute)
return myAttrString
}
}
It doesn't look good, but that can you customize the way you want. You can also add images or UiView in the picker view objects.
No, I don't think a standard picker view offers that option. That said, you might be able to subclass UIPickerView and teach your custom subclass to have section dividers as you describe.
It would certainly be possible, although more work, to create your own control that acts like a picker with sections as you describe.