:  ~ 14 min read

Handling the Next button automatically

Entering text in multiple text fields is such a common pattern — everywhere, not just iOS — there should be a way to easily navigate from on field to the next, preferably the ”correct” one. Sadly, iOS doesn’t offer this feature, but let’s see how we could accomplish this ourselves.

First, a quick recap on what we need:

Cool, let’s dive in and start with the protocol. What would we need here?

protocol NextTextFieldHandler {

   var textFields: [UITextField] { get } // 1

   func setupReturnKeyType(for textField: UITextField) // 2
   func handleReturnKeyTapped(on textField: UITextField) // 3

}

The textFields property (1) can either hold all the fields on a view, or just the ones we’re interested in navigating through — the protocol doesn’t care about this, it’s our job to decide what is needed, when adhering to the protocol. The two functions are used to cover our other two needs (2, 3).

extension NextTextFieldHandler {

   private var _textFields: [UITextField] {
      return textFields.filter { !$0.isHidden && $0.alpha != 0 && $0.isEnabled } // 1
   }

}

We could add some safety measures, in case we’re not careful, which would also act as a convenience: these conditions will have to be written over and over, so we could filter out the hidden fields and the disabled ones automatically (1).

But, as stated above, the protocol shouldn’t care about what goes in textFields; for whatever reason, we might want to include a disabled text field and there would be no way to do that. If you do know for sure the project has no weird scenarios, you can add the above property and use that instead of textFields.

Next, we need a way to extract all the fields after the current one:

extension NextTextFieldHandler {

   private func fields(after textField: UITextField) -> ArraySlice<UITextField> {
      let textFields = self.textFields // 1

      guard let currentIndex = textFields.index(of: textField) else { return [] } // 2

      return textFields.suffix(from: min(currentIndex + 1, textFields.endIndex - 1)) // 3
   }

}

We first save the array in a local constant for a slight boost of efficiency (1), since the textFields property might be a computed one. At a handful of text fields this boost is negligible, but it’s still a good practice to not go through computed properties — especially when iterating — unless we actually need the property to be recalculated over and over, which we don’t need.

We then find the index of the text field passed in, or bail out if we can’t find it. Lastly, we return an ArraySlice with all the text fields starting with the one after the current.

Why use a slice? Because it keeps the original indices. So, for example, if we have [field1, field2, field3, field4] and we do a slice starting with index 2, we’d get [field3, field4], but each element would have the following indices: [2, 3]. We’ll see in just a bit why this is needed.

One last thing before we move to the two functions: a simple helper to check if there are any empty fields in the above slice:

extension NextTextFieldHandler {

   private func emptyFieldsExist(after textField: UITextField) -> Bool {
      return fields(after: textField)
         .filter { $0.text?.isEmpty != false }
         .isEmpty == false
   }

}

It only takes the fields after the current, filters the one that have text in them and checks if the result is empty.

Finally, we can start implementing:

extension NextTextFieldHandler {

   func setupReturnKeyType(for textField: UITextField) {
      let textFields = self.textFields // 1

      guard let currentIndex = textFields.index(of: textField) else { return } // 2

      let emptyFieldsExistAfterCurrent = emptyFieldsExist(after: textField)

      if currentIndex < textFields.endIndex - 1, emptyFieldsExistAfterCurrent { // 2
         textField.returnKeyType = .next
      }
      else { // 3
         textField.returnKeyType = .done
      }
   }

}

As before, we save the textFields array in a local constant (1) and we bail out if we can’t find the index of the passed in textField (2).

We then have a decision to make:

Lastly, the logic for tapping on the Next/Return key is slightly trickier and we’ll finally see why we used an ArraySlice above:

extension NextTextFieldHandler {

   func handleReturnKeyTapped(on textField: UITextField) {
      let textFields = self.textFields // 1

      guard let currentIndex = textFields.index(of: textField) else { return } // 2

      let fieldsAfterCurrent = fields(after: textField) // 3
      let nextEmptyIndex = fieldsAfterCurrent
         .firstIndex { $0.text?.isEmpty != false } // 4
         ?? textFields.index(currentIndex, offsetBy: 1, limitedBy: textFields.endIndex - 1) // 5
         ?? textFields.endIndex - 1 // 6
      let emptyFieldsExistAfterCurrent =	emptyFieldsExist(after: textField)

      if currentIndex == textFields.endIndex - 1 || !emptyFieldsExistAfterCurrent { // 7
         textField.resignFirstResponder()
      }
      else { // 8
         textFields[nextEmptyIndex].becomeFirstResponder()
      }
   }

}

As always, we first save the textFields array in a local constant (1) and we bail out if we can’t find the index of the passed in textField (2). We then get the next fields after the one passed in as a slice (3) and find the index of the first empty field (4), falling back to the next text field if that fails (5), falling back yet again on the last text field if that also fails (6).

We now have another decision to make:

And here’s where we need the ArraySlice. Let’s take the previous example to go through it:

If we wouldn’t use a slice, fields(after: field) would still return field2, field3, field4], but with indices [0, 1, 2]. Also, we don’t use filter inside fields(after:), because that would reset the indices, making them start from 0.

If we wouldn’t use a slice, we could also find field3 by using first { $0.text?.isEmpty != false } at (4), then we’d find its index by calling textFields.index(of: nextField), but I think a slice is slightly better since we can write easier fallbacks (5, 6).

How do we use it? Quite simple, actually:

extension LoginViewController: UITextFieldDelegate, NextTextFieldHandler {

   var textFields: [UITextField] { // 1
      return aStackView.arrangedSubviews
         .compactMap { $0 as? UITextField }
         .filter { !$0.isHidden && $0.alpha != 0 && $0.isEnabled } // 2
      /* 3
      if mode == .login {
         return [emailTextField, passwordTextField]
      }
      else {
         return [nameTextField, emailTextField, passwordTextField]
      }
      */
   }

   func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
      setupReturnKeyType(for: textField) // 4

      return true
   }

   func textFieldShouldReturn(_ textField: UITextField) -> Bool {
      handleReturnKeyTapped(on: textField) // 5

      return true
   }

}

First, we setup the textFields we need (1). We extract all the text fields from our imaginary UIStackView (2) (or return them manually — 3), then call setupReturnKeyType(for:) from textFieldShouldBeginEditing(:_) (4) and handleReturnKeyTapped(on:) from textFieldShouldReturn(:_) (5).

As always, I’d love to know what you think, or if anything can be improved @rolandleth.