UPDATED:
Now use a scrollview instead of moving the view around. See the bottom of this post.
Here’s how to easily, quickly create a manager for a form of UITextFields including Previous/Next buttons on a toolbar above the keyboard. It uses an extension on the UIViewController so it’s non-intrusive.
All you have to do is create your UITextField form and set the tag value for each UITextField to be a unique, ascending value: 0,1,2,3…
The code for the project is at the bottom (as you might suspect) so feel free to download and use that.
UITextField form with Keyboard Input Accessory View UIToolbar for Previous/Next Buttons
Here’s the process…
- Create your UITextField form in Interface Builder and set each text field’s tag value to an incrementing value. 0 is the first field of the form, 1 is next, etc.
2. Create the UIViewController extension… I’ll break this step down…
Prep the UITextFields:
var tbKeyboard : UIToolbar? var tfLast : UITextField? extension UIViewController : UITextFieldDelegate { // set all of the UITextFields' delegate to the view controller (self) // if no view is passed in, start w/ the self.view func prepTextFields(inView view : UIView? = nil) { // cycle thru all the subviews looking for text fields for v in view?.subviews ?? self.view.subviews { if let tf = v as? UITextField { // set the delegate and return key to 'Next' tf.delegate = self tf.returnKeyType = .next // save the last text field for later if tfLast == nil || tfLast!.tag < tf.tag { tfLast = tf } } else if v.subviews.count > 0 { // recursive prepTextFields(inView: v) } } // Set the last text field's return key to 'Send' // * view == nil - only do this on the end of // the original call (not recursive calls) if view == nil { tfLast?.returnKeyType = .send tfLast = nil // release it } }
This first function preps the text fields. It cycles thru the subview of the UIViewController’s view searching for UITextField instances. When it finds one, it sets the delegate the self and sets the keyboard return to ‘Next’ (it stores the last text field – designated by the largest tag value – to set the return key to ‘Send’ at the bottom).
Notice the tbKeyboard and tfLast variables outside of the extension – this is because extensions can’t have stored properties.
First Field, First Responder
// make the first UITextField (tag=0) the first responder // if no view is passed in, start w/ the self.view func firstTFBecomeFirstResponder(view : UIView? = nil) { for v in view?.subviews ?? self.view.subviews { if v is UITextField, v.tag == 0 { (v as! UITextField).becomeFirstResponder() } else if v.subviews.count > 0 { // recursive firstTFBecomeFirstResponder(view: v) } } }
This function cycles through the view’s subviews looking for the text field w/ the tag of 0. When it finds it, it makes it the first responder.
This is so when your form is displayed, the keyboard comes up with the first field already active. This is optional. Feel free to not call it.
Set the Keyboard Input Accessory
public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { // if there's no tool bar, create it if tbKeyboard == nil { tbKeyboard = UIToolbar.init(frame: CGRect.init(x: 0, y: 0, width: self.view.frame.size.width, height: 44)) let bbiPrev = UIBarButtonItem.init(title: "Previous", style: .plain, target: self, action: #selector(doBtnPrev)) let bbiNext = UIBarButtonItem.init(title: "Next", style: .plain, target: self, action: #selector(doBtnNext)) let bbiSpacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) let bbiSubmit = UIBarButtonItem.init(title: "Submit", style: .plain, target: self, action: #selector(UIViewController.doBtnSubmit)) tbKeyboard?.items = [bbiPrev, bbiNext, bbiSpacer, bbiSubmit] } // set the tool bar as this text field's input accessory view textField.inputAccessoryView = tbKeyboard return true }
Since the the view controller is the delegate of the text field, this function will be called whenever a new text field is going to become the first responder. Notice we always return true at the end to allow the text field to become active.
If the toolbar hasn’t been created yet, create it w/ the 3 buttons: Previous, Next and Submit. Each button has it’s own selector to call which we’ll get to soon. Hold on!
We set the toolbar as the text field’s input accessory view so it displays above the keyboard.
Find a Text Field by Tag
// search view's subviews // if no view is passed in, start w/ the self.view func findTextField(withTag tag : Int, inViewsSubviewsOf view : UIView? = nil) -> UITextField? { for v in view?.subviews ?? self.view.subviews { // found a match? return it if v is UITextField, v.tag == tag { return (v as! UITextField) } else if v.subviews.count > 0 { // recursive if let tf = findTextField(withTag: tag, inViewsSubviewsOf: v) { return tf } } } return nil // not found }
We cycle recursively thru the view’s subviews looking for the elusive, red-bellied text field with matching tag value.
NOTE: Instead of storing the tfLast as a text field, we could have just stored the largest tag and then used this function to find it and set its keyboard return key to ‘Send.’ 😉
Make Previous/Next First Responder
// make the next (or previous if next=false) text field the first responder func makeTFFirstResponder(next : Bool) -> Bool { // find the current first responder (text field) if let fr = self.view.findFirstResponder() as? UITextField { // find the next (or previous) text field based on the tag if let tf = findTextField(withTag: fr.tag + (next ? 1 : -1)) { tf.becomeFirstResponder() return true } } return false }
Here’s where it comes together… if ‘next’ is true, we’re going to make the next text field active. If false, the previous. First we find the current first responder in the view (see below) and optionally bind it to fr as a UITextField.
Then we call findTextField (see above) with either the tag plus 1 (for next) or -1 (for previous) and optionally bind that to tf.
Let’s break things up with a pic…
Then we set tf to first responder and return true. If something didn’t play out, we return false.
Previous/Next Button actions
func doBtnPrev(_ sender: Any) { let _ = makeTFFirstResponder(next: false) } func doBtnNext(_ sender: Any) { let _ = makeTFFirstResponder(next: true) }
For the buttons we created in the tool bar, we have matching functions for previous and next.
Each simply calls the makeTFFirstResponder (see above) with either false (previous) or true (next).
Easy.
Next Return Key function
// delegate method public func textFieldShouldReturn(_ textField: UITextField) -> Bool { // when user taps Return, make the next text field first responder if makeTFFirstResponder(next: true) == false { // if it fails (last text field), submit the form submitForm() } return false }
Then the user taps ‘Next’ on the keyboard, this is called. It tries to go to the next text field. If it doesn’t (false is returned), it assumes it’s on the last text field and calls submitForm (see below) to submit the form however you need.
It returns false ultimately because we don’t want to allow the return key to actually be allowed in the text field.
Submitting the Form
func doBtnSubmit(_ sender: Any) { submitForm() } func submitForm() { self.view.endEditing(true) // override me }
The Submit button on the toolbar also has a function that just called submitForm. The submitForm function is just a placeholder to be overridden in your actual instance of UIViewController.
This version of submitForm ends editing so the keyboard goes away. You can call it in your version if you want.
UIView extension to find the current first responder
extension UIView { // go thru this view's subviews and look for the current first responder func findFirstResponder() -> UIResponder? { // if self is the first responder, return it // (this is from the recursion below) if isFirstResponder { return self } for v in subviews { if v.isFirstResponder == true { return v } if let fr = v.findFirstResponder() { // recursive return fr } } // no first responder return nil } }
I mentioned this was coming so don’t say you’re surprised. This is an extension for UIView that finds the current first responder. Since our text fields and other views are subclasses of UIView, we can recursively call this function and check self for first responder.
For each of the subviews, if it’s the first responder, return it. Otherwise, call this same function on it (recursive).
Another way to do this would be to store the current text field (when textFieldShouldBeginEditing is called – you know this will be the current first responder). But I’m trying to create a small footprint. Also, you would then have to clear that out if you ended editing.
Your UIViewController Class
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // make this view controller the delegate of all the text fields prepTextFields() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // make the first form text field the first responder firstTFBecomeFirstResponder() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } override func submitForm() { super.submitForm() // actually submit the form print ("Submit") } }
So your viewDidLoad calls the prepTextFields function to set the delegate and keyboard return key (‘Next’ or ‘Send’).
Your viewDidAppear calls firstTFBecomeFirstResponder to activate the first field.
And your version of submitForm calls the super version to ending editing and then whatever else you need to do: validate the fields, store the data, communicate w/ the server. Don’t look at me!
Hopefully you agree it’s very little code and very little in interface builder. Enjoy. Share improvements please. 🙂
So fine – it wasn’t really 2 steps but I think it always looks good to put things in steps so people know it’s been thought through.
Here’s the whole file put together…
// // ViewController.swift // PrevNextForm // // Created by Bear Cahill on 7/28/17. // Copyright © 2017 Brainwash Inc. All rights reserved. // import UIKit var tbKeyboard : UIToolbar? var tfLast : UITextField? extension UIViewController : UITextFieldDelegate { // set all of the UITextFields' delegate to the view controller (self) // if no view is passed in, start w/ the self.view func prepTextFields(inView view : UIView? = nil) { // cycle thru all the subviews looking for text fields for v in view?.subviews ?? self.view.subviews { if let tf = v as? UITextField { // set the delegate and return key to 'Next' tf.delegate = self tf.returnKeyType = .next // save the last text field for later if tfLast == nil || tfLast!.tag < tf.tag { tfLast = tf } } else if v.subviews.count > 0 { // recursive prepTextFields(inView: v) } } // Set the last text field's return key to 'Send' // * view == nil - only do this on the end of // the original call (not recursive calls) if view == nil { tfLast?.returnKeyType = .send tfLast = nil } } // make the first UITextField (tag=0) the first responder // if no view is passed in, start w/ the self.view func firstTFBecomeFirstResponder(view : UIView? = nil) { for v in view?.subviews ?? self.view.subviews { if v is UITextField, v.tag == 0 { (v as! UITextField).becomeFirstResponder() } else if v.subviews.count > 0 { // recursive firstTFBecomeFirstResponder(view: v) } } } public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { // if there's no tool bar, create it if tbKeyboard == nil { tbKeyboard = UIToolbar.init(frame: CGRect.init(x: 0, y: 0, width: self.view.frame.size.width, height: 44)) let bbiPrev = UIBarButtonItem.init(title: "Previous", style: .plain, target: self, action: #selector(doBtnPrev)) let bbiNext = UIBarButtonItem.init(title: "Next", style: .plain, target: self, action: #selector(doBtnNext)) let bbiSpacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) let bbiSubmit = UIBarButtonItem.init(title: "Submit", style: .plain, target: self, action: #selector(UIViewController.doBtnSubmit)) tbKeyboard?.items = [bbiPrev, bbiNext, bbiSpacer, bbiSubmit] } // set the tool bar as this text field's input accessory view textField.inputAccessoryView = tbKeyboard return true } // search view's subviews // if no view is passed in, start w/ the self.view func findTextField(withTag tag : Int, inViewsSubviewsOf view : UIView? = nil) -> UITextField? { for v in view?.subviews ?? self.view.subviews { // found a match? return it if v is UITextField, v.tag == tag { return (v as! UITextField) } else if v.subviews.count > 0 { // recursive if let tf = findTextField(withTag: tag, inViewsSubviewsOf: v) { return tf } } } return nil // not found } // make the next (or previous if next=false) text field the first responder func makeTFFirstResponder(next : Bool) -> Bool { // find the current first responder (text field) if let fr = self.view.findFirstResponder() as? UITextField { // find the next (or previous) text field based on the tag if let tf = findTextField(withTag: fr.tag + (next ? 1 : -1)) { tf.becomeFirstResponder() return true } } return false } func doBtnPrev(_ sender: Any) { let _ = makeTFFirstResponder(next: false) } func doBtnNext(_ sender: Any) { let _ = makeTFFirstResponder(next: true) } // delegate method public func textFieldShouldReturn(_ textField: UITextField) -> Bool { // when user taps Return, make the next text field first responder if makeTFFirstResponder(next: true) == false { // if it fails (last text field), submit the form submitForm() } return false } func doBtnSubmit(_ sender: Any) { submitForm() } func submitForm() { self.view.endEditing(true) // override me } } extension UIView { // go thru this view's subviews and look for the current first responder func findFirstResponder() -> UIResponder? { // if self is the first responder, return it // (this is from the recursion below) if isFirstResponder { return self } for v in subviews { if v.isFirstResponder == true { return v } if let fr = v.findFirstResponder() { // recursive return fr } } // no first responder return nil } } class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // make this view controller the delegate of all the text fields prepTextFields() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // make the first form text field the first responder firstTFBecomeFirstResponder() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } override func submitForm() { super.submitForm() // actually submit the form print ("Submit") } }
Your two lines are prepTextFields() and firstTFBecomeFirstResponder() – really you only need the prepTextFields call so 2 lines of code was an overestimation.
Of course you have to write the code to submit the form but… 😉
Thanks for reading or just skipping here. Both have goodness sprinkled on them…
Here’s the Code
Xcode Project: PrevNextForm
UPDATED version with UIScrollView
To make this work with a scrollview (instead of moving the view around). We need to add a scrollview, place our form on it and move it accordingly. I’m doing it all programmatically because we already have everything else.
So add these properties (including changing the vToMove to have a didSet):
var vToMove : UIView? { didSet { svToMove = nil } } var svToMove : UIScrollView?
In the checkForPlacement function, create the scroll view if it doesn’t exist (it will be set to nil each time a new form is created which is when vToMove should be set):
var firstTime = false if svToMove == nil { svToMove = UIScrollView.init(frame: vToMove!.frame) svToMove?.alwaysBounceVertical = true svToMove?.contentSize = CGSize.init(width: (svToMove?.frame.size.width)!, height: (vToMove?.frame.size.height)!) vToMove?.superview?.addSubview(svToMove!) svToMove?.addSubview(vToMove!) firstTime = true }
The new animated change block changes the frame of the scrollview (just the first time – see firstTime) and animates to the field:
if let tf = tfCurrent { let bottomOfTF = trueYBottom(ofView: tf) + tf.frame.size.height + 30 // 30 is buffer // let topOfKB = rect.size.height - kbHeight // if bottomOfTF > topOfKB || rect.origin.y < 0 { DispatchQueue.main.async { UIView.animate(withDuration: 0.3, animations: { if firstTime { rect.size.height = (vToMove?.frame.size.height)! - kbHeight svToMove?.frame = rect } svToMove?.contentOffset = CGPoint.init(x: 0.0, y: max(vToMoveOriginalY, bottomOfTF - kbHeight)) // rect.origin.y = min(topOfKB - bottomOfTF, vToMoveOriginalY) // vToMove!.frame = rect }) } // } }
Here’s the whole thing with the scrollview changes:
// move UI for keyboard placement var tfCurrent : UITextField? var kbHeight : CGFloat = 0.0 var vToMoveOriginalY : CGFloat = -999.0 var vToMove : UIView? { didSet { svToMove = nil } } var svToMove : UIScrollView? extension UIViewController { func registerForKeyboardNotifications(){ NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil) } func deregisterFromKeyboardNotifications(){ NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil) } func trueYBottom(ofView view : UIView) -> CGFloat { var bot : CGFloat = 0.0 var v : UIView? = view while v != nil { if v!.frame.origin.y > 0 { bot += v!.frame.origin.y } v = v!.superview } return bot } func checkFormPlacement() { guard vToMove != nil else { return } var firstTime = false if svToMove == nil { svToMove = UIScrollView.init(frame: vToMove!.frame) svToMove?.alwaysBounceVertical = true svToMove?.contentSize = CGSize.init(width: (svToMove?.frame.size.width)!, height: (vToMove?.frame.size.height)!) vToMove?.superview?.addSubview(svToMove!) svToMove?.addSubview(vToMove!) firstTime = true } var rect = vToMove!.frame if vToMoveOriginalY == -999.0 { vToMoveOriginalY = rect.origin.y } if let tf = tfCurrent { let bottomOfTF = trueYBottom(ofView: tf) + tf.frame.size.height + 30 // 30 is buffer // let topOfKB = rect.size.height - kbHeight // if bottomOfTF > topOfKB || rect.origin.y < 0 { DispatchQueue.main.async { UIView.animate(withDuration: 0.3, animations: { if firstTime { rect.size.height = (vToMove?.frame.size.height)! - kbHeight svToMove?.frame = rect } svToMove?.contentOffset = CGPoint.init(x: 0.0, y: max(vToMoveOriginalY, bottomOfTF - kbHeight)) // rect.origin.y = min(topOfKB - bottomOfTF, vToMoveOriginalY) // vToMove!.frame = rect }) } // } } } func keyboardWillShow(notification: NSNotification){ var info = notification.userInfo! if let h = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size.height { kbHeight = h checkFormPlacement() } } func keyboardWillHide(notification: NSNotification){ guard vToMove != nil else { return } DispatchQueue.main.async { UIView.animate(withDuration: 0.3, animations: { var rect = self.view.frame rect.origin.y = 0 vToMove!.frame = rect }) } } public func textFieldDidBeginEditing(_ textField: UITextField){ tfCurrent = textField } public func textFieldDidEndEditing(_ textField: UITextField){ tfCurrent = nil } }