Greetings, traveler!
SwiftUI’s .toolbar
modifier is a useful and flexible way to attach UI controls to a view, especially when used with the keyboard. In many apps, it’s common to display a “Done” button above the keyboard or provide contextual actions relevant to the current text input.
However, in practice, we can achieve an inconsistent behavior when .toolbar
is used in combination with the keyboard.
The Problem
The problem is that the .toolbar
content can fail to appear when the keyboard is shown.
This behavior seems particularly inconsistent when .toolbar
is used inside navigation stacks. There is no confirmed fix from Apple as of writing, and because the bug is intermittent, it’s difficult to isolate.
A Practical Workaround
To mitigate this, I opted for a custom solution that doesn’t rely on .toolbar
but instead conditionally inserts a view above the keyboard using .safeAreaInset
. This approach uses keyboard show/hide notifications to toggle visibility manually.
The core of the solution is a KeyboardToolbarModifier
that listens for keyboard events:
private struct KeyboardToolbarModifier<Toolbar: View>: ViewModifier {
@State private var isKeyboardShown = false
private let toolbar: Toolbar
init(@ViewBuilder toolbar: () -> Toolbar) {
self.toolbar = toolbar()
}
func body(content: Content) -> some View {
content
.safeAreaInset(edge: .bottom) {
if isKeyboardShown {
toolbar
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
withAnimation(.easeInOut.delay(0.15)) {
isKeyboardShown = true
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
isKeyboardShown = false
}
}
}
This approach avoids relying on .toolbar
, which may be influenced by internal SwiftUI layout behaviors, and instead uses layout primitives to insert the view when needed. It also provides clear control over visibility and animation timing.
Now, we can create a function like this:
extension View {
func keyboardToolbar<Content: View>(@ViewBuilder content: @escaping () -> Content) -> some View {
modifier(KeyboardToolbarModifier(toolbar: content))
}
}
Or even something more specific:
extension View {
func keyboardToolbarDoneButton() -> some View {
modifier(
KeyboardToolbarModifier {
HStack {
Spacer()
Button(Localization.done) {
UIApplication
.shared
.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil
)
}
.padding(.horizontal)
}
.frame(height: 46)
.background(.bar)
}
)
}
}
Final Thoughts
This workaround gives predictable behavior and works well in production. If you’re seeing unpredictable toolbar behavior in SwiftUI when dealing with the keyboard, this alternative may offer a more stable user experience—until the underlying issues are resolved in future SwiftUI releases.
If you enjoyed this article, please feel free to follow me on my social media: