Anjali Kevadiya
Anjali Kevadiya

Reputation: 3897

How to navigate through SwiftUI TextFields by clicking on return button from keyboard?

I'm working with SwiftUI's TextField View. Mainly I have 2 questions:

  1. In Swift, we can set Return Key(Text Input Traits) to Next for TextField from storyboard like this right? For this which modifier to use in SwiftUI?

    enter image description here

  2. I have two Textfields, how to navigate to the next TextField when I click return/next button from keyboard?

Can anyone help with this using SwiftUI (not UIKit)? Or any other alternative to perform this feature?

Upvotes: 16

Views: 18569

Answers (6)

Yano
Yano

Reputation: 652

From iOS 15+, we can handle keyboard action using @FocusState and onSubmit(),

struct TextFieldFocus: View {
    enum Field {
        case userName, password
    }
    @State var field1 = ""
    @State var field2 = ""
    @FocusState var focusedField: Field?
    
    var body: some View {
        VStack(spacing: 20.0) {
            TextField("User name", text: $field1)
                .focused($focusedField, equals: .userName)
                .submitLabel(.next)
                .padding()
                .onSubmit {
                    focusedField = .password
                }
            
            SecureField("Password", text: $field2)
                .focused($focusedField, equals: .password)
                .submitLabel(.continue)
                .padding()
                .onSubmit {
                    focusedField = .userName
                }
        }
        .textFieldStyle(.roundedBorder)
    }
}

Upvotes: 2

maxial
maxial

Reputation: 1

I have written a small library that enables automatic navigation between TextFields upon submission: FormView. It also has useful features such as:

  • Validation of TextFields based on specified rules
  • Prevention of incorrect input based on specified rules

Upvotes: 0

mahan
mahan

Reputation: 14935

iOS 15.0+

macOS 12.0+, Mac Catalyst 15.0+, tvOS 15.0+, watchOS 8.0+

Use submitLabel(_:) view modifier that sets the submit label for a view. It takes a predefined case specified in SubmitLabel

Use .next. It defines a submit label with text of “Next”.

Use onFocus(_:) to find when the modified view hierarchy, in this case the TextField, loses focus. When it does, put the focus on the next view (SecureField)

struct LoginForm: View {
    enum Field: Hashable {
        case usernameField
        case passwordField
    }
    
    @State private var username = ""
    @State private var password = ""
    @FocusState private var focusedField: Field?
    
    var body: some View {
        Form {
            TextField("Username", text: $username)
                .focused($focusedField, equals: .usernameField)
                .submitLabel(.next)
                .onFocus { isFocused in
                    if (!isFocused) {
                        focusedField = .passwordField
                    }
                }
            
            SecureField("Password", text: $password)
                .focused($focusedField, equals: .passwordField)
                .submitLabel(.done)
        }
    }
}

Upvotes: 6

Parion
Parion

Reputation: 438

Based on Razib Mollick's answer and https://www.hackingwithswift.com/forums/100-days-of-swiftui/jump-focus-between-a-series-of-textfields-pin-code-style-entry-widget/765

I've come up with the following implementation for array of textfields.

struct NextLineTextField: UIViewRepresentable {
@Binding var text: String
@Binding var selectedField: Int

var tag: Int
var keyboardType: UIKeyboardType = .asciiCapable
var returnKey: UIReturnKeyType = .next

func makeUIView(context: UIViewRepresentableContext<NextLineTextField>) -> UITextField {
    let textField = UITextField(frame: .zero)
    textField.delegate = context.coordinator
    textField.keyboardType = keyboardType
    textField.returnKeyType = returnKey
    textField.tag = tag
    return textField
}

func makeCoordinator() -> NextLineTextField.Coordinator {
    return Coordinator(text: $text)
}

func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<NextLineTextField>) {
    uiView.text = text
    context.coordinator.newSelection = { newSelection in
        DispatchQueue.main.async {
            self.selectedField = newSelection
        }
    }

    if uiView.tag == self.selectedField {
        uiView.becomeFirstResponder()
    }
}

class Coordinator: NSObject, UITextFieldDelegate {

    @Binding var text: String
    var newSelection: (Int) -> () = { _ in }

    init(text: Binding<String>) {
        _text = text
    }

    func textFieldDidChangeSelection(_ textField: UITextField) {
        DispatchQueue.main.async {
            self.text = textField.text ?? ""
        }
    }
    
    func textFieldDidBeginEditing(_ textField: UITextField) {
        self.newSelection(textField.tag)
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        if textField.returnKeyType == .done {
            textField.resignFirstResponder()
        } else {
            self.newSelection(textField.tag + 1)
        }
        return true
    }
  }
}

Then make form element as

class FieldElement: ObservableObject, Identifiable {
var id = UUID()
var title = ""
@Published var value = ""
var keyboard: UIKeyboardType = .asciiCapable
var returnType: UIReturnKeyType = .next

init(title: String, value: String = "", keyboard: UIKeyboardType = 
    .asciiCapable, returnType: UIReturnKeyType = .next) {
    self.title = title
    self.value = value
    self.keyboard = keyboard
    self.returnType = returnType
  }
}

And for implementation

struct FormView: View {

@State var formElements: [FieldElement] = [
    FieldElement(title: "Name"),
    FieldElement(title: "Address"),
    FieldElement(title: "Phone Number"),
    FieldElement(title: "Email Address", keyboard: .emailAddress, returnType: 
.done),
]

@State var selectedField = 0

var body: some View {
    VStack(alignment: .leading) {
        ForEach(Array(zip(formElements.indices, formElements)), id: \.0) { 
        index, element in
            VStack(alignment: .leading, spacing: 0) {
                Text(element.title)

                NextLineTextField(text: self.$formElements[index].value,
                    selectedField: self.$selectedField,
                    tag: index,
                    keyboardType: element.keyboard,
                    returnKey: element.returnType)
                    .frame(height: 35)
                    .frame(maxWidth: .infinity)
                    .overlay(
                        RoundedRectangle(cornerRadius: 8)
                            .stroke(Color.gray.opacity(0.5), lineWidth: 0.7)
                    )
            }.padding(.bottom, 4)
        }

        Button(action: {
            print(self.formElements.map({ $0.value }))
        }) {
            Text("Print Entered Values")
                .foregroundColor(Color.white)
                .font(.body)
                .padding()
        }.frame(height: 50)
            .background(Color.green)
            .cornerRadius(8)
            .padding(.vertical, 10)
        Spacer()
    }.padding()
  }
}

If this is hard to navigate, feel free to look into https://github.com/prakshapan/Utilities/blob/master/FormView.swift

Upvotes: 1

Razib Mollick
Razib Mollick

Reputation: 5042

To resolve your two problems, you need to work with UIKit from SwiftUI. First, you need to customized TextField using UIViewRepresentable. Here is the sample code for test purposes though the code is not so elegance. I bet, there will be having a more robust solution.

  1. Inside the customized TextFieldType, the Keyboard return type has been set.
  2. By using object binding and delegate methods textFieldShouldReturn, View can focus the keyboard by updating the binding variables.

Here is the sample code:

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
        }

    }
}

Output: enter image description here

Upvotes: 20

Procrastin8
Procrastin8

Reputation: 4503

You can't, there is no concept of a responder chain in SwiftUI yet. You can't programmatically initiate focus on any View because they aren't actually the views themselves, merely structs that describe how the views should be set up. My guess it may eventually be exposed via EnvironmentValues (like line truncation, autocorrection, etc.) but it doesn't currently exist.

Upvotes: 1

Related Questions