Solutions to common scenarios when using UITextField on iOS Aug 5 2019

At some point on your app development career, you’ll find yourself working with text fields to get some data from the user. Collecting data from the user sometimes involves additional work than merely reading text from an UITextField. For example, if we have a field at the bottom of the screen, and we show the keyboard, the keyboard might cover it. We have to do additional work to fix this. Like this, there are many other small tasks that we would perform while working with UITextFields. In this post, I’ll show how to handle some of the most common scenarios. I’ll cover:

We have a lot to cover, so let’s started.

Basics of reading data from the user

Our first task is to read data from the user. I’ll assume you know the basics of creating a UITextField from the StoryBoard or programmatically. Once we have a reference to our text field, we can read the data using the property text. The following code is something that you would typically see on a ViewController.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import UIKit

class ViewController: UIViewController {

    var dogYearsTextField: UITextField!
    override func viewDidLoad() {
        super.viewDidLoad()
        //Create a new UITextField and add it to the view
        dogYearsTextField = UITextField()

        ...
        // Add AutoLayout rules
        ...

        view.addSubview(dogYearsTextField)
    }

     func calculateHumanAge() -> Int? {
       if let text = dogYearsTextField.text, let dogYears = Int(text) {
         // let's assume that a dog year equals seven Human years
         return dogYears * 7
       } else {
         return nil
       }
     }
}

We read the dogYearsTextField.text and verify that it can be cast into an Int, then process it. Good, that should get us started. UITextField has many useful properties. One of the most often overlooked properties is clearButtonMode. This property gives your users an easier way to clear the text instead of having to delete one character at a time. You can activate the clear button like this:

1
dogYearsTextField.clearButtonMode = .whileEditing

Now a small button will appear inside your text field. The user can click on the button to clear the whole text. There are more attributes you can tweak on your text fields, but you can explore them on your own. Have a look at Apple’s UITextField docummentation for more information.

Sometimes you don’t want to show a text field right away. You might want to show a UILable that looks nice, and then when the user clicks the label, we show a text field and the user can edit it. You can accomplish this by setting a hide/show toggle between a UILabel and a UITextField.

UILabel and UITextField view toggle

We are going to set our ViewController as the delegate of our text field, that means that our ViewController should implement UITextFieldDelegate.

1
class ViewController: UITextFieldDelegate {

Set the UITextField delegate in our viewDidLoad . Also, set the UILabel as visible and hide the UITextField.

1
2
3
4
titleTextField.delegate = self

titleLabel.isHidden = false
titleTextField.isHidden = true

To handle the tap on the label, we will add a UITapGestureRecognizer to the UILabel.

1
2
3
let tap = UITapGestureRecognizer(target: self, action: #selector(titleLabelTapped)) //We'll create titleLabelTapped soon
titleLabel.isUserInteractionEnabled = true
titleLabel.addGestureRecognizer(tap)

Let’s create the titleLabelTapped function. This function runs when the user taps the label.

1
2
3
4
    @objc func titleLabelTapped(_ sender:UITapGestureRecognizer) {
        titleTextField.isHidden = false
        titleLabel.isHidden = true
    }

The function is simple, we hide titleLable and make titleTextField visible. We only need to implement the textFieldShouldReturn function. The function textFieldShouldReturn runs when the return key in the keyboard is pressed. We can also set the type of return key that the Keyboard displays. The type of return key could be a normal “Return” key that you see in text views, or it could show the message “Done”. There are other types of return buttons. You can check the Official documentation to check all the types. Before implementing textFieldShouldReturn let’s set the return type to done in the viewDidLoad function, it should look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    override func viewDidLoad() {
        super.viewDidLoad()
        //Create a new UITextField and add it to the view
        dogYearsTextField = UITextField()

        ...
        // Any other set up code like AutoLayout rules
        ...

        titleTextField.delegate = self
        titleLabel.isHidden = false
        titleTextField.isHidden = true

        titleTextField.returnKeyType = .done

        view.addSubview(dogYearsTextField)
    }

Ok, now we can add the textFieldShouldReturn function to our ViewController.

1
2
3
4
5
6
7
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  textField.resignFirstResponder()
    titleLabel.text = textField.text
    titleLabel.isHidden = false
    titleTextField.isHidden = true
    return true
}

The function resignFirstResponder tells the text field that it will stop being the first responder of the window, and also hides the keyboard for the text field. We also set the label text to be the content submitted on the text field, and finally, toggle the visibility of the label and text field.

With the basics out of the way, let’s start with the first common scenario, working with keyboards.

Dealing with keyboards

When the user taps a text field, the text field becomes the first responder of the window, and the system’s keyboard appears. We can also set the type of keyboard we want the user to see, giving the user the correct input source.

Keyboard types

In our previous example, we were working with a field that should only receive numbers. Because of it, it would be best to set the keyboard to only display numbers. Or if we are working with an email text field, it’ll be good to show a keyboard that has the @ sing at hand. There are many types of keyboards you can check them all on Apple’s documentation.

Let’s set our keyboard type to numberPad for our dogYearsTextField and emailAddress to the owner’s email field.

1
2
dogYearsTextField.keyboardType = .numberPad
ownersEmailTextField.keyboardType = .emailAddress

That small change improves the user experience. Another improvement to our user experience could be adding a toolbar to the keyboard and adding additional buttons.

Adding a toolbar to the keyboard

Imagine you are using a UITextView for the user input, and the user can enter multiple lines by tapping the “Return” key. How can you the user tell your app that it is done editing? We can add a toolbar with a “Done” button that the user can click when it has finished editing. Let’s create a function to add the toolbar to the keyboard for a specific UITextField. Then you can call this function on your viewDidLoad function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func addToolbarOnKeyboard(textField: UITextField){
  let doneToolbar: UIToolbar = UIToolbar(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
    doneToolbar.barStyle = .default

    let doneButtonTitle = NSLocalizedString("Done", comment: "This is the done text that appears on button for Keyboard Toolbar")

    let items: [UIBarButtonItem]
    let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
    let done: UIBarButtonItem = UIBarButtonItem(title: doneButtonTitle, style: .done, target: self, action: #selector(doneButtonAction(_:)))
    items = [flexSpace, done]

    doneToolbar.items = items
    doneToolbar.sizeToFit()

    textField.inputAccessoryView = doneToolbar
}

// The doneButtonAction
@objc func doneButtonAction(_ sender: UITextField){
  sender.resignFirstResponder()
  //any other action we want to take when the user is done editing
}

That function adds a toolbar on top of the keyboard for the textField. Now we can call it on our viewDidLoad:

1
2
3
4
5
6
7
let ourTextField = UITextField()

...
//more configurations
...

addToolbarOnKeyboard(ourTextField)

Every time the user edits the field, the keyboard will show a toolbar on top. Toolbars give us the chance to add additional functionality. Another useful feature we can provide to our users that doesn’t require a toolbar is to dismiss the keyboard by tapping outside the keyboard.

Dismissing keyboard on tap

To accomplish this, we need to add a UITapGestureRecognizer to our view and tell it to dismiss the keyboard by calling resignFirstResponder. The code looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override func viewDidLoad() {
  super.viewDidLoad()

    ...
    // Set up code to keyboard
    ...


    let tap = UITapGestureRecognizer(target: self, action: #selector(hideKeyboard))
    self.addGestureRecognizer(tap)

}

@objc func hideKeyboard() {
  if activeTextField.isFirstResponder {
    activeTextField.resignFirstResponder()
  }
}

If you want to have additional functionality for the keyboard dismiss, you’ll add it on hideKeyboard function.

Another common problem when working with text fields is that the text field could be covered by the keyboard when the user taps the text field. Let’s see how we can handle this case.

Fix keyboard covering text field

If we place a text field in the lower part of the screen, there is a chance that when we display the keyboard, it will cover the text field. We can fix this by moving the view up, so the keyboard doesn’t cover it. But first, we need to check if the text field is in a position where the keyboard height will hide it. If that is the case, we need to calculate how much we need to move the view to avoid covering the text field with the keyboard. After the user is done editing, we need to make sure to restore the view to its original position. Let’s implement the solution.

First, we’ll add a two observers, UIResponder.keyboardWillHideNotification and UIResponder.keyboardWillChangeFrameNotification. The first, to handle when the user is done editing the text field. The second will give us information about the location and height of the keyboard when the keyboard’s frame changes.

Let’s first add the observers, we will probably do this in our viewDidLoad function:

1
2
3
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(fixKeyboardPosition), name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(fixKeyboardPosition), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)

Both notifications will triger the same function fixKeyboardPosition. Here is the implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@objc func fixKeyboardPosition(notification: Notification) {
  let userInfo = notification.userInfo!

    let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue

    if notification.name == UIResponder.keyboardWillHideNotification {
      self.frame.origin.y = 0
    } else {
      let activeTextFieldY = ourTextField?.frame.origin.y ?? 0
      let additionalPadding: CGFloat = 20
      let yDelta = (frame.maxY + abs(self.frame.origin.y)) - (keyboardScreenEndFrame.height + additionalPadding)
        if yDelta < activeTextFieldY {
          let scrollToY = activeTextFieldY - yDelta + additionalPadding
            self.frame.origin.y = -scrollToY
        } else {
          self.frame.origin.y = 0
        }
    }
}

That’s it. This function will take care of keeping the text field insight when the keyboard covers our text field.

Let’s now have a look at another important aspect of data ingestion that is data validation.

Data Validation

When using UITextField remember that what you read from the user will be a String, you are in charge of converting it to the data type that your application needs. Also, we need to make sure that the data is valid. For example, if we are expecting an email in the text field, we need to verify that it is a String and that it matches the email pattern to be valid.

We can add our validations in the textFieldShouldReturn function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  textField.resignFirstResponder()
    if let value = textField.text, let age = Int(value), isValid(age) {
      // Do anything we need with the success value
      // Remove error styles if the field was invalid before
      errorMessageLabel.isHidden = true //Hide the error label if everything is correct
        textField.layer.masksToBounds = true
        textField.layer.borderColor = UIColor.clear.cgColor
        textField.layer.borderWidth = 0
    } else {
      //We can show a hidden label with the error message
      errorMessageLabel.text = "Invalid Age, should be between 0 and 100."
        errorMessageLabel.isHidden = false
        // we can also add a red border
        textField.layer.masksToBounds = true
        textField.layer.borderColor = UIColor.red.cgColor
        textField.layer.borderWidth = 1.0
    }
  completeInputSubmit(textField)
  return true
}

func isValidAge(value: Int) {
 return value >= 0 && value <= 100
}

That is a simple validation, but you get the idea. It is crucial always to validate the data we get from the user. How complex or simple you want to make this process is up to you, but you should certainly do it.

In all of our examples, we’ve been working only with one text field. It is time we look at how to handle having multiple fields.

Handling multiple Text fields

The most common information we need when we have multiple fields is to know which field is currently active. We will do that by having a UITextField that always point to the latest edited text field.

Let’s add activeTextField to our ViewController.

1
2
3
4
class ViewController: UIViewController {

    var activeTextField: UITextField!
...

Also, we want our view controller to be the delegate for our text fields, so our ViewController should implement UITextFieldDelegate.

1
class ViewController: UIViewController, UITextFieldDelegate {

And on our viewDidLoad function we should set the delegate to self:

1
2
3
4
5
firstTextField.delegate = self
secondTextField.delegate = self
thirdTextField.delegate = self
fourtTextField.delegate = self
// etcetera

How can we tell which of those fields is the active text field? We can use the textFieldDidBeginEditing to set it up:

1
2
3
func textFieldDidBeginEditing(_ textField: UITextField) {
  activeTextField = textField
}

Now, activeTextField will point to the latest edited text field. Having access to the activeTextField allows us to run different code depending on which field is currently active, for example:

1
2
3
4
5
6
7
8
9
10
switch activeTextField {
  case firstTextField:
    firstTextField.becomeFirstResponder()
  case secondTextField:
      secondTextField.becomeFirstResponder()
  case thirdTextField:
        third.becomeFirstResponder()
  default:
          return
}

We can use that pattern to identify and run different code depending on which of our text fields is currently the active text field. Let’s quickly go through all the previous scenarios and adapt them to multiple text fields. Let’s start with adding a toolbar to our keyboard.

Add toolbar that allows us to navigate our text fields

Let’s add next and previous to our toolbar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func addToolbarOnKeyboard(textField: UITextField){
  let doneToolbar: UIToolbar = UIToolbar(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
    doneToolbar.barStyle = .default

    let doneButtonTitle = NSLocalizedString("Done", comment: "This is the done text that appears on button for Keyboard Toolbar")
    let prevButtonTitle = NSLocalizedString("Prev", comment: "This is the Previous text that appears on button for Keyboard Toolbar")
    let nextButtonTitle = NSLocalizedString("Next", comment: "This is the Next text that appears on button for Keyboard Toolbar")

    let items: [UIBarButtonItem]
    let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
    let done: UIBarButtonItem = UIBarButtonItem(title: doneButtonTitle, style: .done, target: self, action: #selector(self.doneButtonAction))
    switch textField {
      case colorTextField:
        let next = UIBarButtonItem(title: nextButtonTitle, style: .plain, target: self, action: #selector(switchToNextTextField))
          items = [next, flexSpace, done]
      case brightnessTextField:
          let prev = UIBarButtonItem(title: prevButtonTitle, style: .plain, target: self, action: #selector(switchToPrevTextField))
            items = [prev, flexSpace, done]
      default:
            let prev = UIBarButtonItem(title: prevButtonTitle, style: .plain, target: self, action: #selector(switchToPrevTextField))
              let next = UIBarButtonItem(title: nextButtonTitle, style: .plain, target: self, action: #selector(switchToNextTextField))
              items = [prev, next, flexSpace, done]
    }

  doneToolbar.items = items
    doneToolbar.sizeToFit()

    textField.inputAccessoryView = doneToolbar
}

Here is a possible implementation of our next, previous and done functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@objc func switchToNextTextField() {
  switch activeTextField {
    case firstTextField:
      secondTextField.becomeFirstResponder()
    case secondTextField:
        thirdTextField.becomeFirstResponder()
    case thirdTextField:
          fourthTextField.becomeFirstResponder()
    default:
            return
  }
}

@objc func switchToPrevTextField() {
  switch activeTextField {
    case fourthTextField:
      thirdTextField.becomeFirstResponder()
    case thirdTextField:
        secondTextField.becomeFirstResponder()
    case secondTextField:
          firstTextField.becomeFirstResponder()
    default:
            return
  }
}

@objc func doneButtonAction(){
  activeTextField.resignFirstResponder()
    completeInputSubmit(activeTextField)
}

The completeInputSubmit function could look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func completeInputSubmit(_ textField: UITextField) {
  switch activeTextField {
    case firstTextField:
      //Execute code for first text field
    case secondTextField:
      //Execute code for second text field
    case thirdTextField:
      //Execute code for third text field
    case fourthTextField:
      //Execute code for fourth text field
    default:
      return
  }
}

Our textFieldShouldReturn would look like this:

1
2
3
4
5
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  textField.resignFirstResponder()
  completeInputSubmit(textField)
  return true
}

And finally our function fixKeyboardPosition that makes sure our text fields stay visible would look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@objc func fixKeyboardPosition(notification: Notification) {
  let userInfo = notification.userInfo!

    let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue

    if notification.name == UIResponder.keyboardWillHideNotification {
      self.frame.origin.y = 0
    } else {
      let activeTextFieldY = activeTextField?.frame.origin.y ?? 0
      let additionalPadding: CGFloat = 20
      let yDelta = (frame.maxY + abs(self.frame.origin.y)) - (keyboardScreenEndFrame.height + additionalPadding)
        if yDelta < activeTextFieldY {
          let scrollToY = activeTextFieldY - yDelta + additionalPadding
            self.frame.origin.y = -scrollToY
        } else {
          self.frame.origin.y = 0
        }
    }
}

And that’s it. We covered the most common scenarios. You can adapt the code examples to any other situation where you are working with multiple text fields.

I hope that was useful.

Final Thoughts

Many applications deal with UITextFields, but as you saw, the interaction with the text fields is not only reading the text value. It is useful to know what to do in different scenarios. I hope that the examples in this post give you a good base that you can expand to handle any situation you encounter in your app development.

As always let me know if you have any questions or feedback, and remember to check the next section for more information related to this post.

Related topics/notes of interest

1
2
  let item = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
  //then you add it to the toolbar as we did before

** There is no comment system yet, but you can send me a message on twitter @rderik or send me an email: derik[at]rderik[dot]com.