I have a UIPickerView that is used to select musical instruments, and I have a play icon next to each instrument name that I want to play the appropriate sound when the user clicks it, so that they can hear what the instrument sounds like before they select it in the picker.
I have no problem creating images that response to taps in general.
However, within a UIPickerView, an image that is created as part of the row doesn't seem to receive clicks (I would guess the UIPickerView somehow takes priority?)
What do I need to do to ensure that my images get tap events?
Many thanks!
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
let parentView = UIView()
let label = UILabel(frame: CGRect(x: 60, y: 0, width: 80, height: 50))
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height:50))
if #available(iOS 13.0, *) {
imageView.image = UIImage.add
let tapGR = UITapGestureRecognizer(target: self, action: #selector(self.imageTapped))
imageView.addGestureRecognizer(tapGR)
imageView.isUserInteractionEnabled = true
} else {
// Fallback on earlier versions
}
label.text = "red"
parentView.addSubview(label)
parentView.addSubview(imageView)
return parentView
}
You won't be able to add a gesture recognizer to the viewForRow view -- all touches get eaten by the picker view.
So, you could write your own picker view which, depending on how closely you want to replicate the built-in picker view, could be fairly simple or super complex.
Or... you can add a tap recognizer to the picker view ... then check if the tap is inside the center row's image view.
Here's a quick example...
We'll use this as our custom row view:
class MyPickerRowView: UIView {
let label = UILabel(frame: CGRect(x: 60, y: 0, width: 80, height: 50))
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height:50))
var selected: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
if #available(iOS 13.0, *) {
imageView.image = UIImage.add
} else {
// Fallback on earlier versions
imageView.backgroundColor = .red
}
addSubview(label)
addSubview(imageView)
}
func tappedImageView(_ p: CGPoint) -> Bool {
// let's toggle the image if the tap hits
if imageView.frame.contains(p) {
selected.toggle()
if #available(iOS 13.0, *) {
imageView.image = selected ? UIImage.checkmark : UIImage.add
} else {
imageView.backgroundColor = selected ? .green : .red
}
return true
}
return false
}
}
The only "non-usual" thing is the func tappedImageView(_ p: CGPoint) -> Bool... it will check the passed point and return true if the point is inside the image view frame, or false if it's not.
We're also going to toggle the image between UIImage.add and UIImage.checkmark so we have some visual feedback.
Our tap gesture handler (in the controller class) will:
get the view for the selected (center) row
make sure it's our custom MyPickerRowView class
translate the tap point to that view
ask that view if the point is inside the image view frame
So, here's the controller class:
class PickerTestViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate, UIGestureRecognizerDelegate {
let picker = UIPickerView()
override func viewDidLoad() {
super.viewDidLoad()
picker.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(picker)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
picker.leadingAnchor.constraint(equalTo: g.leadingAnchor),
picker.trailingAnchor.constraint(equalTo: g.trailingAnchor),
picker.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
// create a tap recognizer
let tap = UITapGestureRecognizer(target: self, action: #selector(pickerTapped(_:)))
// don't prevent all the normal behaviors of the picker view
tap.cancelsTouchesInView = false
// tap delegate needs to be set so we can use shouldRecognizeSimultaneouslyWith
tap.delegate = self
// add the tap recognizer to the PICKER view itself
picker.addGestureRecognizer(tap)
picker.delegate = self
picker.dataSource = self
// just so we can easily see the frame of the picker view
picker.layer.borderColor = UIColor.red.cgColor
picker.layer.borderWidth = 1
}
#objc func pickerTapped(_ tapRecognizer:UITapGestureRecognizer) {
// get the index of the selected row (the "center" row)
let n = picker.selectedRow(inComponent: 0)
// make sure the view for that row is a MyPickerRowView
if let v = picker.view(forRow: n, forComponent: 0) as? MyPickerRowView {
// convert the tap location to that view
let p = tapRecognizer.location(in: v)
// ask the view if the tap is in the image view frame
if v.tappedImageView(p) {
print("tapped in image view in center row")
// do something
}
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return 100
}
func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
return 50
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
let parentView = MyPickerRowView()
parentView.label.text = "\(row)"
return parentView
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
Good question. I haven't done a lot with this specific scenario but I've done similar stuff with UITableView. Some ideas:
Double check the following: isUserEnabled is true for the whole set of subviews from parentView down to imageView? Is imageView toward the front? Maybe make imageView.layer.zPosition closer to 1 and others farther back? You can double check this in simulator with the debug view hierarchy stack icon:
Debug View Hierarchy Icon in XCode
Work around: Does it need to happen on a click or can you just play a small sample when you hit the didSelectRow delegate method?
Related
So I feel stupid asking about this, but I simply cannot figure out what is going wrong. Basically, when I click a textField that has a UIPickerView as its inputView it will show up very quickly and then disappear. However, the toolBar (its accessoryView still remains on screen). I haven't seen anyone else online who has experienced this, so that's why I had to ask it on SO.
At first I thought it had something do with when I was setting the pickerView's .isHidden property. But I omitted those calls to no effect.
Thus, I will be including all of the code related to my pickerViews since I really don't know where the issue is. I'm sure it's something minor I'm missing, but any help would be appreciated.
class myAssessmentsViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate {
#IBOutlet weak var contentSelectionTextField: UITextField!
#IBOutlet weak var contentOrderingTextField: UITextField!
var contentSelectionPickerView: UIPickerView = UIPickerView()
var contentOrderingPickerView: UIPickerView = UIPickerView()
var contentSelectionOptions: [String] = ["All", "Physics HL", "Chemistry HL", "Spanish Ab SL"]
var contentOrderingOptions: [String] = ["Date", "Subject", "Grade", "Title"]
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
contentSelectionPickerView.tag = 1 //for the delegate methods
contentSelectionPickerView.isHidden = true //commenting this out did nothing
contentSelectionPickerView.delegate = self
contentSelectionPickerView.dataSource = self
contentSelectionTextField.inputView = contentSelectionPickerView //set pickerView as responder
contentSelectionTextField.delegate = self
contentOrderingPickerView.tag = 2 //for the delegate methods
contentOrderingPickerView.isHidden = true //commenting this out also did nothing
contentOrderingPickerView.delegate = self
contentOrderingPickerView.dataSource = self
initializePickerViewToolBar(clearButtonFunc: "clearPressedContentSelectionPickerView", doneButtonFunc: "donePressedContentSelectionPickerView", textField: contentSelectionTextField)
initializePickerViewToolBar(clearButtonFunc: "clearPressedContentOrderingPickerView", doneButtonFunc: "donePressedContentOrderingPickerView", textField: contentOrderingTextField)
contentOrderingTextField.inputView = contentOrderingPickerView //set pickerView as responder
contentOrderingTextField.delegate = self
// Do any additional setup after loading the view.
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1 //same for both pickers
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if pickerView.tag == 1 { //contentSelectionPickerView
return contentSelectionOptions.count
} else if pickerView.tag == 2 { //contentOrderingPickerView
return contentOrderingOptions.count
} else {
return 1
}
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
self.view.endEditing(true)
if pickerView.tag == 1 { //contentSelectionPickerView
return contentSelectionOptions[row]
} else if pickerView.tag == 2 { //contentOrderingPickerView
return contentOrderingOptions[row]
} else {
return "1"
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if pickerView.tag == 1 {
contentSelectionTextField.text = contentSelectionOptions[row]
} else if pickerView.tag == 2 {
contentOrderingTextField.text = contentOrderingOptions[row]
}
}
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField == contentSelectionTextField {
contentSelectionPickerView.isHidden = false //also was not source of problem
} else if textField == contentOrderingTextField {
contentOrderingPickerView.isHidden = false //same here
}
}
func donePressedContentSelectionPickerView(){
contentSelectionTextField.resignFirstResponder()
}
func donePressedContentOrderingPickerView(){
contentOrderingTextField.resignFirstResponder()
}
func clearPressedContentSelectionPickerView(){
contentSelectionTextField.resignFirstResponder()
contentSelectionTextField.text = ""
}
func clearPressedContentOrderingPickerView(){
contentOrderingTextField.resignFirstResponder()
contentOrderingTextField.text = ""
}
func initializePickerViewToolBar(clearButtonFunc: String, doneButtonFunc: String, textField: UITextField){
let toolBar = UIToolbar(frame: CGRect(x: 0, y: textField.frame.size.height/6, width: textField.frame.size.width, height: 40.0))
toolBar.layer.position = CGPoint(x: textField.frame.size.width/2, y: textField.frame.size.height-20.0)
toolBar.barStyle = .default
toolBar.tintColor = UIColor.black
let clearButton = UIBarButtonItem(title: "Clear", style: .plain, target: self, action: Selector(clearButtonFunc))
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: Selector(doneButtonFunc))
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
toolBar.setItems([clearButton,flexSpace,doneButton], animated: true)
toolBar.isUserInteractionEnabled = true
textField.inputAccessoryView = toolBar
}
}
Here is also a photo of what I am talking about visually. As you can see at the bottom of the screen the accessoryView is still visible but the content isn't. I would think that the accessoryView would be a subview of the UIPickerView and that they would disappear together but that is apparently not the case.
Again, apologies for all of the code (and the large image), I know it is a lot to read through, but any insight would be greatly appreciated!
So it turns out that one of the answers from SO that I was following told me to include self.view.endEditing(true) in my pickerViewTitleForRow delegate method. This meant whenever the titles were being set, the first responder was being dismissed, causing this issue. Removing it caused the problem to disappear. Thanks to #MikeTaverne for pointing it out.
I am struggling a bit with optionals and setting up views and subviews by using an #IBOutlet. I am currently trying to create a controller to add a user to the database. One of my fields is related to the user's gender, so I want it to be linked to a UIPickerView. I am not using Storyboard.
I removed all the other fields from my code to make it a lot shorter and readable, but it will also have other UIPickerView fields (like birthday).
Can someone help me with my code so that my textfield will display a UIPickerView when selected?
class AddUserController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate {
var picker: UIPickerView = UIPickerView()
var genderData = ["Male", "Female"]
// Containers
let inputsContainerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.whiteColor()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.masksToBounds = true
return view
}()
// Subviews
#IBOutlet weak var genderTextField: UITextField! = {
var gender = UITextField()
gender.placeholder = "Gender"
gender.translatesAutoresizingMaskIntoConstraints = false
gender.leftViewMode = UITextFieldViewMode.Always
gender.autocorrectionType = .No
let lbl = UILabel(frame: CGRect(x: 0, y: 0, width: 120, height: 30))
lbl.text = "GENDER"
lbl.font = UIFont.boldSystemFontOfSize(12)
gender.leftView = lbl
return gender
}()
override func viewDidLoad() {
super.viewDidLoad()
picker = UIPickerView()
picker.dataSource = self
picker.delegate = self
picker.hidden = true
genderTextField?.delegate = self
genderTextField?.inputView = picker
genderTextField?.text = genderData[0]
view.backgroundColor = UIColor(white: 0.95, alpha: 1)
view.addSubview(inputsContainerView)
setupInputsContainerView()
}
// Picker functions
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return genderData.count
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return genderData[row]
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)
{
genderTextField.text = genderData[row]
self.view.endEditing(true)
picker.hidden = true
}
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
picker.hidden = false
return true
}
// Setup all the views
func setupInputsContainerView() {
inputsContainerView.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor).active = true
inputsContainerView.topAnchor.constraintEqualToAnchor(view.bottomAnchor, constant: 12).active = true
inputsContainerView.widthAnchor.constraintEqualToAnchor(view.widthAnchor, constant: -24).active = true
inputsContainerView.heightAnchor.constraintEqualToConstant(40).active = true
inputsContainerView.addSubview(genderTextField)
genderTextField.leftAnchor.constraintEqualToAnchor(inputsContainerView.leftAnchor, constant: 12).active = true
genderTextField.topAnchor.constraintEqualToAnchor(inputsContainerView.topAnchor).active = true
genderTextField.widthAnchor.constraintEqualToAnchor(inputsContainerView.widthAnchor).active = true
genderTextField.heightAnchor.constraintEqualToAnchor(inputsContainerView.heightAnchor).active = true
}
}
genderTextField is getting created every time you call that.
genderTextField?.delegate = self
genderTextField?.inputView = picker
genderTextField?.text = genderData[0]
All these are different objects
inputsContainerView.addSubview(genderTextField)
What you added here is also different. So the textField you adedd to the view and the delegate/datasource you added are all different. One quick solution is to move
genderTextField?.delegate = self
genderTextField?.inputView = picker
genderTextField?.text = genderData[0]
these code after this line inputsContainerView.addSubview(genderTextField)
I have an array of strings, and I want to set them up, so the user can scroll through the strings, and which ever one it lands on that one is selected, but I am unsure on how to do it. I was thinking of using a scroll view or a pagevc, but those take up the whole view, and it will show one string per page. Instead I need them side by side so I can see the value one ahead or one behind. A roadmap on how to execute this would be perfect, I am so lost on this component.
func numberOfItemsInCarousel(carousel: iCarousel) -> Int {
//getInfo()
return animal.count
}
func carousel(carousel: iCarousel, viewForItemAtIndex index: Int, reusingView view: UIView?) -> UIView {
var labelsubject: UILabel
var itemView: UIImageView
var labelname: UILabel
var cardimage: UIImageView
var cardtypeimage: UIImageView
if(view == nil){
itemView = UIImageView(frame:CGRect(x:0, y:0, width:400, height:100))
itemView.image = UIImage(named: "page")
itemView.contentMode = .Center
labelsubject = UILabel(frame: itemView.bounds)
labelsubject.backgroundColor = UIColor.clearColor()
labelsubject.textAlignment = .Center
labelsubject.textColor = UIColor.whiteColor()
labelsubject.tag = 1
itemView.addSubview(labelsubject)
}else{
itemView = view as! UIImageView;
labelsubject = itemView.viewWithTag(1) as! UILabel!
}
selectedData.anim = animal[index]
return itemView
}
func carouselDidEndDragging(carousel: iCarousel, willDecelerate decelerate: Bool) {
if !decelerate {
print(carousel.currentItemIndex)
//text[carousel.currentItemIndex]
}
}
func carouselDidEndDecelerating(carousel: iCarousel) {
print(carousel.currentItemIndex)
//text[carousel.currentItemIndex]
}
What I want to do is to get an action evnet from the bar button. At the beginning, I put the bar button on the bar tool on on pickerview. The button display on the pickerview, but I couldn't push the button. The color of the button doesn't change at all. It means I cannot touch the button. The pickerview can be scrolled even on the button area. It seems like the button is under the pickerview.
Once I add the tool bar with slef.view.adSubView directly on the view, I could push the bar button and get the action. I want to get an action from the bar button on the picker view. I don't use these UIs on main story board for this time.
var pickerView1: UIPickerView = UIPickerView()
var toolBar1 = UIToolbar()
var barButton1 = UIBarButtonItem()
let strData: [String] = ["abc","xyz"]
override func viewDidLoad() {
super.viewDidLoad()
// init bar button
barButton1 = UIBarButtonItem(title: "PUSH", style: UIBarButtonItemStyle.Plain, target: self, action: Selector("pushBarButton:"))
barButton1.enabled = true
// init tool bar
toolBar1.barStyle = UIBarStyle.Default
toolBar1.translucent = true
toolBar1.userInteractionEnabled = true
toolBar1.sizeToFit()
toolBar1.delegate = self
// init picker
pickerView1.delegate = self
pickerView1.frame = CGRectMake(0.0, (self.view.bounds.height/3)*2, self.view.bounds.width , self.view.bounds.height / 3.0 )
// add button on tool bar
toolBar1.setItems([barButton1], animated: true)
// add tool bar on picker
pickerView1.addSubview(toolBar1)
// add picker on view
self.view.addSubview(pickerView1)
}
func pushBarButton( sender: AnyObject){
pickerView1.hidden = true
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return strData.count
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return strData[row]
}
I use Xcode 7.1.
The first time you was not able to push the button may be due to UserInteractionEnabled was not selected in xib.
Anyways, please try this to add programmatically:
barButton1 = UIBarButtonItem(title:"PUSH",style:UIBarButtonItemStyle.Plain,target:self,action: Selector("pushBarButton"))
func pushBarButton(){
pickerView1.hidden = true
}
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.
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: .zero)
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.
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:
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 =)
Based on borisgolovnev's suggestion and CodeMonkey's answer, I have implemented the idea with an invisible UITextField in the following way in Swift 5.
import UIKit
class PickerViewPresenter: UITextField, UIPickerViewDataSource, UIPickerViewDelegate {
private lazy var doneToolbar: UIToolbar = {
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped))
let items = [flexSpace, doneButton]
toolbar.items = items
toolbar.sizeToFit()
return toolbar
}()
private lazy var pickerView: UIPickerView = {
let pickerView = UIPickerView()
pickerView.dataSource = self
pickerView.delegate = self
return pickerView
}()
var items: [Item] = []
var didSelectItem: ((Item) -> Void)?
private var selectedItem: Item?
init() {
super.init(frame: .zero)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
inputView = pickerView
inputAccessoryView = doneToolbar
}
#objc private func doneButtonTapped() {
if let selectedItem = selectedItem {
didSelectItem?(selectedItem)
}
resignFirstResponder()
}
func showPicker() {
becomeFirstResponder()
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return items.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return items[row].name
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectedItem = items[row]
}
}
Usage in your ViewController:
private var selectedItem: Item?
private lazy var pickerViewPresenter: PickerViewPresenter = {
let pickerViewPresenter = PickerViewPresenter()
pickerViewPresenter.didSelectItem = { [weak self] item in
self?.selectedItem = item
}
return pickerViewPresenter
}()
view.addSubview(pickerViewPresenter)
// e.g. on a button tap
#objc private func buttonTapped() {
pickerViewPresenter.showPicker()
}
Example:
I have attached it to a UITapGestureRecognizer.
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);
}];