nickcoding
nickcoding

Reputation: 475

Retrieving keyboard height when textfield editing starts in SwiftUI

So I have a class with a published variable called keyboardHeight that is used to retrieve the value of the keyboardHeight:

class KeyboardHeightHelper: ObservableObject {
    @Published var keyboardHeight: Double = 0
    
    init() {
        listenForKeyboardNotifications()
    }
    
    private func listenForKeyboardNotifications() {
        NotificationCenter.default.addObserver(
            forName: UIResponder.keyboardDidShowNotification,
            object: nil,
            queue: .main
        ) { notification in
            guard
                let userInfo = notification.userInfo,
                let keyboardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
            else { return }
            
            self.keyboardHeight = keyboardRect.height
        }
        
        NotificationCenter.default.addObserver(
            forName: UIResponder.keyboardDidHideNotification,
            object: nil,
            queue: .main)
        { _ in
            self.keyboardHeight = 0
        }
    }
}

Then, in ContentView I just have a TextField that should print the keyboard height when you start/stop editing the field:

import SwiftUI

struct ContentView: View {
    @State var textFieldText = ""
    @ObservedObject var keyboardHeightHelper = KeyboardHeightHelper()
    var body: some View {
    VStack {
        TextField("Text field",
                  text: $textFieldText, onEditingChanged: { _ in print("the keyboard height is \(self.keyboardHeightHelper.keyboardHeight)") })
        
    }

   }
}

The problem I have is this: When I am not editing the textfield and then click it, it prints the keyboard height is 0.0 (I guess this is because it grabs the keyboardHeight value before it presents the keyboard, so at the time the height is 0.0 as the keyboard isn't seen). When I press return and the keyboard is dismissed, the height of the keyboard (for the iPhone 8 simulator) is printed as the correct value of 260.0. My question is how do I access the value of the keyboard when I start editing?

Upvotes: 2

Views: 2200

Answers (2)

Asperi
Asperi

Reputation: 257513

The .onEditingChanged is not appropriate place to read keyboard height, because you receive this callback right in the moment of click in TextField, so there is no keyboard yet shown (and, so, no notification received).

Instead you can listen explicitly for your keyboardHeight property publisher and be notified exactly when it is changed (what is performed on keyboard notifications synchronously, so in time)

Here is a solution (tested with Xcode 12 / iOS 14)

VStack {
    TextField("Text field",
              text: $textFieldText, onEditingChanged: { _ in  })
        .onReceive(keyboardHeightHelper.$keyboardHeight) { value in
            print("the keyboard height is \(value)")
        }
}

Upvotes: 1

Subha_26
Subha_26

Reputation: 450

Try this. You can integrate it using just a modifier to SwiftUI view:

extension UIResponder {
    static var currentFirstResponder: UIResponder? {
        _currentFirstResponder = nil
        UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder(_:)), to: nil, from: nil, for: nil)
        return _currentFirstResponder
    }

    private static weak var _currentFirstResponder: UIResponder?

    @objc private func findFirstResponder(_ sender: Any) {
        UIResponder._currentFirstResponder = self
    }
    
    var globalFrame: CGRect? {
        guard let view = self as? UIView else { return nil }
        return view.superview?.convert(view.frame, to: nil)
    }
}
extension Publishers {
    static var keyboardHeight: AnyPublisher<CGFloat, Never> {
        let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
            .map { $0.keyboardHeight }
        
        let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
            .map { _ in CGFloat(0) }
        
        return MergeMany(willShow, willHide)
            .eraseToAnyPublisher()
    }
}

extension Notification {
    var keyboardHeight: CGFloat {
        return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
    }
}
struct KeyboardAdaptive: ViewModifier {
    @State private var bottomPadding: CGFloat = 0

    func body(content: Content) -> some View {
        GeometryReader { geometry in
            content
                .padding(.bottom, self.bottomPadding)
                .onReceive(Publishers.keyboardHeight) { keyboardHeight in
                    let keyboardTop = geometry.frame(in: .global).height - keyboardHeight
                    let focusedTextInputBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0
                    self.bottomPadding = max(0, focusedTextInputBottom - keyboardTop - geometry.safeAreaInsets.bottom)
            }
            .animation(.easeOut(duration: 0.16))
        }
    }
}

extension View {
    func keyboardAdaptive() -> some View {
        ModifiedContent(content: self, modifier: KeyboardAdaptive())
    }
}

Usage:

struct ContentView: View {
    @State private var text = ""
    
    var body: some View {
        VStack {
            Spacer()
            
            TextField("Enter something", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
        }
        .padding()
        .keyboardAdaptive()//note this
    }
}

Credits to multiple online sources.

Upvotes: 2

Related Questions