I'm looking for a way to present a UIPickerView
when the user taps on a UIBarButtonItem
. Imagine a filter for the table view results.
I know I could use a UITextField
inputView, but this would not be the case -- all I have is a UIBarButtonItem
and a UITableView
.
I've seen into using a UIActionSheet, but it does not look natural, specially when it's animating to show.
Would animating the UIView
on and off the screen manually the only option?
The app is iOS 6+ and iPhone only, so I don't need to keep compatibility with any other versions/idioms.
It might not be your only option but animating the UIPickerView should be relatively easy for you to do. Add the picker view so it's displayed off the bottom of the screen. When it's time to animate it in:
[UIView animateWithDuration:0.3 animations:^{
self.datePicker.frame = CGRectMake(0, self.view.bounds.size.height - datePicker.bounds.size.height, datePicker.bounds.size.width, datePicker.bounds.size.height);
}];
And when it's time to hide it:
[UIView animateWithDuration:0.3 animations:^{
self.datePicker.frame = CGRectMake(0, self.view.bounds.size.height, datePicker.bounds.size.width, datePicker.bounds.size.height);
}];
Attach a UIPickerView as the inputView of a 0-sized UITextField that you have added to your view.
let picker = UIPickerView()
picker.dataSource = self
picker.delegate = self
let dummy = UITextField(frame: CGRectZero)
view.addSubview(dummy)
dummy.inputView = picker
dummy.becomeFirstResponder()
A better option is still to use the UITextField
. You don't have to actually show it on screen. Just put it in a 0x0 UIView
so that it is not visible, set it's inputView
to your UIPickerView
and call becomeFirstResponder
on it to show the picker and resignFirstResponder
to hide it.
It is convenient to have a UIView
subclass that implements all of that along with UIPickerViewDelegate
and UIPickerViewDataSource
methods.
I went with borisgolovnev's suggestion of an invisible UITextField
and came up with the following Swift 2.3 implementation:
import UIKit
class PickerViewPresenter: UITextField {
// MARK: - Initialization
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init() {
super.init(frame: CGRect.zero)
inputView = pickerView
inputAccessoryView = pickerInputAccessoryView
}
// MARK: - Public
var pickerDelegate: UIPickerViewDelegate? {
didSet {
pickerView.delegate = pickerDelegate
}
}
var pickerDataSource: UIPickerViewDataSource? {
didSet {
pickerView.dataSource = pickerDataSource
}
}
var selectButtonAction: (() -> Void)?
var currentlySelectedRow: Int {
return pickerView.selectedRowInComponent(0)
}
func selectRowAtIndex(index: Int) {
pickerView.selectRow(index, inComponent: 0, animated: false)
}
func showPicker() {
self.becomeFirstResponder()
}
func hidePicker() {
self.resignFirstResponder()
}
// MARK: - Views
private let pickerView = UIPickerView(frame: CGRect.zero)
private lazy var pickerInputAccessoryView: UIView = {
let frame = CGRect(x: 0.0, y: 0.0, width: 0.0, height: 48.0)
let pickerInputAccessoryView = UIView(frame: frame)
// Customize the view here
return pickerInputAccessoryView
}()
func selectButtonPressed(sender: UIButton) {
selectButtonAction?()
}
}
PickerViewPresenter can then conveniently be used as such:
class ViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(pickerViewPresenter)
}
private let dataModel = [10, 20, 30, 40, 50]
private lazy var pickerViewPresenter: PickerViewPresenter = {
let pickerViewPresenter = PickerViewPresenter()
pickerViewPresenter.pickerDelegate = self
pickerViewPresenter.pickerDataSource = self
pickerViewPresenter.selectButtonAction = { [weak self] () -> Void in
guard let strongSelf = self else {
return
}
let result = strongSelf.dataModel[pickerViewPresenter.currentlySelectedRow]
pickerViewPresenter.hidePicker()
// ...
}
return pickerViewPresenter
}()
private func presentPickerView {
let index = 0 // [0..dataModel.count-1]
pickerViewPresenter.selectRowAtIndex(index)
pickerViewPresenter.showPicker()
}
// MARK: - UIPickerViewDataSource
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dataModel.count
}
// MARK: - UIPickerViewDelegate
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return "\(dataModel[row]) drunken sailors"
}
}
Hope someone finds this useful =)
The cleanest way at least for me, and for Swift4 + Autolayout solution, without a UITextField
is adding a UIView
as a container of the UIPickerView
and a UIToolbar
for the top views/buttons.
Then toggle the inset/offset of that container view with animation if you want, to hide and unhide the picker.
PROPERTIES
private lazy var view_PickerContainer: UIView = {
let view = UIView()
view.backgroundColor = .white
view.addSubview(self.timePicker)
view.addSubview(self.toolbar_Picker)
return view
}()
private lazy var timePicker: UIDatePicker = {
let picker = UIDatePicker()
picker.minimumDate = Date()
picker.datePickerMode = .time
picker.setDate(Date(), animated: true)
return picker
}()
private let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm a"
return formatter
}()
private lazy var toolbar_Picker: UIToolbar = {
let toolbar = UIToolbar()
toolbar.barStyle = .blackTranslucent
toolbar.barTintColor = .blueDarkText
toolbar.tintColor = .white
self.embedButtons(toolbar)
return toolbar
}()
I use SnapKit
to layout my views programmatically. You can use the timePicker
's bounds.size as reference for your layout.
IMPLEMENTATION
In viewDidLoad()
// Setup timepicker and its container.
self.view.addSubview(self.view_PickerContainer)
self.view_PickerContainer.snp.makeConstraints { (make) in
make.height.equalTo(self.timePicker.bounds.size.height + 50.0)
make.leading.trailing.equalToSuperview()
self.constraint_PickerContainerBottom = make.bottom.equalToSuperview().inset(-500.0).constraint
}
self.timePicker.snp.makeConstraints { (make) in
make.height.equalTo(self.timePicker.bounds.size.height)
make.width.equalToSuperview()
make.bottom.equalToSuperview()
}
// Add toolbar for buttons.
self.toolbar_Picker.snp.makeConstraints { (make) in
make.height.equalTo(40.0)
make.top.leading.trailing.equalToSuperview()
}
Embedding top views
private func embedButtons(_ toolbar: UIToolbar) {
func setupLabelBarButtonItem() -> UIBarButtonItem {
let label = UILabel()
label.text = "Set Alarm Time"
label.textColor = .white
return UIBarButtonItem(customView: label)
}
let todayButton = UIBarButtonItem(title: "Today", style: .plain, target: self, action: #selector(self.todayPressed(_:)))
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(self.donePressed(_:)))
let flexButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
toolbar.setItems([todayButton, flexButton, setupLabelBarButtonItem(), flexButton, doneButton], animated: true)
}
And the result looks like this: