Navigating Text Fields in SwiftUI
As part of my recent development efforts on Altilium, I wanted to remove my dependency on using the SwiftUI-Introspect library to access the underlying UITextField
that back the SwiftUI TextField
. The reason I was using the library was because, at the time (iOS 14), there was no first-party support in SwiftUI for either adding a Toolbar
to the keyboard or customising the focus state of the TextField
in the form.
I’m not going to regurgitate Paul Hudson’s excellent articles - so for more detailed information please see:
This article is to quickly demonstrate a generalised solution I’ve come up with to adding quick keyboard toolbar based navigation to a form - a must-have for any app that utilises the .numberPad
or .decimalPad
keyboard styles.
I’ve built a simple type conforming to View
, that takes the current FocusState
from your parent view (where your TextField
’s live) and the array of all the available field types you have on that particular view:
protocol InputField: Hashable, CaseIterable {
var index: Int { get }
}
struct TextFieldNavigationView<FieldType: InputField>: View {
var focusedField: FocusState<FieldType?>.Binding
let allFields: [FieldType]
var body: some View {
Group {
Spacer()
Button {
if let f = focusedField.wrappedValue {
focusedField.wrappedValue = allFields[f.index - 1]
}
} label: {
Image(systemName: "chevron.up")
}
.disabled(focusedField.wrappedValue?.index == 0)
Button {
if let f = focusedField.wrappedValue {
focusedField.wrappedValue = allFields[f.index + 1]
}
} label: {
Image(systemName: "chevron.down")
}
.disabled(focusedField.wrappedValue == allFields.last)
}
}
}
This view simply enables or disables a chevron icon in the toolbar based on how many fields are before or after - utilising the CaseIterable
protocol and the allCases
property. So, what does an implementation on your form look like?
enum MyCustomInputField: Int, InputField {
case testing1
case testing2
case testing3
var index: Int {
rawValue
}
}
This enum conforms to the InputField
protocol we previously defined, and adds cases for each of the text fields I have on one of my screens, I’m using the Int
as a backing type to easily provide the index, but this can easily be defined in a custom manner within the index
property itself.
We then use the advice from Paul Hudson’s articles and harness the .focused
and the toolbar
view modifier. It’s worth noting, you only need one view modifier for the .toolbar
on the parent of your TextField
, e.g.
@FocusState var field: MyCustomerInputField?
/// ...
ScrollView {
VStack {
TextField("Testing 1", text: $foo)
.focused($field, equals: .testing1)
TextField("Testing 2", text: $bar)
.focused($field, equals: .testing2)
TextField("Testing 3", text: $test)
.focused($field, equals: .testing3)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
TextFieldNavigationView<MyCustomInputField>(
focusedField: $field,
allFields: MyCustomInputField.allCases
)
}
}
And that’s all there is to it! Let me know your thoughts on Twitter