iOS开发基础22-键盘通知全解析:监听、适配与动画同步
iOS 键盘通知全解析:监听、适配与动画同步
键盘弹出/隐藏时需要调整视图位置避免内容被遮挡。本文详细讲解键盘通知的种类、userInfo 字段、坐标系转换、动画同步,以及 Safe Area 和现代 API 的适配。
一、键盘通知种类
系统在键盘状态变化时发送六种通知:
| 通知名 | 触发时机 |
|---|---|
UIKeyboardWillShowNotification |
键盘即将显示 |
UIKeyboardDidShowNotification |
键盘显示完毕 |
UIKeyboardWillHideNotification |
键盘即将隐藏 |
UIKeyboardDidHideNotification |
键盘隐藏完毕 |
UIKeyboardWillChangeFrameNotification |
键盘 frame 即将变化(推荐监听) |
UIKeyboardDidChangeFrameNotification |
键盘 frame 变化完毕 |
推荐监听
UIKeyboardWillChangeFrameNotification:它覆盖了弹出、隐藏和 frame 变化(如切换输入法、第三方键盘高度变化)所有场景,比单独监听 WillShow/WillHide 更全面。
二、userInfo 字典字段
键盘通知的 userInfo 包含键盘位置、尺寸和动画信息:
| 键 | 类型 | 说明 |
|---|---|---|
UIKeyboardFrameBeginUserInfoKey |
NSValue(CGRect) |
动画开始前键盘的 frame(相对于屏幕) |
UIKeyboardFrameEndUserInfoKey |
NSValue(CGRect) |
动画结束后键盘的 frame(相对于屏幕)——最常用 |
UIKeyboardAnimationDurationUserInfoKey |
NSNumber |
动画时长(秒) |
UIKeyboardAnimationCurveUserInfoKey |
NSNumber |
动画曲线(值通常为 7,键盘私有曲线) |
UIKeyboardIsLocalUserInfoKey |
NSNumber |
是否当前 App 触发的键盘(iOS 9+) |
UIKeyboardBoundsUserInfoKey |
NSValue |
键盘 bounds(iOS 3.2 起废弃,不推荐使用) |
UIKeyboardCenterBeginUserInfoKey |
NSValue |
动画开始时键盘中心(已废弃) |
UIKeyboardCenterEndUserInfoKey |
NSValue |
动画结束时键盘中心(已废弃) |
废弃字段仍可获取值,但 Apple 不保证未来版本继续提供,应优先使用 Frame 和 Animation 相关字段。
三、监听与适配键盘
1. 注册与移除通知
最佳实践:在 viewWillAppear: 注册,viewWillDisappear: 移除,避免多个可见控制器同时响应键盘通知。
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillChangeFrame:)
name:UIKeyboardWillChangeFrameNotification
object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIKeyboardWillChangeFrameNotification
object:nil];
}
2. 处理键盘 frame 变化(核心)
- (void)keyboardWillChangeFrame:(NSNotification *)note {
NSDictionary *userInfo = note.userInfo;
// 1. 获取键盘最终 frame(相对于屏幕/UIWindow 坐标系)
CGRect keyboardEndFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
// 2. 坐标系转换:将键盘 frame 从 window 坐标系转换到 self.view 坐标系
// 关键:键盘 frame 是相对于屏幕的,不是相对于 self.view 的
CGRect keyboardFrameInView = [self.view convertRect:keyboardEndFrame fromView:nil];
// 3. 计算键盘在 self.view 中遮挡的高度
// 如果 view 覆盖整个屏幕,也可以用 self.view.bounds.size.height - keyboardFrameInView.origin.y
CGFloat keyboardHeight = CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(keyboardFrameInView);
// 键盘隐藏时 keyboardHeight 可能为 0 或负数,取 MAX 保护
keyboardHeight = MAX(0, keyboardHeight);
// 4. 更新约束(假设 bottomConstraint 是输入框底部到 view 底部的约束)
self.bottomConstraint.constant = keyboardHeight + 10; // +10 间距
// 5. 同步键盘动画:获取动画时长和曲线
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
NSInteger curveValue = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
// 动画曲线值需要左移 16 位才能作为 UIViewAnimationOptions 使用
UIViewAnimationOptions curve = curveValue << 16;
[UIView animateWithDuration:duration delay:0 options:curve animations:^{
[self.view layoutIfNeeded]; // 约束变化后必须调用 layoutIfNeeded 触发布局
} completion:nil];
}
3. 关键要点说明
| 要点 | 说明 |
|---|---|
| 坐标系转换 | 键盘 frame 相对于屏幕(window),必须 convertRect:fromView:nil 转换到 view 坐标系,否则有导航栏/tabBar 时计算错误 |
| 动画曲线同步 | 键盘动画曲线值通常为 7(私有值),需 curveValue << 16 转为 UIViewAnimationOptions,否则动画节奏与键盘不一致 |
| layoutIfNeeded | 修改约束后必须在动画 block 中调用 layoutIfNeeded,否则无动画效果 |
| MAX 保护 | 键盘隐藏或外部键盘连接时,keyboardHeight 可能为 0 或负数,需 MAX(0, height) 保护 |
4. 让输入框自动上移(ScrollView 方案)
如果输入框在 UIScrollView(或 UITableView/UICollectionView)中,调整 contentInset 更合适:
- (void)keyboardWillChangeFrame:(NSNotification *)note {
CGRect keyboardEndFrame = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect keyboardFrameInView = [self.view convertRect:keyboardEndFrame fromView:nil];
CGFloat keyboardHeight = MAX(0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(keyboardFrameInView));
// 调整 scrollView 的 contentInset 和 scrollIndicatorInsets
UIEdgeInsets inset = self.scrollView.contentInset;
inset.bottom = keyboardHeight;
self.scrollView.contentInset = inset;
self.scrollView.scrollIndicatorInsets = inset;
// 如果第一响应者被遮挡,滚动到可见区域
UIView *firstResponder = [self findFirstResponderInView:self.view];
if (firstResponder) {
CGRect responderFrame = [self.scrollView convertRect:firstResponder.frame fromView:firstResponder.superview];
[self.scrollView scrollRectToVisible:responderFrame animated:YES];
}
NSTimeInterval duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationOptions curve = [note.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16;
[UIView animateWithDuration:duration delay:0 options:curve animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
5. 点击屏幕退出键盘
方式一:touchesBegan(简单但有局限)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.view endEditing:YES];
}
局限:如果 view 上有按钮等可交互控件,触摸事件会被控件拦截,
touchesBegan不会触发。
方式二:UITapGestureRecognizer(推荐)
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboard)];
tap.cancelsTouchesInView = NO; // 不拦截按钮等控件的点击
[self.view addGestureRecognizer:tap];
}
- (void)dismissKeyboard {
[self.view endEditing:YES];
}
四、userInfo 打印示例
在键盘通知回调中打印 userInfo,可查看实际字段值:
- (void)keyboardWillChangeFrame:(NSNotification *)note {
NSLog(@"userInfo: %@", note.userInfo);
}
典型输出(iPhone SE,键盘弹出):
UIKeyboardAnimationCurveUserInfoKey = 7;
UIKeyboardAnimationDurationUserInfoKey = "0.25";
UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 253}}";
UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 694.5}";
UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 441.5}";
UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 568}, {320, 253}}";
UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 315}, {320, 253}}";
UIKeyboardIsLocalUserInfoKey = 1;
UIKeyboardAnimationCurveUserInfoKey = 7是键盘特有的动画曲线值(UIViewAnimationCurve枚举仅定义 0~3,7 是私有值),必须左移 16 位后作为UIViewAnimationOptions使用。
五、现代 API:UIKeyboardLayoutGuide(iOS 15+)
iOS 15 引入 UIKeyboardLayoutGuide,无需手动监听通知,通过 Auto Layout 自动跟随键盘:
if (@available(iOS 15.0, *)) {
UILayoutGuide *keyboardGuide = self.view.keyboardLayoutGuide;
// 输入框底部约束到键盘顶部,自动跟随键盘移动
[self.inputField.bottomAnchor constraintEqualToAnchor:keyboardGuide.topAnchor constant:-10].active = YES;
// 可选:设置键盘隐藏时的约束(键盘不在屏幕时跟随底部安全区)
[keyboardGuide setBottomAnchorConstraintEqualToSystemLayoutGuideFollowingKeyboard:YES];
}
UIKeyboardLayoutGuide内部自动处理键盘通知和动画同步,是 iOS 15+ 的推荐方案。低版本仍需手动监听通知。
六、Swift 版本对照
class ViewController: UIViewController {
@IBOutlet weak var bottomConstraint: NSLayoutConstraint!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillChangeFrame(_:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(
self,
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
}
@objc func keyboardWillChangeFrame(_ note: Notification) {
guard let userInfo = note.userInfo,
let endFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
let curveValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { return }
// 坐标系转换
let keyboardFrameInView = view.convert(endFrame, from: nil)
let keyboardHeight = max(0, view.bounds.maxY - keyboardFrameInView.minY)
bottomConstraint.constant = keyboardHeight + 10
// 同步动画曲线
let options = UIView.AnimationOptions(rawValue: curveValue << 16)
UIView.animate(withDuration: duration, delay: 0, options: options) {
self.view.layoutIfNeeded()
}
}
// 点击退出键盘
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
@objc func dismissKeyboard() {
view.endEditing(true)
}
}
七、底层逻辑与注意事项
1. 通知是同步的
NSNotificationCenter 的 postNotification 是同步执行的:在发布通知的线程上,依次调用所有观察者的回调方法,全部执行完毕后才返回。不是异步。因此键盘通知的回调默认在主线程执行(键盘状态变化由主线程触发),可以直接操作 UI。
2. 键盘 window 与层级
键盘位于单独的 UIWindow(UIRemoteKeyboardWindow)层级上,不在 App 的主 window 中。因此键盘的 frame 始终相对于屏幕坐标系,必须转换到当前 view 的坐标系。
3. 第三方键盘与 frame 变化
切换输入法(如系统键盘 ↔ 第三方键盘)、从英文切换到表情键盘时,键盘高度会变化,此时只触发 UIKeyboardWillChangeFrameNotification,不触发 WillShow/WillHide。这也是推荐监听 ChangeFrame 的原因。
4. 安全区域(Safe Area)
iOS 11+ 有安全区域概念。如果约束是相对于 view.safeAreaLayoutGuide.bottomAnchor,计算键盘遮挡高度时需注意安全区域底部 inset,避免重复计算。通常将输入框底部约束到 view.bottomAnchor(而非 safeArea),再用键盘高度调整,最为直接。
5. 常见坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 动画与键盘不同步 | 未使用键盘的动画曲线 | curveValue << 16 同步曲线 |
| 有导航栏时高度计算错误 | 键盘 frame 未做坐标系转换 | convertRect:fromView:nil |
| 修改约束无动画 | 未在动画 block 中调用 layoutIfNeeded |
动画 block 内调用 |
| 多个页面同时响应键盘 | 在 viewDidLoad 注册、dealloc 才移除 | viewWillAppear 注册、viewWillDisappear 移除 |
| 切换输入法高度不变 | 只监听了 WillShow/WillHide | 监听 UIKeyboardWillChangeFrameNotification |
八、总结
- 推荐监听
UIKeyboardWillChangeFrameNotification,覆盖弹出、隐藏、frame 变化全部场景。 - 键盘 frame 相对于屏幕坐标系,必须
convertRect:fromView:nil转换到当前 view。 - 同步键盘动画需同时使用
UIKeyboardAnimationDurationUserInfoKey和UIKeyboardAnimationCurveUserInfoKey(曲线值左移 16 位)。 - 修改约束后在动画 block 中调用
layoutIfNeeded才能触发动画。 - 通知注册/移除推荐在
viewWillAppear:/viewWillDisappear:,避免多页面冲突。 - iOS 15+ 可使用
UIKeyboardLayoutGuide简化适配,无需手动监听通知。 - 键盘通知是同步的,回调在主线程执行,可直接操作 UI。

浙公网安备 33010602011771号