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
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.
4 Comments
will variant because the updates are instantly, whereas the did variant you have to wait for the whole appearing/disappearing animation.Main actor-isolated property 'keyboardPublisher' cannot be used to satisfy nonisolated protocol requirementMy 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
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
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..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.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
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
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
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
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
.keyboardonly.
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)
}
}
