Hussein
Hussein

Reputation: 623

SwiftUI: stay on same TextField after on commit?

is it possible in SwiftUI to keep the typing cursor on the same Textfield even after the user taps on Return key on keyboard ?

Here is my code:

struct RowView: View {
    @Binding var checklistItem: ChecklistItem
    @ObservedObject var checklist = Checklist()
    @ObservedObject var viewModel: ChecklistViewModel
    var body: some View {
        HStack {
            Button {
                self.checklistItem.isChecked.toggle()
                self.viewModel.updateChecklist(checklistItem)
            } label: {
                Circle()
                    .strokeBorder(checklistItem.isChecked ? checklistSelected : contentPrimary, lineWidth: checklistItem.isChecked ? 6 : 2)
                    .foregroundColor(backgroundSecondary)
                    .clipShape(Circle())
                    .frame(width: 16, height: 16)
            }.buttonStyle(BorderlessButtonStyle())
            // swiftlint:disable trailing_closure
            TextField(
                "Add...",
                text: $checklistItem.name,
                onCommit: {
                    do {
                        if !checklistItem.name.isEmpty {
                            self.viewModel.updateChecklist(checklistItem)
                            self.checklistItem.name = checklistItem.name
                        }
                    }
                }
            )
            // swiftlint:enable trailing_closure
            .foregroundColor(checklistItem.isChecked ? contentTertiary : contentPrimary)
            Spacer()
        }
    }
}

So after the user taps on return key on keyboard, TextField() onCommit should be activated normally but the cursor stays in that same textfield so the user can keep typing in new elements.

Upvotes: 3

Views: 2639

Answers (2)

Hussein
Hussein

Reputation: 623

I was able to achieve this in iOS 14 by creating a custom TextField class:

struct AlwaysActiveTextField: UIViewRepresentable {
    let placeholder: String
    @Binding var text: String
    var focusable: Binding<[Bool]>?
    var returnKeyType: UIReturnKeyType = .next
    var autocapitalizationType: UITextAutocapitalizationType = .none
    var keyboardType: UIKeyboardType = .default
    var isSecureTextEntry: Bool
    var tag: Int
    var onCommit: () -> Void

    func makeUIView(context: Context) -> UITextField {
        let activeTextField = UITextField(frame: .zero)
        activeTextField.delegate = context.coordinator
        activeTextField.placeholder = placeholder
        activeTextField.font = .systemFont(ofSize: 14)
        activeTextField.attributedPlaceholder = NSAttributedString(
            string: placeholder,
            attributes: [NSAttributedString.Key.foregroundColor: UIColor(contentSecondary)]
        )
        activeTextField.returnKeyType = returnKeyType
        activeTextField.autocapitalizationType = autocapitalizationType
        activeTextField.keyboardType = keyboardType
        activeTextField.isSecureTextEntry = isSecureTextEntry
        activeTextField.textAlignment = .left
        activeTextField.tag = tag
        // toolbar
        if keyboardType == .numberPad { // keyboard does not have next so add next button in the toolbar
            var items = [UIBarButtonItem]()
            let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)

            let toolbar: UIToolbar = UIToolbar()
            toolbar.sizeToFit()
            let nextButton = UIBarButtonItem(title: "Next", style: .plain, target: context.coordinator, action: #selector(Coordinator.showNextTextField))
            items.append(contentsOf: [spacer, nextButton])
            toolbar.setItems(items, animated: false)
            activeTextField.inputAccessoryView = toolbar
        }
        // Editin listener
        activeTextField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)

        return activeTextField
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text

        if let focusable = focusable?.wrappedValue {
            if focusable[uiView.tag] { // set focused
                uiView.becomeFirstResponder()
            } else { // remove keyboard
                uiView.resignFirstResponder()
            }
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    final class Coordinator: NSObject, UITextFieldDelegate {
        let activeTextField: AlwaysActiveTextField
        var hasEndedViaReturn = false
        weak var textField: UITextField?

        init(_ activeTextField: AlwaysActiveTextField) {
            self.activeTextField = activeTextField
        }

        func textFieldDidBeginEditing(_ textField: UITextField) {
            self.textField = textField
            guard let textFieldCount = activeTextField.focusable?.wrappedValue.count else { return }
            var focusable: [Bool] = Array(repeating: false, count: textFieldCount) // remove focus from all text field
            focusable[textField.tag] = true // mark current textField focused
            activeTextField.focusable?.wrappedValue = focusable
        }
        // work around for number pad
        @objc
        func showNextTextField() {
            if let textField = self.textField {
                _ = textFieldShouldReturn(textField)
            }
        }

        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            hasEndedViaReturn = true
            guard var focusable = activeTextField.focusable?.wrappedValue else {
                textField.resignFirstResponder()
                return true
            }
            focusable[textField.tag] = true // mark current textField focused
            activeTextField.focusable?.wrappedValue = focusable
            activeTextField.onCommit()
            return true
        }

        func textFieldDidEndEditing(_ textField: UITextField) {
            if !hasEndedViaReturn {// user dismisses keyboard
                guard let textFieldCount = activeTextField.focusable?.wrappedValue.count else { return }
                // reset all text field, so that makeUIView cannot trigger keyboard
                activeTextField.focusable?.wrappedValue = Array(repeating: false, count: textFieldCount)
            } else {
                hasEndedViaReturn = false
            }
        }
        @objc
        func textFieldDidChange(_ textField: UITextField) {
            activeTextField.text = textField.text ?? ""
        }
    }
}

and use in in the SwiftUI view by adding this @State variable:

@State var fieldFocus: [Bool] = [false]

and add the Textfield code it self anywhere waiting the view body:

AlwaysActiveTextField(
                            placeholder: "Add...",
                            text: $newItemName,
                            focusable: $fieldFocus,
                            returnKeyType: .next,
                            isSecureTextEntry: false,
                            tag: 0,
                            onCommit: {
                                print("any action you want on commit")
                            }
                        )

Upvotes: 4

George
George

Reputation: 30341

iOS 15+

You can use @FocusState and, on commit, immediately set the TextField to have focus again.

Example:

struct ContentView: View {
    @State private var text = "Hello world!"
    @FocusState private var isFieldFocused: Bool

    var body: some View {
        Form {
            TextField("Field", text: $text, onCommit: {
                isFieldFocused = true
                print("onCommit")
            })
            .focused($isFieldFocused)
        }
    }
}

Result:

Result

Upvotes: 5

Related Questions