I'm using UITextField and UIViewRepresentable to achieve this.
Define tag of each text field and declare a list of booleans with same count of available text fields to be focused of return key, fieldFocus, that will keep track of which textfield to focus next base on the current index/tag.
Usage:
import SwiftUI
struct Sample: View {
@State var firstName: String = ""
@State var lastName: String = ""
@State var fieldFocus = [false, false]
var body: some View {
VStack {
KitTextField (
label: "First name",
text: $firstName,
focusable: $fieldFocus,
returnKeyType: .next,
tag: 0
)
.padding()
.frame(height: 48)
KitTextField (
label: "Last name",
text: $lastName,
focusable: $fieldFocus,
returnKeyType: .done,
tag: 1
)
.padding()
.frame(height: 48)
}
}
}
UITextField in UIViewRepresentable:
import SwiftUI
struct KitTextField: UIViewRepresentable {
let label: String
@Binding var text: String
var focusable: Binding<[Bool]>? = nil
var isSecureTextEntry: Binding<Bool>? = nil
var returnKeyType: UIReturnKeyType = .default
var autocapitalizationType: UITextAutocapitalizationType = .none
var keyboardType: UIKeyboardType = .default
var textContentType: UITextContentType? = nil
var tag: Int? = nil
var inputAccessoryView: UIToolbar? = nil
var onCommit: (() -> Void)? = nil
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
textField.placeholder = label
textField.returnKeyType = returnKeyType
textField.autocapitalizationType = autocapitalizationType
textField.keyboardType = keyboardType
textField.isSecureTextEntry = isSecureTextEntry?.wrappedValue ?? false
textField.textContentType = textContentType
textField.textAlignment = .left
if let tag = tag {
textField.tag = tag
}
textField.inputAccessoryView = inputAccessoryView
textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
uiView.isSecureTextEntry = isSecureTextEntry?.wrappedValue ?? false
if let focusable = focusable?.wrappedValue {
var resignResponder = true
for (index, focused) in focusable.enumerated() {
if uiView.tag == index && focused {
uiView.becomeFirstResponder()
resignResponder = false
break
}
}
if resignResponder {
uiView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UITextFieldDelegate {
let control: KitTextField
init(_ control: KitTextField) {
self.control = control
}
func textFieldDidBeginEditing(_ textField: UITextField) {
guard var focusable = control.focusable?.wrappedValue else { return }
for i in 0...(focusable.count - 1) {
focusable[i] = (textField.tag == i)
}
control.focusable?.wrappedValue = focusable
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard var focusable = control.focusable?.wrappedValue else {
textField.resignFirstResponder()
return true
}
for i in 0...(focusable.count - 1) {
focusable[i] = (textField.tag + 1 == i)
}
control.focusable?.wrappedValue = focusable
if textField.tag == focusable.count - 1 {
textField.resignFirstResponder()
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
control.onCommit?()
}
@objc func textFieldDidChange(_ textField: UITextField) {
control.text = textField.text ?? ""
}
}
}
