15
votes

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?

4

4 Answers

19
votes

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

1
votes

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.

1
votes

This is still in beta as of 15 June 2021.


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

0
votes

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