'SwiftUI - Adding a keyboard toolbar button for only one TextField adds it for all TextFields

Background

I have two TextFields, one of which has a keyboard type of .decimalPad.

Given that there is no 'Done' button when using a decimal pad keyboard to close it, rather like the return key of the standard keyboard, I would like to add a 'Done' button within a toolbar above they keypad only for the decimal keyboard in SwiftUI.

Problem

Adding a .toolbar to any TextField for some reason adds it to all of the TextFields instead! I have tried conditional modifiers, using focussed states and checking for the Field value (but for some reason it is not set when checking, maybe an ordering thing?) and it still adds the toolbar above the keyboard for both TextFields.

How can I only have a .toolbar for my single TextField that accepts digits, and not for the other TextField that accepts a string?

Code

Please note that I've tried to make a minimal example that you can just copy and paste into Xcode and run it for yourself. With Xcode 13.2 there are some issues with displaying a keyboard for TextFields for me, especially within a sheet, so maybe simulator is required to run it properly and bring up the keyboard with 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()
    }
}



Solution 1:[1]

Try to make toolbar content conditional and move toolbar outside, like below. (No possibility to test now - just idea)

Note: test on real device

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

}

Solution 2:[2]

I've found wrapping each TextField in its own NavigationView gives each its own context and thus a unique toolbar. It feels not right and I've seen constraint warnings in the console. Use something like this:

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

Solution 3:[3]

There is work. But the other TextField will still display toolbar.

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)

with Done with Toolbar without

Solution 4:[4]

Using introspect you can do something like this in any part in your View:

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

and for the 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
    }

}

and for the CustomBarButtonItem this is a bar button item that takes a closure

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Asperi
Solution 2 sidekickr
Solution 3 Sifan
Solution 4 JAHelia