Focus on the next TextField/SecureField in SwiftUI
I've improved on the answer from Gene Z. Ragan and Razib Mollick. Fixes a crash, this allows for any amount of textfields, supports passwords and made it into its own class.
struct UITextFieldView: UIViewRepresentable {
let contentType: UITextContentType
let returnVal: UIReturnKeyType
let placeholder: String
let tag: Int
@Binding var text: String
@Binding var isfocusAble: [Bool]
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.textContentType = contentType
textField.returnKeyType = returnVal
textField.tag = tag
textField.delegate = context.coordinator
textField.placeholder = placeholder
textField.clearButtonMode = UITextField.ViewMode.whileEditing
if textField.textContentType == .password || textField.textContentType == .newPassword {
textField.isSecureTextEntry = true
}
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
if uiView.window != nil {
if isfocusAble[tag] {
if !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
} else {
uiView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: UITextFieldView
init(_ textField: UITextFieldView) {
self.parent = textField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
// Without async this will modify the state during view update.
DispatchQueue.main.async {
self.parent.text = textField.text ?? ""
}
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
setFocus(tag: parent.tag)
return true
}
func setFocus(tag: Int) {
let reset = tag >= parent.isfocusAble.count || tag < 0
if reset || !parent.isfocusAble[tag] {
var newFocus = [Bool](repeatElement(false, count: parent.isfocusAble.count))
if !reset {
newFocus[tag] = true
}
// Without async this will modify the state during view update.
DispatchQueue.main.async {
self.parent.isfocusAble = newFocus
}
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
setFocus(tag: parent.tag + 1)
return true
}
}
}
struct UITextFieldView_Previews: PreviewProvider {
static var previews: some View {
UITextFieldView(contentType: .emailAddress,
returnVal: .next,
placeholder: "Email",
tag: 0,
text: .constant(""),
isfocusAble: .constant([false]))
}
}
When using UIKit, one would accomplish this by setting up the responder chain. This isn't available in SwiftUI, so until there is a more sophisticated focus and responder system, you can make use of the onEditingChanged
changed of TextField
You will then need to manage the state of each field based on stored State variables. It may end up being more work than you want to do.
Fortunately, you can fall back to UIKit in SwiftUI by using UIViewRepresentable.
Here is some code that manages the focus of text fields using the UIKit responder system:
import SwiftUI
struct KeyboardTypeView: View {
@State var firstName = ""
@State var lastName = ""
@State var focused: [Bool] = [true, false]
var body: some View {
Form {
Section(header: Text("Your Info")) {
TextFieldTyped(keyboardType: .default, returnVal: .next, tag: 0, text: self.$firstName, isfocusAble: self.$focused)
TextFieldTyped(keyboardType: .default, returnVal: .done, tag: 1, text: self.$lastName, isfocusAble: self.$focused)
Text("Full Name :" + self.firstName + " " + self.lastName)
}
}
}
}
struct TextFieldTyped: UIViewRepresentable {
let keyboardType: UIKeyboardType
let returnVal: UIReturnKeyType
let tag: Int
@Binding var text: String
@Binding var isfocusAble: [Bool]
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.keyboardType = self.keyboardType
textField.returnKeyType = self.returnVal
textField.tag = self.tag
textField.delegate = context.coordinator
textField.autocorrectionType = .no
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if isfocusAble[tag] {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldTyped
init(_ textField: TextFieldTyped) {
self.parent = textField
}
func updatefocus(textfield: UITextField) {
textfield.becomeFirstResponder()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if parent.tag == 0 {
parent.isfocusAble = [false, true]
parent.text = textField.text ?? ""
} else if parent.tag == 1 {
parent.isfocusAble = [false, false]
parent.text = textField.text ?? ""
}
return true
}
}
}
You can refer to this question to get more information about this particular approach.
Hope this helps!
iOS 15+
In iOS 15 we can now use @FocusState
to control which field should be focused.
Here is an example how to add buttons above the keyboard to focus the previous/next field:
struct ContentView: View {
@State private var email: String = ""
@State private var username: String = ""
@State private var password: String = ""
@FocusState private var focusedField: Field?
var body: some View {
NavigationView {
VStack {
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
TextField("Username", text: $username)
.focused($focusedField, equals: .username)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
}
.toolbar {
ToolbarItem(placement: .keyboard) {
Button(action: focusPreviousField) {
Image(systemName: "chevron.up")
}
.disabled(!canFocusPreviousField()) // remove this to loop through fields
}
ToolbarItem(placement: .keyboard) {
Button(action: focusNextField) {
Image(systemName: "chevron.down")
}
.disabled(!canFocusNextField()) // remove this to loop through fields
}
}
}
}
}
extension ContentView {
private enum Field: Int, CaseIterable {
case email, username, password
}
private func focusPreviousField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue - 1) ?? .password
}
}
private func focusNextField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue + 1) ?? .email
}
}
private func canFocusPreviousField() -> Bool {
guard let currentFocusedField = focusedField else {
return false
}
return currentFocusedField.rawValue > 0
}
private func canFocusNextField() -> Bool {
guard let currentFocusedField = focusedField else {
return false
}
return currentFocusedField.rawValue < Field.allCases.count - 1
}
}