Photo by eleven x on Unsplash

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:

See the Next button if there are any fields after the current one; preferably just ones that are still empty.

Tapping the Next button takes us to the correct field.

See the Return button if there are no more fields after the current one; preferably taking empty ones into consideration.

Tapping the Return button resigns the current responder.

Everything should work automatically, no matter the view and no matter how many fields there are.

Everything should probably be encapsulated in a protocol.

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

A list of text fields (1).

A method that handles the text fields’ editing begin, to display the correct button (2).

A method that handles the text fields’ editing end/return press, to perform the correct action (3).

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:

If we’re not on the last textField and we have empty fields after the current, the return key should be .next (2).

and we have empty fields after the current, the return key should be (2). If we’re on the last textField , or all fields after the current one are filled, the return key should be .done (3).

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:

If we’re on the last textField or all fields after the current one are filled, resign first responder (7).

or all fields after the current one are filled, resign first responder (7). If we’re not on the last textField and we have empty fields after the current one, focus the first empty textField after the current one (8).

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

textFields = [field1, field2, field3, field4] .

. We’re on field1 , and the first empty text field is field3 , which has an index of 2.

, and the first empty text field is , which has an index of 2. fields(after: field1) return [field2, field3, field4] with indices [1, 2, 3] .

return with indices . firstIndex { $0.text?.isEmpty != false } (4) would return index 2 , as it should.

(4) would return index , as it should. textFields[nextEmptyIndex].becomeFirstResponder() will focus the correct field, field3 .

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.