'SwiftUI - Adding a keyboard toolbar button for only one TextField adds it for all TextFields
Background
I have two TextField
s, 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 TextField
s 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 TextField
s.
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 TextField
s 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)
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 |