Swift - Adding a pickerView to a textfield in UIViewController - ios

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)

Related

UIPickerView Behaving Erratically

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.

[UITextField retain]: message sent to deallocated instance

I get this message:
[UITextField retain]: message sent to deallocated instance.
I understand the message BUT I do not know what "message" is sent and how to stop it from happening....
This code produces the error:
func dismissController() {
self.view.endEditing(true)
self.dismissViewControllerAnimated(false, completion: nil)
}
While the following works "fine" I am not sure why I have to delay before dismissing the controller:
func dismissController() {
self.view.endEditing(true)
let delay = 0.75 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) { () -> Void in
self.dismissViewControllerAnimated(false, completion: nil)
}
}
EDIT:(almost full code):
#IBOutlet weak var locationTextView: UITextField!
#IBOutlet weak var userIDTextView: UITextField!
var treeLocationArray = NSMutableArray()
var treeUserIDArray = NSMutableArray()
let pickerView = UIPickerView()
var pickerButton = UIButton()
var keyboardButton = UIButton()
var locationIsSelected = true
override func viewDidLoad() {
super.viewDidLoad()
//....
locationTextView.delegate = self
userIDTextView.delegate = self
//Get the Plist... etc
treeLocationArray = dict?.objectForKey("Location") as! NSMutableArray
treeUserIDArray = dict?.objectForKey("UserID") as! NSMutableArray
locationTextView.text = userPrefs.objectForKey("DefaultLocation") as? String
userIDTextView.text = userPrefs.objectForKey("DefaultName") as? String
locationTextView.text = userPrefs.objectForKey("DefaultLocation") as? String
/// Create a view for the Accessory View
let customView = UIView(frame: CGRectMake(0, 0, 10, 50))
customView.backgroundColor = UIColor.lightGrayColor()
/// Setup the picker button
pickerButton = UIButton(frame: CGRectMake(60 , 8, 32, 32) )
pickerButton.setImage(UIImage(named:"dropIcon"), forState: .Normal)
pickerButton.tintColor = UIColor.blueColor()
pickerButton.addTarget(self, action: #selector(UserDefaultsViewController.pickerTapped), forControlEvents: UIControlEvents.TouchUpInside)
customView.addSubview(pickerButton)
/// Setup the keyboard button
keyboardButton = UIButton(frame: CGRectMake(10 , 8, 32, 32) )
keyboardButton.setImage(UIImage(named:"keyboardIcon"), forState: .Normal)
keyboardButton.tintColor = UIColor.blueColor()
keyboardButton.addTarget(self, action: #selector(UserDefaultsViewController.keyboardTapped), forControlEvents: UIControlEvents.TouchUpInside)
customView.addSubview(keyboardButton)
locationTextView.inputAccessoryView = customView
userIDTextView.inputAccessoryView = customView
}
func textFieldDidBeginEditing(textField: UITextField) {
if textField == locationTextView {
locationIsSelected = true
}
if textField == userIDTextView {
locationIsSelected = false
}
self.pickerView.reloadAllComponents()
}
func keyboardTapped(){
if locationIsSelected {
locationTextView.resignFirstResponder()
locationTextView.inputView = nil
locationTextView.becomeFirstResponder()
}
else {
userIDTextView.resignFirstResponder()
userIDTextView.inputView = nil
userIDTextView.becomeFirstResponder()
}
}
func pickerTapped(){
if locationIsSelected {
locationTextView.resignFirstResponder()
locationTextView.inputView = nil
locationTextView.inputView = pickerView
locationTextView.becomeFirstResponder()
}
else {
userIDTextView.resignFirstResponder()
userIDTextView.inputView = nil
userIDTextView.inputView = pickerView
userIDTextView.becomeFirstResponder()
}
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
var numberOfComponents = Int()
if locationIsSelected {
numberOfComponents = treeLocationArray.count
}
else {
numberOfComponents = treeUserIDArray.count
}
return numberOfComponents
}
func pickerView(pickerView: UIPickerView, numberOfRowsInSection section: Int) -> Int {
var numberOfComponents = Int()
if locationIsSelected {
numberOfComponents = treeLocationArray.count
}
else {
numberOfComponents = treeUserIDArray.count
}
return numberOfComponents
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
var componetString = String()
if locationIsSelected {
componetString = (treeLocationArray[row] as? String)!
}
else {
componetString = (treeUserIDArray[row] as? String)!
}
return componetString
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if locationIsSelected {
if treeLocationArray.count >= 1 {
locationTextView.text = treeLocationArray[row] as? String
}
}
else {
if treeUserIDArray.count >= 1 {
userIDTextView.text = treeUserIDArray[row] as? String
}
}
}
This issue happens because you have call to dismissViewControllerAnimated. Due to VC life cycle all objects, that this VC holds will be deallocated. That means, that all manipulations with UI after dismiss are not memory-safe.
From the documentation:
This method looks at the current view and its subview hierarchy for
the text field that is currently the first responder. If it finds one,
it asks that text field to resign as first responder. If the force
parameter is set to true, the text field is never even asked; it is
forced to resign.
So, your VC had been deallocated while endEditing was looking thorough hierarchy, as I guess. It's the only one reason, that may cause memory problem.
Why do you need call to endEditing before dismissing? simply dismiss VC without this call. if you have logic, that depends on endEditing - separate it and call instead of endEditing. Hope this helps.
UPDATE:
Try to call endEditing in viewWillDisappear - this triggers view to resign first responder right before it will be dismissed.

Different UIPickerView Delegate/DataSource in UIAlertController

Please tell me how make two different UIPickerView with different delegate/datasource? I have TableView with two cells, and i need open two different UIAlertController with UIPickerView. My code:
private weak var filterController: UIAlertController! {
let controllerConfig = UIAlertController(title: "", message: "\n\n\n\n\n\n\n\n\n", preferredStyle: .Alert)
controllerConfig.modalInPopover = true
//Create UIPickerView
let pickerFrame = CGRectMake(0, 20, 270, 180)
let picker = UIPickerView(frame: pickerFrame)
//Picker Color
picker.backgroundColor = controllerConfig.view.backgroundColor
//Picker Delegate/DataSource
picker.delegate = self
picker.dataSource = self
//Add Picker
controllerConfig.view.addSubview(picker)
//Create header frame
let headerFrame = CGRectMake(0, 5, 270, 45)
let headerView = UIView(frame: headerFrame)
headerView.backgroundColor = controllerConfig.view.backgroundColor
//Create 'Close' button
var closeButton: UIButton! {
let buttonFrame = CGRectMake(230, 5, 35, 35)
let buttonConfig = UIButton(frame: buttonFrame)
buttonConfig.setTitle("X", forState: .Normal)
//Set color on text
buttonConfig.setTitleColor(UIColor.grayColor(), forState: .Normal)
buttonConfig.setTitleColor(erablagoThemeColor, forState: .Highlighted)
buttonConfig.addTarget(self, action: #selector(didClickOnCloseButton), forControlEvents: .TouchUpInside)
return buttonConfig
}
let showAction = UIAlertAction(title: "Select", style: .Default) { (action) in
//TODO
}
//Add subview
headerView.addSubview(closeButton)
controllerConfig.addAction(showAction)
controllerConfig.view.addSubview(headerView)
return controllerConfig
}
///Close current view
func didClickOnCloseButton() {
dismissViewControllerAnimated(true, completion: nil)
}
And i have implementation UIPickerViewDelegate and UIPickerViewDataSource. I need change this implementation in my ViewContriller.
//MARK: - UIPickerViewDataSource
extension FilterController: UIPickerViewDataSource {
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return 2
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
switch row {
case 0:
return "All"
case 1:
return "Cars and Bicycles"
default:
return nil
}
}
}
May be me need create class with UIAlertController and call him in my ViewController? I don't know. Thank you in advance:]
When you say you want the pickers to have different delegates it you are suggesting that different classes are responsible for handling the delegate methods. Thus the following will solve your problem:
picker.delegate = self
picker2.delegate = anotherClassInstance
But I don't think thats what you're asking. I think you are asking how to have two different pickers with different data both being handled by the same class, and thus share the same delegate class. The easiest way to do this is assign the tag property on each picker:
picker.delegate = self
picker.tag = 1
picker2.delegate = self
picker2.tag = 2
Then, differentiate between them as so:
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if picker.tag == 1 {
switch row {
case 0:
return "All"
case 1:
return "Cars and Bicycles"
default:
return nil
} else {
switch row {
case 0:
return "None"
case 1:
return "Other picker data"
default:
return nil
}
}
Ok, I made new class with UIPickerDelegate/DataSource and made new instance this class. My code looks like that:
Delegate class:
class SortFilterPicker: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return 4
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
switch row {
case 0:
return "1-1"
case 1:
return "2-2"
case 2:
return "3-3"
case 3:
return "4-4"
default:
return nil
}
}
}
And main code:
private let categoryFilter = FilterPicker()
private weak var filterController: UIAlertController! {
var heightPopUp = "\n"
let controllerConfig = UIAlertController(title: "", message: heightPopUp.repeatOf(9), preferredStyle: .Alert)
controllerConfig.modalInPopover = true
let pickerFrame = CGRectMake(0, 20, 270, 180)
let picker = UIPickerView(frame: pickerFrame)
picker.delegate = categoryFilter
picker.dataSource = categoryFilter
picker.backgroundColor = controllerConfig.view.backgroundColor
controllerConfig.view.addSubview(picker)
let headerFrame = CGRectMake(0, 5, 270, 45)
let headerView = UIView(frame: headerFrame)
headerView.backgroundColor = controllerConfig.view.backgroundColor
var closeButton: UIButton! {
let buttonFrame = CGRectMake(230, 5, 35, 35)
let buttonConfig = UIButton(frame: buttonFrame)
buttonConfig.setTitle("X", forState: .Normal)
//Set color on text
buttonConfig.setTitleColor(UIColor.grayColor(), forState: .Normal)
buttonConfig.setTitleColor(erablagoThemeColor, forState: .Highlighted)
buttonConfig.addTarget(self, action: #selector(didClickOnCloseButton), forControlEvents: .TouchUpInside)
return buttonConfig
}
let showAction = UIAlertAction(title: "Ok", style: .Default) { (action) in
//TODO: Make show
}
headerView.addSubview(closeButton)
controllerConfig.addAction(showAction)
controllerConfig.view.addSubview(headerView)
return controllerConfig
}
It's works :]

Is it possible to have a UIPickerView and a UIDatePicker from the same UITextField in Swift?

I'm new to Swift so please bear with me.
Firstly, let me explain my app layout:
I have a Text Field on the screen, with a Label and a button. I am asking the user 3 questions - these questions are displayed using the 'Label', and the next question appears each time the user presses the Next 'Button' (The questions are populated from an array).
One of the the questions needs to populate a Picker View (so the user can pick an option).
One of the questions uses a normal keyboard.
And the last question I need to use a Date Picker.
Is this possible to do? And if so, could you show me how to add it to my code below please?
let questions = ["What is your title?", "What is your full name?", "When were you born?"]
var currentQuestionIndex = 0
let placeholder = ["Title", "Full name", "Date of birth"]
var currentPlaceholderIndex = 0
#IBAction func nextButton(sender: AnyObject) {
textField.hidden = false
textField.placeholder = placeholder[currentPlaceholderIndex]
barImage.hidden = false
// Reset text field to have no text
textField.text = ""
questionLabel.text = questions[currentQuestionIndex]
// Displays the questions in array
if currentQuestionIndex < questions.count {
++currentQuestionIndex
// Displays the placeholder text in the textfield
if currentPlaceholderIndex < placeholder.count {
++currentPlaceholderIndex
}
buttonLabel.setTitle("Next", forState: UIControlState.Normal)
}
else {
//Add some logic here to run whenever the user has answered all the questions.
}
}
thanks for your help!
Nick
NEW CODE BELOW (from 6th April 2015):
class ViewController: UIViewController, UITextFieldDelegate, UIPickerViewDataSource, UIPickerViewDelegate {
var pickerData = ["Mr", "Mrs", "Miss", "Ms", "Doctor"]
var questions = ["What is your title?", "What is your name?", "What is your DOB?"]
var currentQuestionIndex = 0
var datePickerView:UIDatePicker = UIDatePicker()
var pickerView:UIPickerView = UIPickerView()
#IBOutlet var buttonLabel: UIButton!
#IBOutlet var questionsLabel: UILabel!
#IBOutlet var textField: UITextField!
#IBAction func textFieldEditing(sender: UITextField) {
datePickerView.datePickerMode = UIDatePickerMode.Date
sender.inputView = datePickerView
datePickerView.addTarget(self, action: Selector("datePickerValueChanged:"), forControlEvents: UIControlEvents.ValueChanged)
}
func datePickerValueChanged(sender: UIDatePicker) {
var dateformatter = NSDateFormatter()
dateformatter.dateStyle = NSDateFormatterStyle.LongStyle
textField.text = dateformatter.stringFromDate(sender.date)
}
#IBAction func nextButton(sender: UIButton) {
textField.text = ""
textField.hidden = false
buttonLabel.setTitle("Next", forState: UIControlState.Normal)
questionsLabel.text = questions[currentQuestionIndex]
if currentQuestionIndex < questions.count {
currentQuestionIndex++
}
else {
//println("questions are complete")
}
}
func textFieldDidBeginEditing(textField: UITextField) {
switch currentQuestionIndex {
case 0:
self.view.addSubview(datePickerView)
case 2:
self.view.addSubview(pickerView)
default:
println("Test")
}
}
override func viewDidLoad() {
super.viewDidLoad()
buttonLabel.setTitle("Start", forState: UIControlState.Normal)
textField.hidden = true
questionsLabel.text = ""
textField.delegate = self
pickerView.delegate = self
pickerView.dataSource = self
// 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.
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
self.view.endEditing(true)
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return pickerData.count
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
return pickerData[row]
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
textField.text = pickerData[row]
pickerView.hidden = true
}
}
Based on currentQuestionIndex you can add either UIPickerView or UIDatePicker to your UITextField instance
switch currentQuestionIndex {
case 0:
// TODO: Add UIPickerView
case 2:
// TODO: Add UIDatePicker
}
Edit: More explanation
At first, conform to UITextFieldDelegate and implement textFieldDidBeginEditing(:)
You must implement that method because you wanna get notify when the UITextField instance get first responder state
Also, set UITextField instance's delegate property to self or what ever instance/object that you want to put and implement textFieldDidBeginEditing(:)
Then write this code
func textFieldDidBeginEditing(_ textField: UITextField) {
switch currentQuestionIndex {
case 0:
// TODO: Add UIPickerView
// Add it for example at the bottom your view self.view.addSubview(pickerViewInstance)
// or add it as the subview of UIPopoverController instance that is added to your UITextField instance
case 2:
// TODO: Add UIDatePicker
// Add it for example at the bottom your view self.view.addSubview(datePickerInstance)
// or add it as the subview of UIPopoverController instance that is added to your UITextField instance
}
}
Remember to remove added view when you want add another view to your UIPopoverViewController.
Edit: Add real world implementation
class NewCaseViewController : UIViewController {
. . .
#IBOutlet weak var textField: UITextField // TODO: Initialize it properly in designated initializer
var popoverController: UIPopoverController! // TODO: Initialize it
. . .
}
class PopoverContentManager : UIViewController {
. . .
var pickerView: UIPickerView // TODO: Initialize it properly
var datePicker: UIDatePicker // TODO: Initialize it properly
. . .
}
extension NewCaseViewController : UITextFieldDelegate {
func textFieldBeginEditing(_ textField: UITextField) {
println(__FUNCTION__)
var popover = PopoverContentManager()
switch self.currentQuestionIndex {
case 0:
popover.datePicker.hidden = true
self.popoverController = init(contentViewController: popOver)
self.popoverController.presentPopoverFromRect(self.textField.bounds, inView: self.textField, permittedArrowDirections: UIPopoverArrowDirectionDown, animated: true)
self.popoverController.popoverContentSize = CGSizeMake(120, 90)
case 1:
popover.pickerView.hidden = true
self.popoverController = init(contentViewController: popOver)
self.popoverController.presentPopoverFromRect(self.textField.bounds, inView: self.textField, permittedArrowDirections: UIPopoverArrowDirectionDown, animated: true)
self.popoverController.popoverContentSize = CGSizeMake(120, 90)
}
}
}

Show UIPickerView like a keyboard, without UITextField

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);
}];

Resources