How to Fix toolbar Issues in SwiftUI When Using the Keyboard


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.