46

I want to know if the keyboard is present when the button is pressed. How would I do this? I have tried but I don't have any luck. Thanks.

8 Answers 8

71

Using this protocol, KeyboardReadable, you can conform to any View and get keyboard updates from it.

KeyboardReadable protocol:

import Combine
import UIKit


/// Publisher to read keyboard changes.
protocol KeyboardReadable {
    var keyboardPublisher: AnyPublisher<Bool, Never> { get }
}

extension KeyboardReadable {
    var keyboardPublisher: AnyPublisher<Bool, Never> {
        Publishers.Merge(
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillShowNotification)
                .map { _ in true },
            
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillHideNotification)
                .map { _ in false }
        )
        .eraseToAnyPublisher()
    }
}

It works by using Combine and creating a publisher so we can receive the keyboard notifications.

With an example view of how it can be applied:

struct ContentView: View, KeyboardReadable {
    
    @State private var text: String = ""
    @State private var isKeyboardVisible = false
    
    var body: some View {
        TextField("Text", text: $text)
            .onReceive(keyboardPublisher) { newIsKeyboardVisible in
                print("Is keyboard visible? ", newIsKeyboardVisible)
                isKeyboardVisible = newIsKeyboardVisible
            }
    }
}

You can now read from the isKeyboardVisible variable to know if the keyboard is visible.

When the TextField is active with the keyboard showing, the following prints:

Is keyboard visible? true

When the keyboard is then hidden upon hitting return, the following prints instead:

Is keyboard visible? false

You can use keyboardWillShowNotification/keyboardWillHideNotification to update as soon as they keyboard starts to appear or disappear, and the keyboardDidShowNotification/keyboardDidHideNotification variants to update after the keyboard has appeared or disappeared. I prefer the will variant because the updates are instant for when the keyboard shows.

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

4 Comments

To be strict you should use DidShow/DidHide notifications for PO question, because Will* variants are posted before asked state. Sometimes it is important.
@Asperi Good point - I have added that now, thanks! I typically prefer the will variant because the updates are instantly, whereas the did variant you have to wait for the whole appearing/disappearing animation.
This is a really clever answer. I try and think of ways to use Combine and I would never have thought of this. But I am clever enough to capitalise on it!
This solution does not work will with modern Swift concurrency. The compiler throws the following error: Main actor-isolated property 'keyboardPublisher' cannot be used to satisfy nonisolated protocol requirement
32

My little improvement @George's answer.

Implement publisher right inside the View protocol

import Combine

extension View {

  var keyboardPublisher: AnyPublisher<Bool, Never> {
    Publishers
      .Merge(
        NotificationCenter
          .default
          .publisher(for: UIResponder.keyboardWillShowNotification)
          .map { _ in true },
        NotificationCenter
          .default
          .publisher(for: UIResponder.keyboardWillHideNotification)
          .map { _ in false })
      .debounce(for: .seconds(0.1), scheduler: RunLoop.main)
      .eraseToAnyPublisher()
  }
}

I also added debounce operator in order to prevent true - false toggle when you have multiple TextFields and user moves between them.

Use in any View

struct SwiftUIView: View {
  @State var isKeyboardPresented = false
  @State var firstTextField = ""
  @State var secondTextField = ""
  
  var body: some View {
    VStack {
      TextField("First textField", text: $firstTextField)
      TextField("Second textField", text: $secondTextField)
    }
    .onReceive(keyboardPublisher) { value in
      isKeyboardPresented = value
    }
  }
}

1 Comment

Wait, what, tab indents are set to 2? You go Max, stick it to them! (I have refused to set my tabs to anything else but 2 since the eighties :-)
19

With Environment Key...

Taking @Lepidopteron's answer, but using it to drive an environment key.

This allows you to access the keyboard state in any view using

@Environment(\.keyboardShowing) var keyboardShowing

All you have to do is add a view modifier at the top of your hierarchy

RootView()
.addKeyboardVisibilityToEnvironment()

This is all powered by the following ViewModifier file...

public extension View {
    
    /// Sets an environment value for keyboardShowing
    /// Access this in any child view with
    /// @Environment(\.keyboardShowing) var keyboardShowing
    func addKeyboardVisibilityToEnvironment() -> some View {
        modifier(KeyboardVisibility())
    }
}

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

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.keyboardWillHideNotification)
                    .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
}

3 Comments

This is so simple...many thanks to you. For beginners (like me), make sure you add import SwiftUI and import Combine to the top of your file. I added .addKeyboardVisibilityToEnvironment() to the first calling of ContentView() in my initial WindowGroup and @Environment(\.keyboardShowing) var keyboardShowing to the top of my ContentView view. Everything works perfectly. Thanks again.
I found this zhuanlan.zhihu.com/p/436070583?utm_id=0 use AnyCancellable , is the best aproach?
This should be the right solution. We can not use above .onReceive(keyboardPublisher) solution. It does not catch the initial event value but only change event. For example, if you have a list of views with a filter function, and you put .onReceive(keyboardPublisher) into each list item, then you will get into problem.
18

iOS 15:

You can use the focused(_:) view modifier and @FocusState property wrapper to know whether a text field is editing, and also change the editing state.

@State private var text: String = ""
@FocusState private var isTextFieldFocused: Bool

var body: some View {
    VStack {
        TextField("hello", text: $text)
            .focused($isTextFieldFocused)
        
        if isTextFieldFocused {
            Button("Keyboard is up!") {
                isTextFieldFocused = false
            }
        }
    }
}

1 Comment

Focus and keyboard presence are not necessarily the same thing. If there is an external keyboard connected, this will not show the right result, in terms of keyboard presence.
5

You could also create a viewModifier to get notified of Keyboard notifications.

struct OnkeyboardAppearHandler: ViewModifier {
    var handler: (Bool) -> Void
    func body(content: Content) -> some View {
        content
            .onAppear {
                NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { _ in
                    handler(true)
                }
                
                NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
                    handler(false)
                }
            }
    }
}

extension View {
    public func onKeyboardAppear(handler: @escaping (Bool) -> Void) -> some View {
        modifier(OnkeyboardAppearHandler(handler: handler))
    }
}

Comments

0

Modified from @Confused_Vorlon, Change AnyPublisher to AnyCancellable, best aproach, good practice to avoid memory leak.

Code

import Foundation
import SwiftUI
import Combine

public extension View {
    
    /// Sets an environment value for keyboardShowing
    /// Access this in any child view with
    /// @Environment(\.keyboardShowing) var keyboardShowing
    func addKeyboardVisibilityToEnvironment() -> some View {
        modifier(KeyboardVisibility())
    }
}

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

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

private final class KeyboardMonitor: ObservableObject {
    @Published var isKeyboardShowing: Bool = false
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        #if os(macOS)
        // No es necesario hacer nada para macOS
        #else
        NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
            .map { _ in true }
            .assign(to: \.isKeyboardShowing, on: self)
            .store(in: &cancellables)
        
        NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
            .map { _ in false }
            .assign(to: \.isKeyboardShowing, on: self)
            .store(in: &cancellables)
        #endif
    }
}

private struct KeyboardVisibility: ViewModifier {
    @StateObject private var keyboardMonitor = KeyboardMonitor()
    
    fileprivate func body(content: Content) -> some View {
        content
            .environment(\.keyboardShowing, keyboardMonitor.isKeyboardShowing)
    }
}

Usage

App
WindowGroup {
    ContentView()
    .addKeyboardVisibilityToEnvironment()


View

@Environment(\.keyboardShowing) var keyboardShowing
...
Text("Keyboard present: \(keyboardShowing.description)" )

Test

Test in MacOs and simulator iphone15 pro, XCode 15

Comments

0

The elementary answers is:

    SomeView    
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
            ... update some observed value
        }
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
            ... update some observed value
        }

Comments

0

Here is a pure SwiftUI solution that works by comparing the height of the bottom safe area inset for two background views containing Color.clear:

  • one view does not ignore safe areas
  • the other ignores the safe area for .keyboard only.

If the bottom safe area insets are different then it is assumed that the keyboard is showing. This works for devices with a bottom inset of 0 too (such as iPhone SE), because the bottom inset height includes the height of the keyboard when it is showing.

To make it all more convenient to use, the detection code can be implemented as a ViewModifier. The implementation here uses .onGeometryChange to measure the safe area insets, which is backwards compatible with iOS 16:

struct DetectKeyboard: ViewModifier {
    @Binding var isKeyboardShowing: Bool
    @State private var bottomInsetWithoutKeyboard: CGFloat?
    @State private var bottomInsetWithKeyboard: CGFloat?

    private var isKeyboardDetected: Bool {
        if let bottomInsetWithoutKeyboard, let bottomInsetWithKeyboard {
            bottomInsetWithoutKeyboard != bottomInsetWithKeyboard
        } else {
            false
        }
    }

    func body(content: Content) -> some View {
        ZStack {
            Color.clear
                .onGeometryChange(for: CGFloat.self, of: \.safeAreaInsets.bottom) { bottomInset in
                    bottomInsetWithoutKeyboard = bottomInset
                }
                .ignoresSafeArea(.keyboard) // Must come after .onGeometryChange
            Color.clear
                .onGeometryChange(for: CGFloat.self, of: \.safeAreaInsets.bottom) { bottomInset in
                    bottomInsetWithKeyboard = bottomInset
                }
            content
        }
        // iOS 16: .onChange(of: isKeyboardDetected) { newVal in
        .onChange(of: isKeyboardDetected) { _, newVal in
            isKeyboardShowing = newVal
        }
    }
}

extension View {
    func detectKeyboard(isKeyboardShowing: Binding<Bool>) -> some View {
        modifier(DetectKeyboard(isKeyboardShowing: isKeyboardShowing))
    }
}

The modifier should be applied to a top-level view, or at least to a view that is in contact with the bottom safe area inset.

Example use:

struct ContentView: View {
    @State private var text = ""
    @State private var isKeyboardShowing = false

    var body: some View {
        Form {
            TextField("Enter some text", text: $text)
        }
        .scrollContentBackground(.hidden)
        .background(isKeyboardShowing ? .yellow : Color(.systemGroupedBackground))
        .detectKeyboard(isKeyboardShowing: $isKeyboardShowing)
    }
}

Animation

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.