You can create your own SwiftUI Environment…

Every now and then, I’m frustrated that SwiftUI is missing an obvious environment variable.
I really expected it to have

@Environment(.keyboardShowing) var keyboardShowing

It doesn’t. But you can make your own.
Simple example of how you use it here:

@main
struct KeyboardIndicatorApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                // Simple modifier injects the new environment variable
                .addKeyboardVisibilityToEnvironment()
        }
    }
}

struct ContentView: View {
    //You can then read it in any view
    @Environment(\.keyboardShowing) var keyboardShowing
    
    @FocusState private var isTextFieldFocused: Bool
    @State private var text:String = ""
    
    var body: some View {
        
        TextField("Type Something...", text: $text)
            .focused($isTextFieldFocused)
            .safeAreaInset(edge: .trailing) {
                //Button shows if keyboard is showing
                Button {
                    isTextFieldFocused = false
                } label: {
                    Image(systemName: "keyboard.chevron.compact.down")
                }
                .opacity(keyboardShowing ? 1 : 0)
            }
            .padding()
    }
}

In this example, we show a ‘keyboard down’ button if the keyboard is showing.
The button clears focus and hides the keyboard.

Note – this is a VERY contrived example. In this simple case, you could just use the @FocusState for the same job.

In my use case, the ‘hide keyboard’ button and the text field are in different parts of my view tree – so using the environment variable avoids having to create a more complex data-passing solution.

The implementation is based on this Stack Overflow answer, but with the addition of wrapping the publisher up into a view modifier, and using that to set an environment variable.



public extension View {
    /// Sets an environment value for keyboardShowing
    /// Access this in any child view with
    /// @Environment(\.keyboardShowing) var keyboardShowing
    /// You should add the modifier near the top of your view hierarchy (to RootView / ContentView or similar)
    /// On MacOS, this is always false
    func addKeyboardVisibilityToEnvironment() -> some View {
        modifier(KeyboardVisibility())
    }
}

private struct KeyboardShowingEnvironmentKey: EnvironmentKey {
    static let defaultValue: Bool = false
}

public extension EnvironmentValues {
    var keyboardShowing: Bool {
        get { self[KeyboardShowingEnvironmentKey.self] }
        set { self[KeyboardShowingEnvironmentKey.self] = newValue }
    }
}

private struct KeyboardVisibility:ViewModifier {
    
#if os(macOS)
    
    fileprivate func body(content: Content) -> some View {
        content
            .environment(\.keyboardShowing, false)
    }
    
#else
    
    @State var isKeyboardShowing:Bool = false
    
    private var keyboardPublisher: AnyPublisher<Bool, Never> {
        Publishers
            .Merge(
                NotificationCenter
                    .default
                    .publisher(for: UIResponder.keyboardWillShowNotification)
                    .map { _ in true },
                NotificationCenter
                    .default
                    .publisher(for: UIResponder.keyboardDidHideNotification)
                    .map { _ in false })
            .debounce(for: .seconds(0.1), scheduler: RunLoop.main)
            .eraseToAnyPublisher()
    }
    
    fileprivate func body(content: Content) -> some View {
        content
            .environment(\.keyboardShowing, isKeyboardShowing)
            .onReceive(keyboardPublisher) { value in
                isKeyboardShowing = value
            }
    }
    
#endif
}

Creating our own environment value is very simple, and wrapping the whole thing up in a view modifier makes it really clean to add to a project.

I have added this to my HSSwiftUI package of useful SwiftUI extensions.