SwiftUI - 为仅一个TextField添加键盘工具栏按钮会将其添加到所有TextField中

19

背景

我有两个TextField,其中一个的键盘类型是.decimalPad

考虑到在使用十进制小数键盘时没有“完成”按钮来关闭它,而是像标准键盘的返回键一样,我想在SwiftUI中为十进制小数键盘上方的工具栏中添加一个“完成”按钮,仅适用于小数键盘。

问题

.toolbar添加到任何TextField都会将其添加到所有TextField中! 我尝试过条件修饰符,使用焦点状态以及检查字段值(但由于某些原因,当检查时它未设置,可能是由于排序问题?),但它仍会为两个TextField添加工具栏。

我如何仅为接受数字的单个TextField添加.toolbar,而不是其他接受字符串的TextField

代码

请注意,我尝试制作了一个最小化的示例,您可以将其复制并粘贴到Xcode中并运行。 对于我来说,在Xcode 13.2中显示TextField键盘存在一些问题,特别是在对话框中,因此可能需要模拟器才能正确运行并使用cmd+K带来键盘。

import SwiftUI

struct TestKeyboard: View {
    @State var str: String = ""
    @State var num: Float = 1.2

    @FocusState private var focusedField: Field?
    private enum Field: Int, CaseIterable {
        case amount
        case str
    }

    var body: some View {
        VStack {
            Spacer()
            
            // I'm not adding .toolbar here...
            TextField("A text field here", text: $str)
                .focused($focusedField, equals: .str)

            // I'm only adding .toolbar here, but it still shows for the one above..
            TextField("", value: $num, formatter: FloatNumberFormatter())
                .keyboardType(.decimalPad)
                .focused($focusedField, equals: .amount)
                .toolbar {
                    ToolbarItem(placement: .keyboard) {
                        Button("Done") {
                            focusedField = nil
                        }
                    }
                }

            Spacer()
        }
    }
}

class FloatNumberFormatter: NumberFormatter {
    override init() {
        super.init()
        
        self.numberStyle = .currency        
        self.currencySymbol = "€"
        self.minimumFractionDigits = 2
        self.maximumFractionDigits = 2
        self.locale = Locale.current
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
}

// So you can preview it quickly
struct TestKeyboard_Previews: PreviewProvider {
    static var previews: some View {
        TestKeyboard()
    }
}


1
只要它们在同一个视图中,两个键盘都将有“完成”按钮。这就是 toolbar 的工作方式。有一些关于如何向键盘添加一个应该做你想要的事情的按钮的 iOS 15 之前的教程。请参见此答案 - Yrb
@Yrb 我差不多用浮点数实现了你的建议,但是使用NumberFormatter不起作用,而且视图占据了屏幕的其余部分。它的行为不像我期望的那样。我还有什么遗漏的吗?代码。PS-再次感谢您的回复! - Jimbo
我能够使用numberFormatter.string(from: NSNumber(value: value))!。但是然后你可以删除符号。我觉得与我尝试使用iOS 15特定的SwiftUI功能相比,这相当于一个hack。 - Jimbo
对我来说,工具栏出现在其他呈现的视图上,但以“灰色禁用”的形式出现。同时也无法释放焦点 :( - Serj Rubens
7个回答

6

尝试让工具栏内容有条件地移动到外部,如下所示。(现在没有测试的可能性 - 只是想法)

注意:在真实设备上进行测试

var body: some View {
    VStack {
        Spacer()
        
        TextField("A text field here", text: $str)
            .focused($focusedField, equals: .str)

        TextField("", value: $num, formatter: FloatNumberFormatter())
            .focused($focusedField, equals: .amount)
            .keyboardType(.decimalPad)

        Spacer()
    }
    .toolbar {          // << here !!
        ToolbarItem(placement: .keyboard) {
            if field == .amount {             // << here !!
               Button("Done") {
                  focusedField = nil
               }
            }
        }
    }

}

10
似乎这个方法不起作用。看起来条件语句只会被评估一次,然后就再也没有被执行过了。在我的测试中,只有field的初始值能够决定工具栏是否出现。 - Alex

5

我尝试了很多次,但最终选择了下面这个。

    .focused($focusedField, equals: .zip)
                .toolbar{
                    ToolbarItem(placement: .keyboard) {
                        switch focusedField{
                        case .zip:
                            HStack{
                                Spacer()
                                Button("Done"){
                                    focusedField = nil
                                }
                            }
                        default:
                                Text("")
                        }
                    }
                }

1
成功了。这应该被标记为正确答案。谢谢。 - undefined
成功了。这个应该标记为正确答案。谢谢。 - fs_tigre

3
这是我的解决方案:
func textFieldSection(title: String,
                      text: Binding<String>,
                      keyboardType: UIKeyboardType,
                      focused: FocusState<Bool>.Binding,
                      required: Bool) -> some View {
    TextField(
        vm.placeholderText(isRequired: required),
        text: text
    )
    .focused(focused)
    .toolbar {
        ToolbarItemGroup(placement: .keyboard) {
            if focused.wrappedValue {
                Spacer()
                Button {
                    focused.wrappedValue = false
                } label: {
                    Text("Done")
                }
            }
        }
    }
}

我的项目中在一个视图上有五个TextField视图,所以我在视图的扩展中创建了这个方法。

我传递唯一的FocusState<Bool>.Binding值,并在ToolbarItemGroup闭包中使用它来确定是否应该显示内容(Spacer,Button)。如果特定的TextFieldfocused,我们就会显示工具栏内容(所有其他未聚焦的TextField不会显示)。


唯一正确的答案。 - Rob

1

使用introspect,您可以在视图中的任何部分执行以下操作:

.introspectTextField { textField in
                    textField.inputAccessoryView = UIView.getKeyboardToolbar {
                        textField.resignFirstResponder()
                    }
            }

关于getKeyboardToolbar

extension UIView {

static func getKeyboardToolbar( _ callback: @escaping (()->()) ) -> UIToolbar {
        let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44))
        let doneButton = CustomBarButtonItem(title: "Done".localized, style: .done) { _ in
            callback()
        }
        
        let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        toolBar.items = [space, doneButton]
        
        return toolBar
    }

}

对于 CustomBarButtonItem,它是一个接受闭包的工具栏按钮项。

import UIKit

class CustomBarButtonItem: UIBarButtonItem {
    typealias ActionHandler = (UIBarButtonItem) -> Void

    private var actionHandler: ActionHandler?

    convenience init(image: UIImage?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
        self.init(image: image, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
        target = self
        self.actionHandler = actionHandler
    }

    convenience init(title: String?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
        self.init(title: title, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
        target = self
        self.actionHandler = actionHandler
    }

    convenience init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem, actionHandler: ActionHandler?) {
        self.init(barButtonSystemItem: systemItem, target: nil, action: #selector(barButtonItemPressed(sender:)))
        target = self
        self.actionHandler = actionHandler
    }

    @objc func barButtonItemPressed(sender: UIBarButtonItem) {
        actionHandler?(sender)
    }
}

不知为何我不太喜欢这个库,但它是唯一对我有效的答案。谢谢。 - Serj Rubens

1

SwiftUI中的数字键盘返回解决方案, 键盘上方的工具栏按钮, 聚焦字段

struct NumberOfBagsView:View{
   @FocusState var isInputActive: Bool
   @State var phoneNumber:String = ""
   TextField("Place holder",
                  text: $phoneNumber,
                  onEditingChanged: { _ in
                
            //do actions while writing something in text field like text limit
            
        })
        .keyboardType(.numberPad)
        .focused($isInputActive)
        .toolbar {
            ToolbarItem(placement: .keyboard) {

                Button("Done") {
                    print("done clicked")
                    isInputActive = false
                }
                
            }
        }
}

-1
我发现将每个TextField包装在自己的NavigationView中,可以为每个文本框提供自己的上下文,从而获得独特的工具栏。但这种做法感觉不太对,并且我在控制台中看到了约束警告。建议使用类似以下的方式:
   var body: some View {
    VStack {
        Spacer()
        
        // I'm not adding .toolbar here...
        NavigationView {
          TextField("A text field here", text: $str)
            .focused($focusedField, equals: .str)
        }
        // I'm only adding .toolbar here, but it still shows for the one above..
        NavigationView {
          TextField("", value: $num, formatter: FloatNumberFormatter())
            .keyboardType(.decimalPad)
            .focused($focusedField, equals: .amount)
            .toolbar {
                ToolbarItem(placement: .keyboard) {
                    Button("Done") {
                        focusedField = nil
                    }
                }
            }
        }
        Spacer()
    }
}

1
那对我没用。猜测是因为我的父视图中也有NavigationView。 - Serj Rubens

-2

有工作要做。但是另一个文本字段仍然会显示工具栏。

--- 更新 --- 嗨,我更新了代码,使用ViewModifier使代码更易于使用,这次代码确实编译并运行了 >_<

struct ToolbarItemWithShow<Toolbar>: ViewModifier where Toolbar: View {
    var show: Bool
    let toolbar: Toolbar
    let placement: ToolbarItemPlacement
    
    func body(content: Content) -> some View {
        content.toolbar {
            ToolbarItemGroup(placement: placement) {
                ZStack(alignment: .leading) {
                    if show {
                        HStack { toolbar }
                            .frame(width: UIScreen.main.bounds.size.width - 12)
                    }
                }
            }
        }
    }
}

extension View {
    func keyboardToolbar<ToolBar>(_ show: Bool, @ViewBuilder toolbar: () -> ToolBar) -> some View where ToolBar: View {
        modifier(ToolbarItemWithShow(show: show, toolbar: toolbar(), placement: .keyboard))
    }
}


struct ContentView: View {
    private enum Field: Hashable {
        case name
        case age
        case gender
    }
    
    @State var name = "Ye"
    @State var age = "14"
    @State var gender = "man"
    
    @FocusState private var focused: Field?

    
    var body: some View {
        VStack {
            TextField("Name", text: $name)
                .focused($focused, equals: .name)
                .keyboardToolbar(focused == .name) {
                    Text("Input Name")
                }
            
            TextField("Age", text: $age)
                .focused($focused, equals: .age)
                .keyboardToolbar(focused == .age) {
                    Text("Input Age")
                }

            TextField("Gender", text: $gender)
                .focused($focused, equals: .gender)
                .keyboardToolbar(focused == .gender) {
                    Text("Input Sex")
                }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

--- 旧的 ---

struct TextFieldWithToolBar<Label, Toolbar>: View where Label: View, Toolbar: View {
    @Binding public var text: String
    public let toolbar: Toolbar?

    @FocusState private var focus: Bool

    var body: some View {
        TextField(text: $text, label: { label })
            .focused($focus)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    ZStack(alignment: .leading) {
                        if focus {
                            HStack {
                                toolbar
                                Spacer()
                                Button("Done") {
                                    focus = false
                                }
                            }
                            .frame(width: UIScreen.main.bounds.size.width - 12)
                        }
                    }
                }
            }
    }
}

TextFieldWithToolBar("Name", text: $name)
TextFieldWithToolBar("Name", text: $name){
    Text("Only Brand")
}
TextField("Name", "Set The Name", text: $name)

完成后 带有工具栏 不带


你是否忘记添加标签变量?请尝试编译您的代码。另外,我不确定TextField(SwiftUI 3)是否有这样的构造函数。对我来说它不起作用。 - Serj Rubens
1
抱歉,我太粗心了,我更新了代码。请确保这次代码能正常工作。 - Sifan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接