Get height of iOS keyboard without displaying keyboard
Asked Answered
K

5

10

I'm trying to get the height of the iOS keyboard. I've gone through and used the method involving subscribing to a notification such as detailed here: https://gist.github.com/philipmcdermott/5183731

- (void)viewDidAppear:(BOOL) animated {
    [super viewDidAppear:animated];
    // Register notification when the keyboard will be show
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(keyboardWillShow:)
        name:UIKeyboardWillShowNotification
        object:nil];

    // Register notification when the keyboard will be hide
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(keyboardWillHide:)
        name:UIKeyboardWillHideNotification
        object:nil];
}

- (void)keyboardWillShow:(NSNotification *)notification {
    CGRect keyboardBounds;

    [[notification.userInfo valueForKey:UIKeyboardFrameBeginUserInfoKey] getValue:&keyboardBounds];

    // Do something with keyboard height
}

- (void)keyboardWillHide:(NSNotification *)notification {
    CGRect keyboardBounds;

    [[notification.userInfo valueForKey:UIKeyboardFrameBeginUserInfoKey] getValue:&keyboardBounds];

    // Do something with keyboard height
}

This works fine for when the user actually displays the keyboard.

My problem: I have another view, let's call it micView, that may be presented before the keyboard appears. The user may choose to use the microphone before typing. I would like the micView to be the same height as the keyboard, which is why I need the keyboard's height, but I need it before the keyboard was forced to appear. Thus the UIKeyboardWillShowNotification is not reached before I need to read the value of the height.

My question is: how do I get the height of the keyboard through Notifications, or some other method without ever having the keyboard appear.

I considered explicitly forcing the keyboard to appear in viewDidLoad, so that I can set an instance variable to that value, then hiding it and getting rid of the animations for both things. But is that really the only way to do that?

Korn answered 17/11, 2014 at 20:22 Comment(0)
P
11

A quick solution that you could use, is the same one used when you want to cache the keyboard (the first time you show it, you get a slight delay...). The library is here. The interesting bits:

[[[[UIApplication sharedApplication] windows] lastObject] addSubview:field];
[field becomeFirstResponder];
[field resignFirstResponder];
[field removeFromSuperview];

So basically is showing it and then hiding it. You could listen for notifications and just get the height without actually seeing it. Bonus: you get to cache it. :)

Paperweight answered 17/11, 2014 at 20:38 Comment(4)
Thank you! This works. I put the code in the viewWillAppear method right after setting the notification selectors and it works. I would upvote, but I don't have that privilege yet. For clarification, do you know if the keyboard caching is still a major problem on new iOS devices with their better internals?Korn
It is. I can see it on an app I have right now on the app store that's iOS 8.0 only.Paperweight
what should be the value of field?Piliform
@Piliform field would be a UITextField.Paperweight
D
15

This Swift class provides a turn-key solution that manages all the necessary notifications and initializations, letting you simply call a class method and have returned the keyboard size or height.

Calling from Swift:

let keyboardHeight = KeyboardService.keyboardHeight()
let keyboardSize = KeyboardService.keyboardSize()

Calling from Objective-C:

 CGFloat keyboardHeight = [KeyboardService keyboardHeight];
 CGRect keyboardSize = [KeyboardService keyboardSize];

If wanting to use this for initial view layout, call this from the viewWillAppear method of a class where you want the keyboard height or size before the keyboard appears. It should not be called in viewDidLoad, as a correct value relies on your views having been laid out. You can then set an autolayout constraint constant with the value returned from the KeyboardService, or use the value in other ways. For instance, you might want to obtain the keyboard height in prepareForSegue to assist in setting a value associated with the contents of a containerView being populated via an embed segue.

Note re safe area, keyboard height, and iPhone X:
The value for keyboard height returns the full height of the keyboard, which on the iPhone X extends to the edge of the screen itself, not just to the safe area inset. Therefore, if setting an auto layout constraint value with the returned value, you should attach that constraint to the superview bottom edge, not to the safe area.

Note re hardware keyboard in Simulator:
When a hardware keyboard is attached, this code will provide the on-screen height of that hardware keyboard, that is, no height. This state does need to be accounted for, of course, as this simulates what will occur if you have a hardware keyboard attached to an actual device. Therefore, your layout that is expecting a keyboard height needs to respond appropriately to a keyboard height of zero.

KeyboardService class:
As usual, if calling from Objective-C, you simply need to import the app's Swift bridging header MyApp-Swift.h in your Objective-C class.

import UIKit

class KeyboardService: NSObject {
    static var serviceSingleton = KeyboardService()
    var measuredSize: CGRect = CGRect.zero

    @objc class func keyboardHeight() -> CGFloat {
        let keyboardSize = KeyboardService.keyboardSize()
        return keyboardSize.size.height
    }

    @objc class func keyboardSize() -> CGRect {
        return serviceSingleton.measuredSize
    }

    private func observeKeyboardNotifications() {
        let center = NotificationCenter.default
        center.addObserver(self, selector: #selector(self.keyboardChange), name: .UIKeyboardDidShow, object: nil)
    }

    private func observeKeyboard() {
        let field = UITextField()
        UIApplication.shared.windows.first?.addSubview(field)
        field.becomeFirstResponder()
        field.resignFirstResponder()
        field.removeFromSuperview()
    }

    @objc private func keyboardChange(_ notification: Notification) {
        guard measuredSize == CGRect.zero, let info = notification.userInfo,
            let value = info[UIKeyboardFrameEndUserInfoKey] as? NSValue
            else { return }

        measuredSize = value.cgRectValue
    }

    override init() {
        super.init()
        observeKeyboardNotifications()
        observeKeyboard()
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }    
}

Head nod:
The observeKeyboard method here based on the original approach outlined by Peres in the Objective-C answer to this question.

Downcast answered 3/11, 2017 at 23:53 Comment(1)
Last time I use this method and it's working fine but now the keyboard height will only return after user open any keyboard first, what was happening?Explanatory
P
11

A quick solution that you could use, is the same one used when you want to cache the keyboard (the first time you show it, you get a slight delay...). The library is here. The interesting bits:

[[[[UIApplication sharedApplication] windows] lastObject] addSubview:field];
[field becomeFirstResponder];
[field resignFirstResponder];
[field removeFromSuperview];

So basically is showing it and then hiding it. You could listen for notifications and just get the height without actually seeing it. Bonus: you get to cache it. :)

Paperweight answered 17/11, 2014 at 20:38 Comment(4)
Thank you! This works. I put the code in the viewWillAppear method right after setting the notification selectors and it works. I would upvote, but I don't have that privilege yet. For clarification, do you know if the keyboard caching is still a major problem on new iOS devices with their better internals?Korn
It is. I can see it on an app I have right now on the app store that's iOS 8.0 only.Paperweight
what should be the value of field?Piliform
@Piliform field would be a UITextField.Paperweight
C
1

Looks like this solution did stop working.

I modified it:

  • adding a callback to know when the notification arrives with the real height,
  • moving the textfield to another window to avoid showing it, and
  • setting a timeout for the case when is used in the simulator and the software keyboard is setted up to now show.

Using Swift 4:

import UIKit

public class KeyboardSize {
  private static var sharedInstance: KeyboardSize?
  private static var measuredSize: CGRect = CGRect.zero

  private var addedWindow: UIWindow
  private var textfield = UITextField()

  private var keyboardHeightKnownCallback: () -> Void = {}
  private var simulatorTimeout: Timer?

  public class func setup(_ callback: @escaping () -> Void) {
    guard measuredSize == CGRect.zero, sharedInstance == nil else {
      return
    }

    sharedInstance = KeyboardSize()
    sharedInstance?.keyboardHeightKnownCallback = callback
  }

  private init() {
    addedWindow = UIWindow(frame: UIScreen.main.bounds)
    addedWindow.rootViewController = UIViewController()
    addedWindow.addSubview(textfield)

    observeKeyboardNotifications()
    observeKeyboard()
  }

  public class func height() -> CGFloat {
    return measuredSize.height
  }

  private func observeKeyboardNotifications() {
    let center = NotificationCenter.default
    center.addObserver(self, selector: #selector(self.keyboardChange), name: UIResponder.keyboardDidShowNotification, object: nil)
  }

  private func observeKeyboard() {
    let currentWindow = UIApplication.shared.keyWindow

    addedWindow.makeKeyAndVisible()
    textfield.becomeFirstResponder()

    currentWindow?.makeKeyAndVisible()

    setupTimeoutForSimulator()
  }

  @objc private func keyboardChange(_ notification: Notification) {
    textfield.resignFirstResponder()
    textfield.removeFromSuperview()

    guard KeyboardSize.measuredSize == CGRect.zero, let info = notification.userInfo,
      let value = info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
      else { return }

    saveKeyboardSize(value.cgRectValue)
  }

  private func saveKeyboardSize(_ size: CGRect) {
    cancelSimulatorTimeout()

    KeyboardSize.measuredSize = size
    keyboardHeightKnownCallback()

    KeyboardSize.sharedInstance = nil
  }

  private func setupTimeoutForSimulator() {
    #if targetEnvironment(simulator)
    let timeout = 2.0
    simulatorTimeout = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false, block: { (_) in
      print(" KeyboardSize")
      print(" .keyboardDidShowNotification did not arrive after \(timeout) seconds.")
      print(" Please check \"Toogle Software Keyboard\" on the simulator (or press cmd+k in the simulator) and relauch your app.")
      print(" A keyboard height of 0 will be used by default.")
      self.saveKeyboardSize(CGRect.zero)
    })
    #endif
  }

  private func cancelSimulatorTimeout() {
    simulatorTimeout?.invalidate()
  }

  deinit {
    NotificationCenter.default.removeObserver(self)
  }
}

Is used in the following way:

    let splashVC = some VC to show in the key window during the app setup (just after the didFinishLaunching with options)
    window.rootViewController = splashVC

    KeyboardSize.setup() { [unowned self] in
      let kbHeight = KeyboardSize.height() // != 0 :)
      // continue loading another things or presenting the onboarding or the auth
    }
Congener answered 25/3, 2019 at 10:46 Comment(0)
E
0

For iOS 14.0, I noticed that this solution stopped working on around the 10th call as NotificationCenter stopped broadcasting keyboardChange notification. I was not able to fully figure out why that was happening.

So, I tweaked the solution to make KeyboardSize a singleton and added a method updateKeyboardHeight() as such:

    static let shared = KeyboardSize()

    /**
     Height of keyboard after the class is initialized
    */
    private(set) var keyboardHeight: CGFloat = 0.0

    private override init() {
        super.init()

        observeKeyboardNotifications()
        observeKeyboard()
    }

    func updateKeyboardHeight() {
        observeKeyboardNotifications()
        observeKeyboard()
    }

and used it as

KeyboardSize.shared.updateKeyboardHeight()
let heightOfKeyboard = KeyboardSize.shared.keyboardHeight
Eldwin answered 5/11, 2020 at 20:47 Comment(0)
R
0

Tested with Swift 5.0 and iOS 16.

I struggled with all the answers, so I just used the original approach from Peres.

So I have a LaunchViewController that shows a SwiftUI splash screen. This screen is alive for a minimum of 0.2 seconds before it is deinit. When the screen has appeared I toggle the becomeFirstResponder to get the height, which NotificationCenter observes. Then I save the height to UserDefaults for later use.

Note: In regard to famfamfam's question. You should get the original height without any safe area, and then when you use the height you should check for edge cases like safeArea or tabbarHeight (see at the bottom where I have a conditional edge case).

LaunchViewController:

import SwiftUI

final class LaunchViewController: UIViewController {
    
    lazy var fakeTextField = UITextField(withAutolayout: true)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        observeKeyboard()
        
        view.addSubview(fakeTextField)
        fakeTextField.anchor(height: 50, width: 200 ,centerX: view.centerXAnchor, centerY: view.centerYAnchor)

        let childView = UIHostingController(rootView: LaunchView())
        addChild(childView)
        view.addSubview(childView.view)
        childView.didMove(toParent: self)
        childView.view.pin(to: view)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        fakeTextField.becomeFirstResponder()
        fakeTextField.removeFromSuperview()
    }
    
    func observeKeyboard() {
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillAppear), name: UIResponder.keyboardWillShowNotification, object: nil)
    }
    
    // MARK: - Actions
    @objc private func keyboardWillAppear(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
            UserDefaults.standard.keyboardHeight = Float(keyboardSize.height)
            print("\(keyboardSize.height)")
        }
    }
    
    deinit {
        print("✅ Deinit LaunchViewController")
        NotificationCenter.default.removeObserver(self)
    }
}

UserDefaults:

// MARK: - Keys
extension UserDefaults {
    private enum Keys {
        static let keyboardHeight = "keyboardHeight"
    }
}

// MARK: - keyboardHeight
extension UserDefaults {
   
    var keyboardHeight: Float? {
        get {
            float(forKey: Keys.keyboardHeight)
        }
        set {
            set(newValue, forKey: Keys.keyboardHeight)
        }
    }
}

In practise:

if keyboardHeight == .zero {
    // use the height we saved from splash screen
    keyboardHeight = CGFloat(UserDefaults.standard.keyboardHeight ?? 301) - (parent?.presentingViewController?.view.safeAreaInsets.bottom != nil ? parent!.presentingViewController!.view.safeAreaInsets.bottom : .zero)
}
Reprieve answered 10/7, 2023 at 9:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.