13

I added toolbar to 2 text fields, but the toolbar shows up twice:

This is the code for my custom text field:

public struct SUIDecimalField: View {
  public var body: some View {
    TextField(placeholder, text: $value)
      .focused(focused)
      .textFieldStyle(.roundedBorder)
      .keyboardType(.decimalPad)
      .toolbar {
        ToolbarItemGroup(placement: .keyboard) {
          HStack {
            Button(LOC(.clearKeyboardButton)) {
              value = ""
            }
            Spacer()
            Button(LOC(.dismissKeyboardButton)) {
              focused.wrappedValue = false
            }
          }
        }
      }
    }
  }

As you can see i only added the toolbar to the text field. I have 2 text fields, and it showed up twice.

Looks like the toolbar is somehow shared between all text fields. I am wondering how do I resolve it? I really want to put the toolbar inside the text field implementation so that the logic is encapsulated.

enter image description here

9
  • This is a known thing. The only way around it is using UIKit instead Commented Feb 26, 2022 at 19:39
  • @loremipsum I am not using uikit for my project and i don't think it's possible to convert it to uikit now. Commented Feb 26, 2022 at 19:41
  • Look up uiviewrepresentable Commented Feb 26, 2022 at 19:50
  • Look at the orange implementation here Commented Feb 26, 2022 at 19:52
  • I meant i already have lots of infrastructure based on Swift UI's TextField, and it'd be great if we can do it with SwiftUI itself, not the UIViewRepresentable Commented Feb 26, 2022 at 22:20

5 Answers 5

8

Try applying the .toolbar modifier on the Form itself and not on each individual TextField like so:

import SwiftUI

struct ContentView: View {
    
    @State private var value1 = ""
    @State private var value2 = ""
    @FocusState private var focused: Int?
    
    var body: some View {
        Form {
            MyTextField(value: $value1, tag: 1, focused: $focused)
                .focused($focused, equals: 1)
            MyTextField(value: $value2, tag: 2, focused: $focused)
                .focused($focused, equals: 2)
        }
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                HStack {
                    Button("Clear") {
                        switch focused {
                        case 1: value1 = ""
                        case 2: value2 = ""
                        default: return
                        }
                    }
                    Spacer()
                    Button("Done") {
                        focused = nil
                    }
                }
            }
        }
    }
}

struct MyTextField: View {
    
    @Binding var value: String
    var tag: Int
    var focused: FocusState<Int?>.Binding
    
    var body: some View {
        TextField("Test", text: $value)
            .textFieldStyle(.roundedBorder)
            .keyboardType(.decimalPad)
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

that solved my issue with multiple TextFields in a list. I was adding the ToolBar to on particular TextField only and it worked for all TextFields but made the scroll jerky when a TextField other than the one I added the Toolbar to was active. Moving the ToolbarModifier to the List instead of the TextField solved the problem and no more jerky scroll. Thanks.
5

Here's a possible solution:

@FocusState var isFocused: Bool
...
TextField(text: $value)
    .focused($isFocused)
    .toolbar {
        if isFocused {
            ToolbarItem(placement: .keyboard) {
                HStack {
                    Spacer()
                    Button("Done") {
                        isFocused = false
                    }
                }
            }
        } else {
            EmptyView()
        }
    }

Comments

1

this is not ideal, but it might be a start:

struct ContentView: View {
    
    @State private var value1 = ""
    @State private var value2 = ""
    @FocusState private var focused: Int?
    
    var body: some View {
        Form {
            MyTextField(value: $value1, tag: 1, focused: $focused)
                .focused($focused, equals: 1)
            
            MyTextField(value: $value2, tag: 2, focused: $focused)
                .focused($focused, equals: 2)
        }
    }
}

struct MyTextField: View {
    
    @Binding var value: String
    var tag: Int
    var focused: FocusState<Int?>.Binding
    
    var body: some View {
        TextField("Test", text: $value)
            .textFieldStyle(.roundedBorder)
            .keyboardType(.decimalPad)
        
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    HStack {
                        if focused.wrappedValue == tag {
                            Button("Clear") {
                                value = ""
                            }
                            Spacer()
                            Button("Done") {
                                focused.wrappedValue = nil
                            }
                        }
                    }
                }
            }
    }
}

5 Comments

I think if you put the if statement under HStack, there will be multiple HStack with empty views inside
interestingly, if i put the if statement outside of HStack, the toolbar doesn't show up. I don't know why
The extra empty HStacks is an issue. They take up spaces too
So if you add 2 fields, the first field's toolbar will only occupy the left half of toolbar space, and the second field's toolbar will only occupy the right half of the toolbar space
you're absolutely right, this was the only way I could somehow get rid of the duplicates ... anyway this is a SwiftUI issue which hopefully will be resolved at some point.
0

Top voted answer works but it's also forces Done button on text fields where I don't need it. I have not found an API that hides the keyboard toolbar. I have used introspect library and got it working

content
.introspect(.textField, on: .iOS(.v16, .v17, .v18)) { textField in
    let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
    let done = UIBarButtonItem(barButtonSystemItem: .done, target: nil, action: nil)

    done.action = #selector(UIResponder.resignFirstResponder)
    done.target = textField

    let toolbar = UIToolbar()
    toolbar.barStyle = .default
    toolbar.items = [flexSpace, done]
    toolbar.sizeToFit()

    textField.inputAccessoryView = toolbar
 } 

Comments

-1

I have struggled with similar situation myself, and there is no clean way. What I would suggest in your case is

  1. append a clear button (X) on the right side of the text field, this is very intuitive for the user

  2. you do not need to rewrite your code into UIKit, but can use one useful line of code (wrapped into extension of View) as described here: https://www.hackingwithswift.com/quick-start/swiftui/how-to-dismiss-the-keyboard-for-a-textfield the advantage is, that it is generic and you do not need to determine which focus state to change.

  3. add the toolbar to only one of the TextFields, then the "Dismiss" button will not be replicated in the toolbar and it will work correctly.

Not nice, not clean, but as SwiftUI as you can get and functional.

Comments

Your Answer

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