How do you add a single button to UIPickerView that can be used to dismiss or hide the PickerView?
I found a few solutions to these problems and many did not seem to come up with the answer I wanted. This question was the closest I could find to what I was asking, but it is very outdated so I wanted to display my solution. I have a subclass of UIPickerView that I wanted to add a UIButton to be able to dismiss on. I do not want a UIPickerView with a UIToolBar inside.
The below image describes exactly what I am looking for where the done button is added to my subclass of UIPickerView
It may seem trivial that all you need to do is add a UIButton to the UIPickerView and add a target to call on a method, because I also want the PickerView to respond to user selection on the rows, pressing the Done button caused no response
Short answer: Don't do that.
Apple says not to mess with their components' view hierarchy. You should create a component that includes a picker and your button, and make them look like a single, multi-part component. That way if Apple changes the internal structure of UIPickerView in future releases it won't break you.
Create subclass of UIView as such
class CustomViewWithPicker: UIView {
let picker = UIPickerView(frame: .zero)
let pickerTitle = UILabel(frame: .zero)
let button = UIButton(frame: .zero)
let title: String = "Picker Title"
let buttonName: String = "Button"
override init(frame: CGRect) {
super.init(frame: frame)
didLoad()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
didLoad()
}
func didLoad() {
self.addSubview(picker)
self.addSubview(pickerTitle)
self.addSubview(button)
picker.backgroundColor = .tertiarySystemBackground
picker.layer.cornerRadius = 20
picker.frame = .zero
pickerTitle.text = title
pickerTitle.font = .boldSystemFont(ofSize: 22)
pickerTitle.textAlignment = .center
pickerTitle.backgroundColor = .tertiarySystemBackground
button.setTitle(buttonName, for: .normal)
button.contentHorizontalAlignment = .right
button.contentVerticalAlignment = .top
button.isSelected = true
self.updateConstraints()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.point(inside: point, with: event) {
return super.hitTest(point, with: event)
}
guard isUserInteractionEnabled, !isHidden, alpha > 0 else {
return nil
}
for subview in subviews.reversed() {
let convertedPoint = subview.convert(point, from: self)
if let hitView = subview.hitTest(convertedPoint, with: event) {
return hitView
}
}
return nil
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func updateConstraints() {
// Make Constraints ...
}
}
In the ViewController conform to UIPickerViewDelegate and UIPickerViewDataSource
class MyViewController : UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UIGestureRecognizerDelegate {
let customView = CustomViewWithPicker()
let labels = ["label0", "label1", "label2", "label3", "label4", "label5"]
var selectedRow = 0
override func viewDidLoad() {
super.viewDidLoad()
customView.picker.delegate = self
customView.picker.dataSource = self
customView.button.addTarget(self, action: #selector(doneButtonTapped(_:)), for: .touchUpInside)
self.view.addSubview(customView)
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return labels.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return labels[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectedRow = row
}
#objc func doneButtonTapped(_ selectedButton: UIButton) {
if selectedButton.isSelected {
print("Done Button Tapped")
}
}
}
I looked far and wide for a UIPickerView implementation that did not rely on UIToolBar to detect a tap on the button to no avail.
Thank you to Duncan C. for the input and advice
Related
I want to know how to prevent a user from edition a UITextField but not the user interaction. What I want to do here is when the user taps on the text field a UIPickerView pops up from the bottom and the user can select an item from the picker view and display it on the text field. But I don't want the user to be able to edit the text field. I want to do this for the class below.
import UIKit
typealias PickerTextFieldDisplayNameHandler = ((Any) -> String)
typealias PickerTextFieldItemSelectionHandler = ((Int, Any) -> Void)
class PickerTextField: UITextField {
private let pickerView = UIPickerView(frame: .zero)
private var lastSelectedRow: Int?
public var pickerData: [Any] = []
public var displayNameHandler: PickerTextFieldDisplayNameHandler?
public var itemSelectionHandler: PickerTextFieldItemSelectionHandler?
override init(frame: CGRect) {
super.init(frame: frame)
self.configureView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.configureView()
}
override func caretRect(for position: UITextPosition) -> CGRect {
return .zero
}
private func configureView() {
self.pickerView.delegate = self
self.pickerView.dataSource = self
self.inputView = pickerView
let toolbar = UIToolbar()
toolbar.barStyle = .default
toolbar.sizeToFit()
let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(doneButtonTapped))
toolbar.setItems([spaceButton, doneButton], animated: false)
self.inputAccessoryView = toolbar
}
private func updateText() {
if self.lastSelectedRow == nil {
self.lastSelectedRow = 0
}
if self.lastSelectedRow! > self.pickerData.count {
return
}
let data = self.pickerData[self.lastSelectedRow!]
self.text = self.displayNameHandler?(data)
}
#objc func doneButtonTapped() {
self.updateText()
self.resignFirstResponder()
}
}
extension PickerTextField: UIPickerViewDelegate {
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
let data = self.pickerData[row]
return self.displayNameHandler?(data)
}
}
extension PickerTextField: UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return self.pickerData.count
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
self.lastSelectedRow = row
self.updateText()
let data = self.pickerData[row]
self.itemSelectionHandler?(row, data)
}
}
From my understanding you want to achieve the following behavior:
You would like to show the what the user selected in a picker view inside a textfield.
But you don't want the user to change the text after the text has been inserted in the textfield.
I would use the delegate methods of UITextField and UIPickerView.
There is a method in UIPickerViewDelegate that lets you know which row was selected by the user:
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
textField.text = "Hello \(row)"
}
Here you would set the text to the textfield which solves (1).
And also you can use the method in UITextFieldDelegate that allows you to prevent user input into the textfield.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return false
}
This prevents the user from typing anything into the textfield. This solves (2). However, the cursor will still be showing which might confuse the user. He might think he can enter something into the textfield from the cursor blinking when he actually can't.
And of course you have to make the inputView of textfield to your custom picker view like so:
textField.inputView = myPickerView
I hope this answers your question :)
I'm a complete beginner to swift & iOS dev in general, so be easy on me :)
In my app, I have a horizontal StackView.
Within that StackView - I have a label and a button, and now I would like to add a PickerView that would be populated from some list of options.
I've been googling and reading threads, but the closest I've gotten was getting the PickerView to show its position (using some background color) but with no actual values inside.
This is the code where I create and customize my StackView's components:
class SingleReportInputStackView: UIStackView {
... // creating and customizing my StackView
private func getObjects() -> (UILabel, UIButton, UIPickerView) {
let myLabel: UILabel = {
... // creating UILabel
}()
let myButton: UIButton = {
... // creating UIButton
}()
class MyPicker: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
let dataArray = ["English", "Maths", "History", "German", "Science"]
let UIPicker: UIPickerView = UIPickerView()
override init() {
super.init()
self.UIPicker.delegate = self
self.UIPicker.dataSource = self
self.UIPicker.backgroundColor = UIColor.white
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dataArray.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
let row = dataArray[row]
return row
}
}
let myPicker = MyPicker()
return (myLabel, myButton, myPicker.UIPicker)
}
...
}
Then, I add those components to my Horizontal StackView by calling setupSingleInput():
class SingleReportInputStackView: UIStackView {
...
private func setupSingleInput() {
let (myLabel, myButton, myPicker) = getObjects()
self.addArrangedSubview(myLabel)
self.addArrangedSubview(myButton)
self.addArrangedSubview(myPicker)
self.translatesAutoresizingMaskIntoConstraints = false
}
...
}
As I've said, I can see the label, the button and the PickerView's white background (looks like an empty, white rectangle).
BTW, I don't have a storyboard (if that wasn't obvious already) - I'm creating the UI programatically.
Can someone help me out? Why is my PickerView not being properly populated by my dataArray?
I'm a complete beginner to swift & iOS dev in general ...
I'd recommend starting a bit simpler... embedding a class inside a func is almost certainly not the way to go here.
The biggest problem is that you create an instance of MyPicker inside your getObjects() func, but then you return a UI element from that class, and the class instance goes away -- it goes out of scope:
private func getObjects() -> (UILabel, UIButton, UIPickerView) {
// ... all the stuff you're doing in here
let myPicker = MyPicker()
// as soon as you return, myPicker no longer exists!!!
return (myLabel, myButton, myPicker.UIPicker)
}
So, you have returned a UIPickerView, but it no longer has any code (its Delegate and DataSource) backing it.
Here's a quick modification:
class SingleReportInputStackView: UIStackView {
private var myPicker: MyPicker!
override init(frame: CGRect) {
super.init(frame: frame)
setupSingleInput()
}
required init(coder: NSCoder) {
super.init(coder: coder)
setupSingleInput()
}
private func setupSingleInput() {
let (myLabel, myButton) = getObjects()
myPicker = MyPicker()
self.addArrangedSubview(myLabel)
self.addArrangedSubview(myButton)
self.addArrangedSubview(myPicker.UIPicker)
self.translatesAutoresizingMaskIntoConstraints = false
}
private func getObjects() -> (UILabel, UIButton) {
let myLabel: UILabel = {
let v = UILabel()
v.text = "The Label"
return v
}()
let myButton: UIButton = {
let v = UIButton(type: .system)
v.setTitle("The Button", for: [])
return v
}()
return (myLabel, myButton)
}
private class MyPicker: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
let dataArray = ["English", "Maths", "History", "German", "Science"]
let UIPicker: UIPickerView = UIPickerView()
override init() {
super.init()
self.UIPicker.delegate = self
self.UIPicker.dataSource = self
self.UIPicker.backgroundColor = UIColor.white
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dataArray.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
let row = dataArray[row]
return row
}
}
}
and a sample view controller to try it:
class UriYakirViewController: UIViewController {
let singleReportStack = SingleReportInputStackView()
override func viewDidLoad() {
super.viewDidLoad()
// a yellow-ish background so we can see the white picker view frame
view.backgroundColor = UIColor(red: 1.0, green: 0.8, blue: 0.5, alpha: 1)
view.addSubview(singleReportStack)
singleReportStack.axis = .vertical
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
singleReportStack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
singleReportStack.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}
That code will give you this result:
I can't seem to set my outside pickerview datasource and delegate to my main view. Is there away I can set it? I want to keep my pickerview in a separate file due to other future views that might use it
This is my custom picker view
final class LenghtPickerView: UIPickerView, UIPickerViewDataSource, UIPickerViewDelegate {
let feet = Array(4...7)
let inches = Array(1...11)
private var textFieldBeginEdited: UITextField?
var selectedValue: String {
get {
return "\(feet[selectedRow(inComponent: 0)]) ft \(inches[selectedRow(inComponent: 0)]) in"
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return feet.count
} else {
return inches.count
}
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return String(feet[row])
} else {
return String(inches[row])
}
}
}
and in my main view declaring it
private let lenghtPickerView = LenghtPickerView()
also tried
private let lenghtPickerView: LenghtPickerView = LenghtPickerView()
cant set the datasource and delegate in order for the data to be render
lenghtPickerView.delegate = self
lenghtPickerView.dataSource = self
If you set
lenghtPickerView.delegate = self
lenghtPickerView.dataSource = self
from your Main View.
That means delegate & datasource of your picker should be adopted in your Main View
If you're just trying to get your data to show up, you can simply set the dataSource and delegate like so:
lenghtPickerView.delegate = lenghtPickerView
lenghtPickerView.dataSource = lenghtPickerView
https://ibb.co/2gRVTxJ
But if I understand your question correctly, and you're trying to get a value from a pickerview and set the textfield value from it and make it reusable, you might want to do a little differently.
Here is one option:
the LengthPickerViewDelegate are optional, if you don't need done besides setting the textfield.text value
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
let pickerViewTextField = LengthPickerView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 50))
pickerViewTextField.placeholder = "Select Length"
pickerViewTextField.center = self.view.center
pickerViewTextField.pickerDelegate = self
self.view.addSubview(pickerViewTextField)
}
}
extension MainViewController: LengthPickerViewDelegate {
func doSomething() {
//
print("Picker view value changed")
}
}
protocol LengthPickerViewDelegate: class {
// do something, pass values to mainVC if needed
func doSomething()
}
class LengthPickerView: UITextField {
let feet = Array(4...7)
let inches = Array(1...11)
lazy var pickerView: UIPickerView = {
let pickerView = UIPickerView()
pickerView.delegate = self
pickerView.dataSource = self
return pickerView
}()
weak var pickerDelegate: LengthPickerViewDelegate?
var selectedValue: String {
get {
return "\(feet[pickerView.selectedRow(inComponent: 0)]) ft \(inches[pickerView.selectedRow(inComponent: 1)]) in"
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.inputView = pickerView
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension LengthPickerView: UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return feet.count
} else {
return inches.count
}
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return String(feet[row])
} else {
return String(inches[row])
}
}
}
extension LengthPickerView: UIPickerViewDelegate {
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
// do something with selected values, if you need a calculation done
self.pickerDelegate?.doSomething()
// or if you only want to set the textfield value
self.text = selectedValue
}
}
If you trying to do delegate pickerView in UIView class, try this.
check awakeFromNib() function.
Because mine was looks like this
override class func awakeFromNib() {
super.awakeFromNib()
pickerView.delegate = self //Cant delegate
pickerView.dataSource = self
}
After a couple minutes I figured out something different here and I delete the "class" from "override class func awakeFromNib(){} "
Now its working I can delegate and my awakeFromNib() function look like this:
override func awakeFromNib() {
super.awakeFromNib()
pickerView.delegate = self
pickerView.dataSource = self
}
I have created list of countries in UIPickerView and calling it in UItextField. Now, I want user to tap on UIPIckerView to select the country name. I did some research in stack overflow and found (surprisingly!!) there is no easy of doing it. I have written the below code for this purpose it is showing me an below error:
Cannot convert value of type 'CGRect' to expected argument type 'CGFloat'
is there a easy way doing it .. it is very basic requirement should very straightforward; else, can someone help how to resolve the error I am getting when creating tapped function
below is my code
class StudentSignUpViewController: UIViewController,UIScrollViewDelegate, UITextFieldDelegate,UIPickerViewDelegate,UIPickerViewDataSource, UIGestureRecognizerDelegate {
#IBOutlet weak var countryText: UITextField!
#IBOutlet weak var scrollView: UIScrollView!
///who are you option dropdown box
var picker = UIPickerView()
override func viewDidLoad() {
super.viewDidLoad()
//setting portrait
AppUtility.lockOrientation(.portrait)
//hide keyboard when click outside
self.hideKeyboardWhenTappedAround()
//hide keyboard when click on return
self.countryText.delegate = self
self.scrollView.delegate = self
picker.delegate = self
picker.dataSource = self
self.countryText.inputView = picker
let tap = UITapGestureRecognizer(target: self, action: #selector(pickerTapped))
tap.cancelsTouchesInView = false
tap.delegate = self
picker.addGestureRecognizer(tap)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
AppUtility.lockOrientation(.portrait)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
AppUtility.lockOrientation(.all)
}
//*************************************************************** creating dropdown for country list ***************************************************************
let countries = NSLocale.isoCountryCodes.map { (code:String) -> String in
let id = NSLocale.localeIdentifier(fromComponents: [NSLocale.Key.countryCode.rawValue: code])
return NSLocale(localeIdentifier: "en_US").displayName(forKey: NSLocale.Key.identifier, value: id) ?? "Country not found for code: \(code)"
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return self.countries.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String?
{
return self.countries[row];
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)
{
self.countryText.text = self.countries[row];
self.countryText.endEditing(true)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true}
#objc func pickerTapped(tapRecognizer:UITapGestureRecognizer)
{
if (tapRecognizer.state == UIGestureRecognizerState.ended)
{
let frame = CGRect.zero
let rowHeight : CGFloat = self.picker.rowSize(forComponent: 0).height
let selectedRowFrame: CGRect = frame.insetBy(dx: self.picker.bounds, dy: (self.picker.frame.height - rowHeight) / 2.0 ) //Error: Cannot convert value of type 'CGRect' to expected argument type 'CGFloat'
let userTappedOnSelectedRow = (selectedRowFrame.contains(tapRecognizer.location(in: picker)))
if (userTappedOnSelectedRow)
{
self.picker.selectedRow(inComponent: 0)
}
}
}
}
As per apple standards, you should add toolbar to pickerview having a button done which will dismiss the pickerview. Selection is already being done by scrolling the pickerview row and keeping the selected row at the central segment. But if your requirement is very strict then you should create your own custom component
I'm trying to create a subclass of UIPickerView so I can use my picker in multiple views. I'm trying to call my picker programmatically as inputView of a UITextField but I can't figure out how to initialise it correctly. I'm in doubt if this is the right approach and how I can get it to work. I hope any of you can help me.
UIPickerView subclass:
import UIKit
class GroupPicker : UIPickerView, UIPickerViewDelegate, UIPickerViewDataSource{
var cdm = CoreDataManager()
var groupObjList : [Group]!
init() {
groupObjList = cdm.groupList()
}
//MARK: - Delegates and data sources
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return groupObjList.count
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
return groupObjList[row].title
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
println("picked \(groupObjList[row].title)")
}
}
How I try to call it in the view controller:
override func viewDidLoad() {
super.viewDidLoad()
groupPicker = GroupViewPicker() //instead of UIPickerView()
groupField.inputView = groupPicker
}
You have to override your init method
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame:frame)
self.delegate = self
self.dataSource = self
}
It was easy like this.
GroupPicker : UIPickerView {
override init(frame: CGRect){
super.init(frame: frame)
self.groupObjList = cdm.groupList()
}
}
Call it:
viewDidLoad() {
groupField.inputView = GroupPicker(frame: CGRectZero)
}
I presume you will want to present this modally?
If so, you could do the following:
override func viewDidLoad() {
super.viewDidLoad()
groupPicker = GroupViewPicker() //instead of UIPickerView()
//Send the data you want your picker view to handle.
groupPicker.groupObjList = dataForPickerView
self.presentViewController(viewControllerToPresent: groupPicker, animated: , completion: nil)
}
And it should pop up and present itself modally
Tip:
This var groupObjPicker: [Group]! will blow up if you never pass in a group to this array (i.e. you dont set the property as shown above). A safer (depending on what you are trying to achieve ofc.) would be to declare it as an empty array like such var groupObjList = [Group](). Then you know that if nothing is passed to it, it will at least just return 0 when .count is called on it etc.