UIViewController-KeyboardAnimation使用6大常见坑清单:忘记取消订阅、Block循环引用如何避免
【免费下载链接】UIViewController-KeyboardAnimationShowing/dismissing keyboard animation in UIViewController category.项目地址: https://gitcode.com/gh_mirrors/ui/UIViewController-KeyboardAnimation
UIViewController-KeyboardAnimation是一款轻量的 iOS 键盘动画开源组件,它通过 UIViewController 分类(Category)提供订阅式 API:键盘出现、消失或尺寸变化时,你的动画 Block 会自动以键盘原生动画的时长与曲线被执行,无需手写通知监听代码。本清单针对新手最常踩的 6 个坑——尤其是忘记取消订阅和Block 循环引用——给出症状、原因和正确做法。
1 分钟了解:这个组件解决了什么 🎯
在 iOS 中处理键盘,通常需要监听UIKeyboardWillShowNotification、UIKeyboardWillHideNotification等通知,手动取出键盘高度和动画时长,再包一层UIView动画——每个页面都要复制一遍这套代码。
本组件把这些重复劳动封装成了 4 个订阅 API(定义在 UIViewController+KeyboardAnimation.h 中):
an_subscribeKeyboardWithAnimations:completion:—— 键盘出现/消失动画 + 完成回调an_subscribeKeyboardWithBeforeAnimations:animations:completion:—— 额外提供"动画开始前"的执行时机an_subscribeKeyboardFrameChangesWithAnimations:—— 响应键盘尺寸变化(如旋转屏幕时键盘高度改变)an_unsubscribeKeyboard/an_unsubscribeKeyboardFrameChanges—— 取消订阅
官方示例(聊天输入框跟随键盘上移)可直接参考演示工程 ViewController.m,核心逻辑只有十几行。
快速上手:安装与最小订阅步骤
在 Podfile 中添加依赖后安装:
pod 'UIViewController+KeyboardAnimation', '~> 1.3'也可以直接复制头文件与实现文件(UIViewController+KeyboardAnimation.h、UIViewController+KeyboardAnimation.m)进工程,或使用 git submodule 方式引入仓库(仓库地址:https://gitcode.com/gh_mirrors/ui/UIViewController-KeyboardAnimation)。
最小订阅写法:
// viewWillAppear 中订阅 __weak typeof(self) weakSelf = self; [self an_subscribeKeyboardWithAnimations:^(CGRect keyboardRect, NSTimeInterval duration, BOOL isShowing) { __strong typeof(weakSelf) self = weakSelf; self.inputBottomSpace.constant = isShowing ? CGRectGetHeight(keyboardRect) : 0; [self.view layoutIfNeeded]; } completion:nil];到这里就能跑起来,但以下 6 个坑不避开,轻则 UI 不跟随,重则内存泄漏。
6 大常见坑清单 ⚠️
坑 1:忘记调用 an_unsubscribeKeyboard —— 别页面上"幽灵"动画
这是使用率最高的 Bug。头文件中的官方警告原文大意是:如果当前视图消失时不取消订阅,已订阅的视图控制器会在其他页面上继续响应键盘事件。
原因在实现里很直白:订阅时通过objc_setAssociatedObject把 Block 挂在视图控制器上(见 UIViewController+KeyboardAnimation.m),并注册了键盘通知观察者;只要视图控制器对象还活着(比如它还在导航栈或 Tab 里),通知照常派发,动画照常执行——哪怕页面早已不可见。
✅ 正确做法:在viewWillDisappear中取消订阅,与订阅成对出现:
- (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self an_unsubscribeKeyboard]; }⚠️ 额外注意:an_unsubscribeKeyboard只清理"出现/消失"通道的 Block 与观察者,不会清理键盘尺寸变化通道。如果你同时订阅了an_subscribeKeyboardFrameChangesWithAnimations:,必须再调用一次an_unsubscribeKeyboardFrameChanges,两条通道互不代替。
坑 2:Block 内强引用 self —— 键盘动画最常见的循环引用
这个坑的头文件里已经写明了(@warning These blocks will be holding inside UIViewController which calls it, so as with any block-style API avoid a retain cycle)。
循环链路是这样的:
- 视图控制器通过关联对象持有你传入的动画 Block(
OBJC_ASSOCIATION_COPY_NONATOMIC会拷贝并强持有 Block 内容); - Block 里又强引用了
self(视图控制器本身); - 结果:控制器 → Block → 控制器,谁也不放手,视图控制器永远无法释放。
✅ 正确做法:Block 内使用弱引用,官方 README 示例采用 ReactiveCocoa 的@weakify/@strongify,不用该库时标准__weak写法完全等价:
__weak typeof(self) weakSelf = self; [self an_subscribeKeyboardWithAnimations:^(CGRect keyboardRect, NSTimeInterval duration, BOOL isShowing) { __strong typeof(weakSelf) self = weakSelf; if (!self) return; // 修改布局…… } completion:nil];同样的规则适用于beforeAnimations、animations、completion三个 Block 参数——任何一个强捕获self都会形成循环引用。
坑 3:在 viewDidLoad 里订阅 —— 订阅时机不对
很多新手默认"初始化"就该放viewDidLoad,但组件文档对每个订阅 API 都标注了官方建议:viewWillAppear是订阅的最佳时机(@tip viewWillAppear is the best place to subscribe to keyboard events)。
在viewDidLoad订阅的问题:
- 视图控制器可能已被创建但尚未显示(例如被 push 前的准备阶段),此时键盘状态可能已变化,而
WillShow/WillHide通知只在状态切换瞬间发一次,错过就补不回来,导致页面出现时 UI 与键盘位置错乱; - 页面每次重新出现都会重走订阅逻辑,与坑 5 叠加会反复添加观察者。
✅ 正确做法:订阅放viewWillAppear,取消订阅放viewWillDisappear,让订阅的生命周期与页面可见性严格对齐(官方 Demo 正是这样做的,见 ViewController.m)。
坑 4:Auto Layout 改了约束却不调用 layoutIfNeeded —— 动画"跳变"不丝滑
动画 Block 是在UIView animate动画事务内部被调用的,这是本组件最大的卖点:你在 Block 里做的修改会自动享受键盘原生的时长和曲线。但有一个前提条件被新手频繁忽略——文档原话:"If using auto layout don't forget to call layoutIfNeeded"。
如果你只修改了约束常量:
self.inputBottomSpace.constant = isShowing ? CGRectGetHeight(keyboardRect) : 0; // 漏了 [self.view layoutIfNeeded];那么约束值虽然变了,但视图并没有在动画事务内真正布局,表现就是:输入框"瞬移"到目标位置,失去了与键盘同步滑动的效果。
✅ 正确做法:在动画 Block 的最后一行加上[self.view layoutIfNeeded],让约束变更进入当前动画事务。
坑 5:重复订阅 subscribe —— 按一次键盘,动画执行 N 次
an_subscribeKeyboardWithAnimations:completion:不是幂等的:每调用一次,都会执行一次addObserver注册新的通知观察者(见 UIViewController+KeyboardAnimation.m 中an_subscribeKeyboardWithAnimations:completion:的实现)。Block 本身会被新值覆盖,但观察者会叠加。
典型事故场景:把订阅写进了viewWillAppear之外的地方(如viewDidAppear里根据条件反复触发,或页面复用组件时重复初始化),订阅 3 次后,键盘每弹起一次,动画 Block 就被派发执行 3 次——布局被连续改写,动画卡顿甚至错乱。
✅ 正确做法:
- 订阅只做一次,或确保"先
an_unsubscribeKeyboard再重新订阅"; - 分清两个通道:
WillShow/WillHide(出现/消失)与WillChangeFrame(尺寸变化,如转屏后键盘高度变化)是两条独立的通知通道,按需选择,避免同一个页面同时订阅两套后又各自忘记清理(参考坑 1)。
坑 6:误读 isShowing 与 keyboardRect —— 键盘收起了,UI 却回不去
动画 Block 的第三个参数isShowing表示"这次是出现还是消失",而keyboardRect是键盘最终frame(屏幕坐标系)。两个高频误用:
- 不区分 isShowing 就写布局。键盘消失时 Block 同样会被调用(
isShowing为NO),如果只写"上移"逻辑、没有对应的"复位"分支,输入框就会永远悬在键盘收起后的位置。正确写法是对两个分支各写一套状态:
if (isShowing) { self.inputBottomSpace.constant = CGRectGetHeight(keyboardRect); } else { self.inputBottomSpace.constant = 0; }- 直接拿 keyboardRect 当视图坐标用。
keyboardRect来自UIKeyboardFrameEndUserInfoKey,是窗口(屏幕)坐标系下的完整 frame,不要直接赋给视图的frame.origin;对"跟随键盘高度"类需求,取CGRectGetHeight(keyboardRect)即可,Demo 工程也是这么做的。
顺带一提:completion回调的参数finished为NO时表示动画在中途被打断(例如用户快速连点输入框),依赖动画完成的逻辑(如置回状态标志位)要判断这个值,官方复杂示例就是用它重置isKeaboardAnimation标记的。
6 大坑速查表 📋
| # | 坑 | 典型症状 | 正确做法 |
|---|---|---|---|
| 1 | 忘记取消订阅 | 别的页面弹键盘,本页 UI 跟着动 | viewWillDisappear调an_unsubscribeKeyboard;帧变化通道单独取消 |
| 2 | Block 强引用 self | 页面销毁后内存不回落 | Block 内__weak/@weakify弱引用 |
| 3 | viewDidLoad订阅 | 页面出现时 UI 与键盘错位 | 订阅放viewWillAppear,与取消订阅成对 |
| 4 | 忘记layoutIfNeeded | 布局瞬移、失去动画同步感 | Auto Layout 场景在动画 Block 末尾补上 |
| 5 | 重复订阅 / 混用双通道 | 一次键盘事件触发多次动画 | 订阅只做一次;两条通道分别管理 |
| 6 | 误读 isShowing / keyboardRect | 键盘收起后输入框不回落 | 分支处理复位;用CGRectGetHeight取高度 |
小结
UIViewController-KeyboardAnimation 的价值在于用极小的 API 面换掉了重复的键盘通知样板代码,而它把"订阅、取消订阅、Block 持有"这三件事的生命周期管理交还给了使用者。记住本文的 6 大坑,尤其是取消订阅成对调用与Block 弱引用 self这两条核心纪律,基本可以覆盖新手阶段全部的高频事故。组件本体仅头文件和实现文件两个源文件(UIViewController+KeyboardAnimation.h、UIViewController+KeyboardAnimation.m),配合 Demo 工程(Demo/KeyboardAnimationDemo/)通读一遍,你对它的所有行为都能建立确定性预期。
【免费下载链接】UIViewController-KeyboardAnimationShowing/dismissing keyboard animation in UIViewController category.项目地址: https://gitcode.com/gh_mirrors/ui/UIViewController-KeyboardAnimation
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考