iOS 里每个 App 全局只有一个系统键盘,它由当前 firstResponder (同一时刻最多只有一个第一响应者。A 调用 becomeFirstResponder,B 自动失去焦点)决定何时弹出:
有 UITextInput 协议的 responder(如 UITextView / UITextField)成为 firstResponder → 系统弹键盘
becomeFirstResponder不会立刻弹起键盘,是异步操作;键盘弹出是系统动画,有延迟,不要在调用后立刻读取 frame。我们在willshow中就可以取到键盘高度了
// 从通知userInfo取出参数
guard let userInfo = notification.userInfo else { return }
// 键盘在屏幕坐标系中的frame!非常重要,是屏幕绝对坐标,不是相对于当前view
let keyboardFrameEnd = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
> 注意:`keyboardFrameEnd` 是屏幕坐标系下的绝对坐标,需转换为当前视图坐标系才能正确计算键盘高度:
> let keyboardFrameInCurrentView = view.convert(keyboardFrameEnd, from: nil)
> let keyboardHeight = keyboardFrameInCurrentView.height
> 若未转换,直接使用 `keyboardFrameEnd.height` 在横竖屏切换或多窗口场景下易出错。此外,`userInfo` 中还包含动画时长(`UIResponder.keyboardAnimationDurationUserInfoKey`)和动画曲线(`UIResponder.keyboardAnimationCurveUserInfoKey`),可用于同步自定义动画。
键盘不属于 App 窗口,是系统另外一个 UIWindow (UIRemoteKeyboardWindow),App 不能直接拿到键盘 View,只能接收系统通知和代理回调。
系统主要包含四种通知
键盘将要弹出(动画启动前) UIResponder.keyboardWillShowNotification
键盘弹出完成(动画结束) UIResponder.keyboardDidShowNotification
键盘将要收起(动画启动前) UIResponder.keyboardWillHideNotification
键盘收起完成(动画结束) UIResponder.keyboardDidHideNotification
我们可以使用如下代码去订阅系统键盘的通知
NotificationCenter.default.addObserver(
self,
selector: #selector(handleKeyboardWillShowNotification(_:)),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleKeyboardWillHideNotification(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
时序总结
点击输入框
调用textView.becomeFirstResponder(),异步向系统申请焦点 系统准备动画,发送keyboardWillShowNotification / keyboardWillChangeFrameNotification,携带 endFrame 执行系统键盘动画,同时执行 App 这边同步 UI 动画 动画完成,发出keyboardDidShowNotification
同一输入框,系统键盘内部模式切换
中文拼音 ↔ 英文 ↔ emoji ↔ 数字键盘,或切换系统内置的不同语言键盘。
此时不会出发willhide,一般来说,如果这个切换会造成frame变化,我们可以监听通知UIKeyboardWillChangeFrameNotification,触发时机是所有键盘位置 / 高度变化的场景都会触发 —— 包括弹出、收起、切换输入法高度变化、iPad 键盘模式切换、屏幕旋转、切换输入框高度变化等。
note:willShow 和 willHide 本质上是 willChangeFrame 的两个特殊子集:
键盘从屏幕外移入屏幕内 → 同时触发 willChangeFrame + willShow
键盘从屏幕内移出到屏幕外 → 同时触发 willChangeFrame + willHide
键盘在屏幕内改变位置 / 高度(切换输入法、转屏、浮动键盘)→ 只触发 willChangeFrame,不触发 show/hide
多个输入框之间切换第一响应者
绝大多数情况不会触发 willHide(只有键盘差距极大的时候才会触发),系统做了流畅性优化:只要新的第一响应者同样需要弹出键盘,就会保持键盘常驻,直接过渡;仅在键盘高度变化时触发 frame 变化通知
我认为如果我们在开发一个模块(比如最常见的聊天模块),我们如果在多处监听键盘通知
这会带来一些问题:
时序无法保证 获取属性代码重复,甚至无法进行正确获取
这里举一个例子:比如当前键盘是否展示,keyboardShowing,我们正常来说应该是在willshow的时候置为true,willhide的时候置为false,并且我们监听这个属性变化去进行一些操作,但是如果键盘是进行切换的,那么这个keyboardShowing还是为true,就不会进行键盘落下的一系列操作和通知(我们在开发的时候这两个键盘可能就承载着不同的功能,对应的处理也是不一致的,比如A键盘弹起的时候应该屏蔽站内通知,而B键盘弹起的时候不进行屏蔽)
所以,我们在这个模块内应该统一设计一个键盘组件,keyboardComponent,其负责监听系统通知
首先可以将willShow分为多个阶段,willShowBegin,willShowEnd,让一些想要先执行的操作可以监听willShowBegin(其余也可以这样设计)
为这个组件添加一些属性,(比如就是keyboardShowing),这样首先也不用为多个依赖于keyboardShowing添加属性了,可以直接通过组件获取
其内部我们均可以自行设计,实际上就看大家的业务需要了
夜雨聆风