18

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)
pawello2222
  • 46,897
  • 22
  • 145
  • 209
Daniel Ryan
  • 6,976
  • 5
  • 45
  • 62
  • In iOS 15 we can now use `@FocusState` to control which field should be focused - see [this answer](https://stackoverflow.com/a/68010785/8697793). – pawello2222 Jun 17 '21 at 13:05

5 Answers5

12

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
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
6

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!

Gene Z. Ragan
  • 2,643
  • 2
  • 31
  • 41
5

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

}
Ashok Khanna
  • 325
  • 2
  • 9
  • This is the correct answer and the same way that Paul Hudson solves it in his fantastic video https://www.hackingwithswift.com/books/ios-swiftui/hiding-the-keyboard – raddevus Jul 14 '23 at 22:46
4

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]))
    }
}
Daniel Ryan
  • 6,976
  • 5
  • 45
  • 62
  • 1
    when using your code every time the focus moves to another textfield or the keyboard is dismissed the following gets printed in the console === AttributeGraph: cycle detected through attribute 105 ===. Also the behavior is not natural/smooth when the keyboard disappears and it was in a modal window. A keyboard on the main view is displayed temporarily and it disappears with more `=== AttributeGraph: cycle detected through attribute 105 ===` error messages. Its seems to be due to the following portion of code `self.parent.isfocusAble = newFocus` in the `DispatchQueue.main.async` – Learn2Code Mar 16 '20 at 19:46
  • Can't say I've seen any issues myself. I'll keep an eye out. – Daniel Ryan Mar 16 '20 at 21:39
  • i am using SwiftUI, try to compile your code in SwiftUI and you will see the issue instantly – Learn2Code Mar 16 '20 at 21:53
  • another issue i am facing which i do not understand why is that `let nextResponder = textField.superview?.viewWithTag(nextTag) as UIResponder` keeps evaluating to `nil`, where it should not!? – Learn2Code Mar 16 '20 at 21:57
  • 3
    The issue is in your `func setFocus(tag: Int) {...} ` function. Specifically where you have `DispatchQueue.main.async { self.parent.isfocusAble = newFocus }` the code portion `self.parent.isfocusAble = newFocus` has the following behavior: **Modifying state during view update, this will cause undefined behavior.**, which subsequently throws the errors mentioned above. – Learn2Code Mar 17 '20 at 00:13
1

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

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77