21

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.

George
  • 25,988
  • 10
  • 79
  • 133
darren z
  • 3,051
  • 3
  • 13
  • 28

4 Answers4

50

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.

George
  • 25,988
  • 10
  • 79
  • 133
  • 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 Jan 19 '21 at 04:18
  • @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. – George Jan 19 '21 at 09:21
  • 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! – P. Ent Jul 24 '21 at 20:08
17

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
    }
  }
}
Lepidopteron
  • 6,056
  • 5
  • 41
  • 53
Max
  • 201
  • 2
  • 6
  • 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 :-) – Yohst May 03 '23 at 18:58
10

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
            }
        }
    }
}
Eilon
  • 2,698
  • 3
  • 16
  • 32
  • 19
    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. – ThomasCle Apr 06 '22 at 14:33
6

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
}
Confused Vorlon
  • 9,659
  • 3
  • 46
  • 49
  • 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. – SPM Jun 20 '23 at 17:48