如何解决UITextView contentInset默认动画干扰键盘同步滚动问题
问题根源
你遇到的动画顺序错乱、体验割裂问题,本质是两个实现错误导致的:
- 修改
contentInset的操作没有放入和键盘弹出参数完全一致的系统动画块中,inset变更和键盘动画不在同一个动画事务里 - UITextView内置的「自动滚动光标到可见区域」逻辑,会在键盘动画执行完成后自动触发,这个隐式滚动无法通过常规的
UIView.setAnimationsEnabled(false)方式禁用,如果你在这之前调用自定义滚动方法,要么会被这个隐式动画覆盖,要么会等它执行完才生效,自然出现动画断层。
正确实现方案
核心思路是把inset更新、自定义滚动逻辑全部放到和键盘弹出完全同步的动画块中执行,在系统触发自动滚动检查前就把目标位置滚到可见区域,从根源上避免隐式动画触发。
实现要点如下:
- 不要硬编码动画时长、动画曲线,所有动画参数从键盘通知的
userInfo中读取,保证和系统键盘动画完全一致 - 提前计算好需要滚动到的目标区域(可以是光标位置、文本末尾或者任意指定位置)
- 在同一个动画块内同时更新inset、执行滚动操作,滚动方法的
animated参数传false,避免叠加UIScrollView内置动画 - 同步实现键盘隐藏的回调,恢复初始inset
完整实现代码:
// 键盘弹出适配 @objc func keyboardWillShow(notification: NSNotification) { guard let userInfo = notification.userInfo, let keyboardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect, let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double, let curveRaw = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt, let selectedRange = textView.selectedTextRange else { return } // 计算适配键盘的底部inset let bottomInset = keyboardRect.height - view.safeAreaInsets.bottom let targetInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0) // 计算目标滚动区域,这里以光标位置为例,可替换为任意自定义位置 let cursorRect = textView.caretRect(for: selectedRange.end) // 给光标位置加10pt上下边距,避免滚动后光标贴边 let targetRect = cursorRect.insetBy(dx: 0, dy: -10) // 用系统键盘的动画参数创建动画块 let animOptions = UIView.AnimationOptions(rawValue: curveRaw << 16) UIView.animate(withDuration: duration, delay: 0, options: [animOptions, .beginFromCurrentState]) { // 同一动画块内更新inset self.textView.contentInset = targetInset self.textView.scrollIndicatorInsets = targetInset // 同一动画块内执行自定义滚动,animated传false避免叠加额外动画 self.textView.scrollRectToVisible(targetRect, animated: false) } } // 键盘隐藏恢复 @objc func keyboardWillHide(notification: NSNotification) { guard let userInfo = notification.userInfo, let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double, let curveRaw = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { return } let animOptions = UIView.AnimationOptions(rawValue: curveRaw << 16) UIView.animate(withDuration: duration, delay: 0, options: [animOptions, .beginFromCurrentState]) { self.textView.contentInset = .zero self.textView.scrollIndicatorInsets = .zero } }
注意事项
- 所有逻辑必须放在
keyboardWillShow/keyboardWillHide回调中执行,不要用keyboardDidShow,后者触发时键盘动画已经结束,必然出现断层 - 动画选项需要拼接
.beginFromCurrentState,避免和textView正在执行的其他滚动动画冲突 - 计算底部inset时记得减去
safeAreaInsets.bottom,避免在全面屏机型上底部inset计算过大 - 如果需要滚动到自定义位置,直接替换代码中
targetRect的计算逻辑即可
内容的提问来源于stack exchange,提问作者Deepak Sharma
相关产品推荐
相关产品推荐

