26

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

3
  • 1
    As long as they are in the same view, both keyboards will have the done button. That is the way toolbar works. There are some pre-iOS 15 tutorials on how to add a button to a keyboard that should do what you want. See this answer. Commented Dec 29, 2021 at 2:14
  • @Yrb I almost have your suggestion working with a float, but using NumberFormatter doesn't work, and the view is taking up the rest of the screen. It's not behaving like I would expect. Is there something else I'm missing? Code. PS - thanks for your response, again! Commented Dec 29, 2021 at 16:29
  • I was able to use numberFormatter.string(from: NSNumber(value: value))!. But then you can remove the symbol. I feel like this is quite a hack compared with what I was trying to do with the iOS 15 specific stuff with SwiftUI.. Commented Dec 29, 2021 at 16:44

10 Answers 10

12
+50

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

}
Sign up to request clarification or add additional context in comments.

2 Comments

This doesn't seem to work. It looks like the conditional is evaluated once, and never again. In my testing only the initial value of field would make toolbar either appear or not appear.
Also it doesn't work without adding NavigationView :|
6

This is my solution:

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

For my project I have five TextField views on one View, so I created this method in the View's extension.

I pass the unique FocusState<Bool>.Binding value and use it in the ToolbarItemGroup closure to determine if we should display the content (Spacer, Button). If the particular TextField is focused, we display the toolbar content (all other unfocused TextFields won't).

Comments

5

I tried it a lot but I ended up in the below one.

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

Comments

2

Number Pad return solution in SwiftUI, Tool bar button over keyboard, Focused Field

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

Comments

1

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

Comments

1
 **This is my working Solution in SwiftUI**    


 TextField(text: $text, label: { label })
  .focused($isFocused)
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                if $isFocused.wrappedValue {
                    Spacer()
                    Button {
                        $isFocused.wrappedValue = false
                    } label: {
                        Text("Done")
                    }
                }
            }
        }

Comments

1

I'm a very new to SwiftUI, but you can use the isFocused flag as a condition for adding item to the toolbar for each text field.

@FocusState private var isFirstFieldFocused: Bool
@State private var firstFieldValue: String = ""
 
var body: some View {
  TextField("First label", text: $firstFieldValue)
    .focused($isFirstFieldFocused)
    .toolbar {
      if isFirstFieldFocused {
        ToolbarItemGroup(placement: .keyboard) {...}
      }
    }
}

Comments

0

I've come across many solutions, but none of them worked for me. So, I created my own custom NumberPadTextFieldWithToolbar, which is highly customizable based on your requirements. You can also extend it to create a generic TextField that supports all keyboard types.

It's a simple and straightforward solution. Hope it helps!

import UIKit
import SwiftUI

// Enum to track which text field is currently focused
enum Field {
    case firstTextField
    case secondTextField
}

// A custom UITextField wrapped for use in SwiftUI with a number pad and toolbar

struct NumberPadTextFieldWithToolbar: UIViewRepresentable {
    @Binding var text: String  // Binding to hold text input
    @FocusState var focusedField: Field?  // Tracks focus state of the text field
    var placeholder: String  // Placeholder text for the text field

    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.keyboardType = .numberPad  // Set keyboard type to number pad
        textField.delegate = context.coordinator  // Set delegate to handle text field events
        textField.text = text  // Set initial text value
        textField.placeholder = placeholder  // Set placeholder text
        
        // Apply styling to the text field
        textField.font = UIFont(name: String.ManropeFont().regular, size: 14)
        textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: textField.frame.height)) // Add left padding
        textField.leftViewMode = .always
        textField.backgroundColor = UIColor.white
        textField.layer.cornerRadius = 8
        textField.clipsToBounds = true

        // Create a toolbar with a "Done" button to dismiss the keyboard
        let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44))
        let doneButton = UIBarButtonItem(title: "Done", style: .done, target: context.coordinator, action: #selector(context.coordinator.doneButtonTapped))
        let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) // Adds spacing before the button
        toolbar.items = [flexibleSpace, doneButton]  // Add items to toolbar
        textField.inputAccessoryView = toolbar  // Attach toolbar to keyboard

        return textField
    }

    // Update the text field when the binding value changes
    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
    }

    // Create a Coordinator to handle UITextField delegate methods
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    // Coordinator class to manage text field events
    class Coordinator: NSObject, UITextFieldDelegate {
        var parent: NumberPadTextFieldWithToolbar  // Reference to parent SwiftUI view

        init(_ parent: NumberPadTextFieldWithToolbar) {
            self.parent = parent
        }

        // Called when the text field's value changes
        func textFieldDidChangeSelection(_ textField: UITextField) {
            parent.text = textField.text ?? ""  // Update binding with new text
        }

        // Called when the text field becomes active
        func textFieldDidBeginEditing(_ textField: UITextField) {
            parent.focusedField = .secondTextField  // Mark the field as focused
        }

        // Called when the text field loses focus
        func textFieldDidEndEditing(_ textField: UITextField) {
            parent.focusedField = nil  // Clear focus state
        }

        // Dismiss the keyboard when the "Done" button is tapped
        @objc func doneButtonTapped() {
            parent.focusedField = nil
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        }
    }
}

and Usage in SwiftUIViews

struct WhateverView: View {
@FocusState private var focusedField: Field?

var body: some View {
VStack{
NumberPadTextFieldWithToolbar(text: $viewModel.someText, focusedField: _focusedField, placeholder: "Some Placeholder.")
                    .focused($focusedField, equals: .secondTextField) // Use .focused modifier
                    .frame(height: 40)

        }
    }
}

Comments

-1

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

Comments

-2

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

--- update --- Hi, I updated the code to use ViewModifier to make the code easier to use and this time the code does compile and run >_<

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

--- old ---

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.