在不显示键盘的情况下获取iOS键盘的高度

10

我正在尝试获取iOS键盘的高度。我已经按照这里详细介绍的方法订阅了通知:

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
}

当用户实际显示键盘时,这个方法可以正常工作。

我的问题是:我有另一个视图,我们称之为micView,在键盘出现之前可能会出现。用户可能选择在打字之前使用麦克风。我希望micView与键盘具有相同的高度,这就是为什么我需要键盘的高度,但我需要在强制出现键盘之前获取它。因此,在我需要读取高度值之前,无法使用UIKeyboardWillShowNotification通知。

我的问题是:如何通过通知或其他方法获取键盘的高度,而不必让键盘出现。

我考虑过在viewDidLoad中显式强制出现键盘,以便我可以将一个实例变量设置为该值,然后隐藏它并摆脱两者的动画。但是,那真的是唯一的方法吗?

5个回答

15

这个Swift类提供了一个一站式解决方案,它管理所有必要的通知和初始化,让你只需调用一个类方法,即可返回键盘大小或高度。

在Swift中使用:

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

从 Objective-C 进行调用:

 CGFloat keyboardHeight = [KeyboardService keyboardHeight];
 CGRect keyboardSize = [KeyboardService keyboardSize];
如果想要在初始视图布局中使用它,请在希望键盘出现之前的类的viewWillAppear方法中调用此方法来获取键盘高度或大小。不应该在viewDidLoad中调用,因为正确的值依赖于您的视图已经被布局。然后,您可以使用从KeyboardService返回的值设置一个自动布局约束常量,或以其他方式使用该值。例如,您可能希望在prepareForSegue中获取键盘高度,以帮助设置与通过嵌入式segue填充的containerView的内容相关联的值。
关于安全区域、键盘高度和iPhone X的注意事项: 键盘高度的值返回键盘的完整高度,在iPhone X上,它延伸到屏幕本身的边缘,而不仅仅是安全区域的插图。因此,如果使用返回的值设置自动布局约束值,则应将该约束附加到父视图底部边缘,而不是安全区域。
关于模拟器中的硬件键盘的注意事项: 当连接硬件键盘时,这段代码会提供该硬件键盘的屏幕高度,也就是零高度。当然,需要考虑到这种状态,因为这模拟了如果在实际设备上连接硬件键盘时会发生什么。因此,期望键盘高度的布局需要适当地对零键盘高度做出响应。
KeyboardService类: 如果从Objective-C中调用,通常只需在Objective-C类中导入应用程序的Swift桥接头文件MyApp-Swift.h即可。
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)
    }    
}

点头:
这里的 observeKeyboard 方法基于 Peres 在回答该问题时提出的原始方法。


1
上次我使用这个方法时它运行得很好,但现在键盘高度只有在用户首先打开任何键盘后才会返回,发生了什么? - Anonymous-E

11

一个快速的解决方案是,使用与缓存键盘相同的方法(第一次显示它时,会有轻微的延迟...)。这个库在这里。有趣的部分:

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

基本上是展示它然后隐藏它。您可以侦听通知并仅获取高度而无需实际查看它。奖励:您可以缓存它。:)


谢谢!这个有效。我把代码放在viewWillAppear方法中,在设置通知选择器后立即运行,它可以工作。我想点赞,但我还没有这个权限。为了澄清,您是否知道键盘缓存在新的iOS设备上仍然是一个主要问题吗? - eager to learn
可以的。我现在在应用商店上看到了一个应用,它只支持iOS 8.0及以上版本。 - Rui Peres
字段的值应该是什么? - John
@John字段将是一个UITextField - Rui Peres

1

看起来这个解决方案已经停止工作了。

我进行了修改:

  • 添加回调函数以知道通知到达时的实际高度,
  • 将文本字段移动到另一个窗口以避免显示它,以及
  • 为在模拟器中使用且软件键盘未设置的情况设置超时。

使用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)
  }
}


被以下方式使用:
    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
    }

谢谢伙计,但我有一个问题:当使用IP11 PRM时,键盘包含安全区域高度,我该如何避免它?谢谢。 - famfamfam

0
对于 iOS 14.0,我注意到这个解决方案在第10次调用时停止工作,因为NotificationCenter停止广播keyboardChange通知。我无法完全弄清楚为什么会发生这种情况。
因此,我对解决方案进行了调整,使KeyboardSize成为单例,并添加了一个方法updateKeyboardHeight(),如下所示:
    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()
    }

并将其用作

KeyboardSize.shared.updateKeyboardHeight()
let heightOfKeyboard = KeyboardSize.shared.keyboardHeight

嗨,键盘高度与安全区域不匹配,请帮忙解决。 - famfamfam

0

在 Swift 5.0 和 iOS 16 上进行了测试。

我对所有的答案都感到困惑,所以我只是使用了 Peres 的原始方法。

所以我有一个 LaunchViewController,它显示一个 SwiftUI 的启动屏幕。这个屏幕至少会存在 0.2 秒钟,然后被释放。当屏幕出现时,我切换 becomeFirstResponder 来获取高度,NotificationCenter 进行观察。然后我将高度保存到 UserDefaults 中以供以后使用。

注意:关于 famfamfam 的 问题。你应该获取没有任何安全区域的原始高度,然后在使用高度时,应该检查边缘情况,比如安全区域或者标签栏高度(请看底部的条件边缘情况)。

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

实际操作中:
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)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接