1

Background

I'm building a SwiftUI form (AddRecipeView) inside a NavigationStack. I’ve added a .toolbar(placement: .keyboard) with a “Done” button to dismiss the keyboard, especially useful for numberPad inputs.

However, the "Done" button does not appear the first time I enter this view and tap a TextField. After navigating away to some other tab and returning and hitting thre TextField again, the "Done" button shows up correctly. This behavior happens on both simulator and physical device (both tested with iOS 18.5, and XCode's version being 16.5).

Here's a simplified reproducible example, but if anyone wants to inspect the entire project, it can be found in this repo:

struct AddRecipeView: View { @State private var time: Int? = nil @State private var path = NavigationPath() var body: some View { NavigationStack(path: $path) { Form { TextField("Time (minutes)", value: $time, formatter: NumberFormatter()) .keyboardType(.numberPad) } .toolbar { ToolbarItemGroup(placement: .keyboard) { Spacer() Button("Done") { UIApplication.shared.sendAction( #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } } } } } 

Console output

When I hit on the TextField for the second time (once I have traveled to another tab and come back), I get these output console messages:

-[RTIInputSystemClient remoteTextInputSessionWithID:performInputOperation:] perform input operation requires a valid sessionID. inputModality = Keyboard, inputOperation = <null selector>, customInfoType = UIEmojiSearchOperations Invalid frame dimension (negative or non-finite). Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. ... "<NSAutoresizingMaskLayoutConstraint:0x6000021c7250 h=--& v=--& _UIToolbarContentView.width == 0 (active)>", "<NSLayoutConstraint:0x6000021aa6c0 H:|-(16)-[_UIButtonBarStackView:0x101480180] (active, names: '|':_UIToolbarContentView:0x1014a6090)>", "<NSLayoutConstraint:0x6000021a9e00 H:[_UIButtonBarStackView:0x101480180]-(16)-| (active, names: '|':_UIToolbarContentView:0x1014a6090)>" Will attempt to recover by breaking constraint <NSLayoutConstraint:0x6000021a9e00 H:[_UIButtonBarStackView:0x101480180]-(16)-| (active, names: '|':_UIToolbarContentView:0x1014a6090)> 

This seems like a layout bug—perhaps the _UIToolbarContentView is rendering with zero width until after a navigation cycle.

Things I’ve already tried:

  • Replaced placeholder views with .frame(width: 100, height: 44) to prevent zero-size issues

  • Used .submitLabel(.done) and @FocusState (but numberPad doesn’t support submit)

  • Applied the NavigationPath workaround using NavigationStack(path:) (from this post) — no luck

Question

Is this a known SwiftUI or UIKit bug in iOS 18? Is there a known way to ensure the keyboard toolbar appears correctly on first presentation?

2 Answers 2

0

You can force the toolbar to appear by adding an empty toolbar item to the main toolbar:

.toolbar { ToolbarItem(placement: .principal) { EmptyView() } // Force toolbar creation ToolbarItemGroup(placement: .keyboard) { // Your Done button } } 

Or you could create a custom UIViewRepresentable for the text field with a proper input accessory view:

struct NumberFieldWithDone: UIViewRepresentable { @Binding var value: Int? func makeUIView(context: Context) -> UITextField { let textField = UITextField() textField.keyboardType = .numberPad textField.placeholder = "Time (minutes)" let toolbar = UIToolbar() toolbar.sizeToFit() let doneButton = UIBarButtonItem( title: "Done", style: .done, target: context.coordinator, action: #selector(Coordinator.dismissKeyboard) ) toolbar.items = [UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), doneButton] textField.inputAccessoryView = toolbar return textField } func updateUIView(_ uiView: UITextField, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator { var parent: NumberFieldWithDone init(_ parent: NumberFieldWithDone) { self.parent = parent } @objc func dismissKeyboard() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } } 

The most reliable solution right now is probably the UIViewRepresentable approach, though it's more work than the native SwiftUI version. Other workarounds may help but could be fragile across iOS updates...

P.S.: The console errors you're seeing (especially the _UIToolbarContentView.width == 0 constraint) confirm this is a UIKit layout bug during the initial presentation. If possible, test with iOS 18 to see if the issue persists.

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

4 Comments

My fault @BenzyNeez, I completely forgot that we are already in "26" beta...
Hey @CaioBerkley! Thank you for your response. First of all, the iOS version I said I was using is wrong: I am using iOS 18.5 for both the simulator and the physical device, and XCode's version 16.5 (edited my question with this information). Unfortunately, none of your solutions seemed to work. Let's hope Apple fixes this one soon!
I'll try to rerun some workarounds here from my side with this new info. If I get something, I'll reach you!
I'm curious if you were able to figure out this issue because I started running into it myself. Every other ToolbarItemPlacement works except .keyboard which makes me think it's a layout bug. Both on iOS 26 and 18...
0

When testing with Xcode 26.0 beta 5 on an iPhone 16 simulator running iOS 18.5, the keyboard toolbar appears first time. But it always shows an error in the console (which you were also seeing):

Invalid frame dimension (negative or non-finite).

This error is shown if there is any kind of content with placement .keyboard. I'm guessing, this could be the cause of the issue you are seeing. I couldn't find a way to prevent the error.

As a workaround, you could try showing the button using .safeAreaInset instead, conditional on when the keyboad is showing. The post How to detect if keyboard is present in swiftui shows ways to detect when the keyboard is showing.

Here is an example that uses a FocusState variable to detect when the field has focus and this is used to control the visibility of the button. If the field has focus it does not necessarily mean that the keyboard is showing, because the user may be using an external keyboard, but it is sufficient for example purposes. It also provides a way to dismiss the keyboard:

struct AddRecipeView: View { @State private var time: Int? = nil @State private var path = NavigationPath() @FocusState private var isFocused var body: some View { NavigationStack(path: $path) { Form { TextField("Time (minutes)", value: $time, formatter: NumberFormatter()) .keyboardType(.numberPad) .focused($isFocused) } .safeAreaInset(edge: .bottom) { if isFocused { Button("Done") { isFocused = false } .padding(.horizontal) .padding(.vertical, 10) .frame(maxWidth: .infinity, alignment: .trailing) .background(.background) } } } } } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.