SwiftUI:提交后留在同一个 TextField 上?

SwiftUI: stay on same TextField after on commit?

即使在用户点击键盘上的 Return 键后,SwiftUI 中是否可以将输入光标保持在同一个文本字段上?

这是我的代码:

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

因此,在用户点击键盘上的 return 键后,TextField() onCommit 应该会正常激活,但光标停留在同一个文本字段中,因此用户可以继续输入新元素。

iOS 15+

您可以使用 @FocusState 并在提交时立即将 TextField 重新设置为焦点。

示例:

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

结果:

我在 iOS 14 中通过创建自定义 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 ?? ""
        }
    }
}

并通过添加此@State 变量在 SwiftUI 视图中使用:

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

并在等待视图正文的任何​​地方自行添加文本字段代码:

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