swifttextfieldswiftuifirst-responder

How to move to next TextField in SwiftUI?


Using Swift5.1.2, iOS13.2, Xcode-11.2,

Having several TextFields in a Stackview, I would like to move to the next TextField as soon as the user types x-amount of characters into the first TextField.

With this link, I achieve to recognise when a TextField entry has reached x-amount of characters. However, I do not know how to make the firstResponder jump to a second TextField inside my StackView.

Is there a solution to this with SwiftUI ?


Solution

  • iOS 15+

    Use @FocusState

    Before iOS 15

    I've taken @Philip Borbon answer and cleaned it up a little bit. I've removed a lot of the customization and kept in the bare minimum to make it easier to see what's required.

    struct CustomTextfield: UIViewRepresentable {
        let label: String
        @Binding var text: String
        
        var focusable: Binding<[Bool]>? = nil
        
        var returnKeyType: UIReturnKeyType = .default
        
        var tag: Int? = nil
        
        var onCommit: (() -> Void)? = nil
        
        func makeUIView(context: Context) -> UITextField {
            let textField = UITextField(frame: .zero)
            textField.placeholder = label
            textField.delegate = context.coordinator
            
            textField.returnKeyType = returnKeyType
            
            if let tag = tag {
                textField.tag = tag
            }
            
            return textField
        }
        
        func updateUIView(_ uiView: UITextField, context: Context) {
            uiView.text = text
            
            if let focusable = focusable?.wrappedValue {
                var resignResponder = true
                
                for (index, focused) in focusable.enumerated() {
                    if uiView.tag == index && focused {
                        uiView.becomeFirstResponder()
                        resignResponder = false
                        break
                    }
                }
                
                if resignResponder {
                    uiView.resignFirstResponder()
                }
            }
        }
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
        
        final class Coordinator: NSObject, UITextFieldDelegate {
            let parent: CustomTextfield
            
            init(_ parent: CustomTextfield) {
                self.parent = parent
            }
            
            func textFieldDidBeginEditing(_ textField: UITextField) {
                guard var focusable = parent.focusable?.wrappedValue else { return }
                
                for i in 0...(focusable.count - 1) {
                    focusable[i] = (textField.tag == i)
                }
                parent.focusable?.wrappedValue = focusable
            }
            
            func textFieldShouldReturn(_ textField: UITextField) -> Bool {
                guard var focusable = parent.focusable?.wrappedValue else {
                    textField.resignFirstResponder()
                    return true
                }
                
                for i in 0...(focusable.count - 1) {
                    focusable[i] = (textField.tag + 1 == i)
                }
                
                parent.focusable?.wrappedValue = focusable
                
                if textField.tag == focusable.count - 1 {
                    textField.resignFirstResponder()
                }
                
                return true
            }
            
            @objc func textFieldDidChange(_ textField: UITextField) {
                parent.text = textField.text ?? ""
            }
        }
    }