Focus on the next TextField/SecureField in SwiftUI

Viewed 11365

I've built a login screen in SwiftUI. I want to focus on the password SecureField when the user is finished entering their email. How can I do this?

struct LoginView: View {
    @State var username: String = ""
    @State var password: String = ""

    var body: some View {
        ScrollView {
            VStack {
                TextField("Email", text: $username)
                    .padding()
                    .frame(width: 300)
                    .background(Color(UIColor.systemGray5))
                    .cornerRadius(5.0)
                    .padding(.bottom, 20)
                    .keyboardType(.emailAddress)

                SecureField("Password", text: $password)
                    .padding()
                    .frame(width: 300)
                    .background(Color(UIColor.systemGray5))
                    .cornerRadius(5.0)
                    .padding(.bottom, 20)

                Button(action: {

                }, label: {
                    Text("Login")
                        .padding()
                        .frame(width: 300)
                        .background((username.isEmpty || password.isEmpty) ? Color.gray : Color(UIColor.cricHQOrangeColor()))
                        .foregroundColor(.white)
                        .cornerRadius(5.0)
                        .padding(.bottom, 20)
                }).disabled(username.isEmpty || password.isEmpty)
5 Answers

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:

enter image description here

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
    }
}

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]))
    }
}

Here you go - Native SwiftUI solution. Thanks Gene Z. Ragan for the link to SwiftUI Documentation in an earlier answer

struct TextFieldTest: View {
    @FocusState private var emailFocused: Bool
    @FocusState private var passwordFocused: Bool
    @State private var username: String = ""
    @State private var password: String = ""


    var body: some View {
        TextField("User name (email address)", text: $username)
            .focused($emailFocused)
            .onSubmit {
                passwordFocused = true
            }

        TextField("Enter Password", text: $password)
            .focused($passwordFocused)
    }

}

I created a view Modifier that takes in a binding of @FocusState. This will automatically handle the next progression and clear the keyboard.

import SwiftUI

struct KeyboardToolsView<Content: View, T: Hashable & CaseIterable & RawRepresentable>: View where T.RawValue == Int {
    var focusedField: FocusState<T?>.Binding
    let content: Content
    
    init(focusedField: FocusState<T?>.Binding, @ViewBuilder content: () -> Content) {
        self.focusedField = focusedField
        self.content = content()
    }
    
    var body: some View {
        content
            .toolbar {
                ToolbarItem(placement: .keyboard) {
                    HStack {
                        Button(action: previousFocus) {
                            Image(systemName: "chevron.up")
                        }
                        .disabled(!canSelectPreviousField)
                        Button(action: nextFocus) {
                            Image(systemName: "chevron.down")
                        }
                        .disabled(!canSelectNextField)
                        Spacer()
                        Button("Done") {
                            focusedField.wrappedValue = nil
                        }
                    }
                }
            }
    }
    
    var canSelectPreviousField: Bool {
        if let currentFocus = focusedField.wrappedValue {
            return currentFocus.rawValue > 0
        } else {
            return false
        }
    }
    
    var canSelectNextField:Bool {
        if let currentFocus = focusedField.wrappedValue {
            return currentFocus.rawValue < T.allCases.count - 1
        } else {
            return false
        }
    }
    
    func previousFocus() {
        if canSelectPreviousField {
            selectPreviousField()
        }
    }
    
    func nextFocus() {
        if canSelectNextField {
            selectNextField()
        }
    }
    
    func selectPreviousField() {
        focusedField.wrappedValue = focusedField.wrappedValue.map {
            T(rawValue: $0.rawValue - 1)!
        }
    }
    
    func selectNextField() {
        focusedField.wrappedValue = focusedField.wrappedValue.map {
            T(rawValue: $0.rawValue + 1)!
        }
    }
}

struct KeyboardToolsViewModifier<T: Hashable & CaseIterable & RawRepresentable>: ViewModifier where T.RawValue == Int {
    var focusedField: FocusState<T?>.Binding
    
    func body(content: Content) -> some View {
        KeyboardToolsView(focusedField: focusedField) {
            content
        }
    }
}

extension View {
    func keyboardTools<T: Hashable & CaseIterable & RawRepresentable>(focusedField: FocusState<T?>.Binding) -> some View where T.RawValue == Int {
        self.modifier(KeyboardToolsViewModifier<T>(focusedField: focusedField))
    }
}

Example of how it can be used:

struct TransactionForm: View {
        @State private var price: Double?
        @State private var titleText: String = ""
        @State private var date: Date = .now
        
        @FocusState private var focusedField: Field?
        
        // Having an enum that is Int and CaseIterable is important. As it will allow the view modifier to properly choose the next focus item.
        private enum Field: Int, CaseIterable {
            case title, price
        }
        
        var body: some View {
            NavigationView {
                Form {
                    TextField("Title", text: $titleText)
                        .focused($focusedField, equals: .title)
                    
                    TextField("$0.00", value: $price, format: .currency(code: "USD"))
                        .focused($focusedField, equals: .price)
                        .keyboardType(.decimalPad)
                    
                    DatePicker("Date", selection: $date, displayedComponents: [.date])
                    
                    Section {
                        Button(action: {...}) {
                            Text("Add")
                        }
                    }
                    .listRowBackground(Color.clear)
                }
                .navigationTitle("Add Transaction")
                .keyboardTools(focusedField: $focusedField)
            }
        }
    }

Keyboard Toolbar View Modifier Image

Related