Prevent editing UITextField but not the user interaction in Swift - ios

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 :)

Related

How do you add a button to a UIPickerView?

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

Swift - creating a UIPickerView within a UIStackView PROGRAMMATICALLY

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:

UIPickerView won't select the first row unless I select a different row first?

It seems that I cannot select the very first row in the UIPickerView when I first load it. I want the textfield to update immediately when I load the picker view. It only updates when I select something else, and then go back to the first row.
var platformData = ["XBOX", "PS4", "PC"]
var picker = UIPickerView()
#IBOutlet var platformSelected: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
picker.delegate = self
picker.dataSource = self
platformSelected.inputView = picker
let toolbar = UIToolbar()
toolbar.sizeToFit()
let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.doneAction))
toolbar.items = [doneButton]
platformSelected.inputAccessoryView = toolbar
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return platformData.count
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
platformSelected.text = platformData[row]
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return platformData[row]
}
Improve your code.
platformSelected.delegate = self
......
// MARK: - UITextFieldDelegate
extension YourUIViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
// TODO: - Specify index what you need. After editing it changed and you should get selected index using its position in platformData Array
let index = 0
picker.selectRow(index, inComponent: 0, animated: true)
}
}

hiding the keyboard when I used PickerView with textField

I want to realize one simple app, but I have some trouble. I add textField and PickerView. When I tab at textField, PickerView is appeared. But standard keyboard is appeared too (I dont want it). And when I again tab on textField, PickerView don't appear. How can I receive this problem? Thank's a lot!
This is my code:
import UIKit
class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate {
#IBOutlet weak var pickerView1: UIPickerView!
#IBOutlet weak var textField1: UITextField!
var age = ["8", "18", "28", "38", "48"]
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return age.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if pickerView == pickerView1 {
}
return age[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if pickerView == pickerView1 {
self.textField1.text = self.age[row]
self.pickerView1.isHidden = true
}
}
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField == self.textField1 {
self.pickerView1.isHidden = false
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
why you are not try the simple way, just add your pickerview as inputview of your textfield
textField1.inputView = pickerView1
This will not give you a direct way to dismiss the view since your UIPickerView has no return button, which is why I recommend to use the inputAccessoryView property to display a toolbar with a done button
let myToolbar = UIToolbar(frame: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(320), height: CGFloat(44)))
//should code with variables to support view resizing
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(self.inputAccessoryViewDidFinish))
//using default text field delegate method here, here you could call
//myTextField.resignFirstResponder to dismiss the views
myToolbar.setItems([doneButton], animated: false)
lblcurrentText.inputAccessoryView = myToolbar
and call the function as
func inputAccessoryViewDidFinish() {
lblcurrentText.resignFirstResponder()
}
at the same time after selection on your pickerview hide the text field like
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if pickerView == pickerView1 {
self.textField1.text = self.age[row]
self.inputAccessoryViewDidFinish()
}
}
Tutorial
for example you can get the step by step tutorial in here

PickerView doesn't show any value. It gives warning like no index path for table cell being reused?

I have created a UIView class in which I have declared pickerView and implement all necessary delegates. When I click on textfield, no value is shown in pickerView and warning comes in log
no index path for table cell being reused.
Please help me find out the solution of this.
import UIKit
class CustomPickerView: UIView, UIPickerViewDelegate, UIPickerViewDataSource {
var pickerView = UIPickerView()
var pickerString: String?
var pickerArray = NSArray()
class var instance: CustomPickerView
{
let pickerVC = CustomPickerView()
return pickerVC
}
override init(frame: CGRect) {
super.init(frame : frame)
pickerView.delegate = self
pickerView.dataSource = self
pickerView.backgroundColor = UIColor.yellowColor()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setValueOfPicker(text: String, array: NSArray, textField: UITextField) -> Void {
pickerString = text
pickerArray = array
setPicker(textField)
pickerView.reloadAllComponents()
pickerView.selectRow(0, inComponent: 0, animated: true)
}
//MARK:
//MARK: Piker View Delegate & DataSource
//MARK:
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return pickerArray.count
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if pickerString == "country" {
return pickerArray[row].valueForKey("country") as? String
}
else if pickerString == "printCoupon"
{
return pickerArray[row] as? String
}
return nil
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
}
func setPicker(dataTextField: UITextField)
{
dataTextField.inputView = pickerView;
let doneToolbar: UIToolbar = UIToolbar()
let flexSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FlexibleSpace, target: nil, action: nil)
let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.Done, target: self, action: #selector(donePickerAction))
let cancle: UIBarButtonItem = UIBarButtonItem(title: "Cancel", style: UIBarButtonItemStyle.Done, target: self, action: #selector(canclePickerAction))
var items = [UIBarButtonItem]()
items.append(cancle)
items.append(flexSpace)
items.append(done)
doneToolbar.sizeToFit()
doneToolbar.items = items
dataTextField.inputAccessoryView = doneToolbar
}
Try to implement this method
func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView?) -> UIView
{
let pickerLabel = UILabel()
if pickerString == "country" {
pickerLabel.text = pickerArray[row].valueForKey("country") as? String
}
else if pickerString == "printCoupon"
{
pickerLabel.text = pickerArray[row] as? String
}
pickerLabel.textAlignment = NSTextAlignment.Center
return pickerLabel
}

Resources