基于键盘显示状态自动滚动UITableView内容的问题
嘿,这个问题我之前做项目的时候也碰到过——UITextView在tableView单元格里随着输入内容撑大,结果光标直接钻到键盘下面去了,打半天字都看不见自己输的啥,太闹心了!你之前尝试调整contentOffset的思路其实是对的,但大概率是没抓准时机或者计算逻辑有问题,我给你分享几个亲测有效的解决方案:
方案1:监听键盘+精准计算光标位置偏移
这个是最直接的思路,核心是在键盘弹出时,判断当前编辑的textView是否被键盘挡住,然后手动调整tableView的滚动位置。
步骤1:注册键盘通知
在你的UITableViewController的viewDidLoad里注册键盘弹出和收起的通知:
override func viewDidLoad() { super.viewDidLoad() // 注册键盘通知 NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) // 别忘了设置tableView自动行高 tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 100 }
步骤2:实现键盘回调逻辑
这里我们需要先拿到当前正在编辑的textView,然后计算它在tableView中的位置,和键盘位置对比,再调整contentOffset:
@objc private func keyboardWillShow(_ notification: Notification) { guard let userInfo = notification.userInfo, let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } // 获取当前正在编辑的textView guard let activeTextView = UIResponder.currentFirstResponder as? UITextView, let cell = activeTextView.superview?.superview as? UITableViewCell, let indexPath = tableView.indexPath(for: cell) else { return } // 把textView的frame转换成相对于tableView的坐标 let textViewFrameInTableView = tableView.convert(activeTextView.frame, from: activeTextView.superview) // 计算键盘顶部的y坐标(相对于tableView的bounds) let keyboardTopY = tableView.bounds.height - keyboardFrame.height // 计算textView底部的y坐标(要加上tableView当前的contentOffset) let textViewBottomY = textViewFrameInTableView.origin.y + textViewFrameInTableView.height + tableView.contentOffset.y // 如果textView底部超过键盘顶部,就调整滚动位置 if textViewBottomY > keyboardTopY { let neededOffset = textViewBottomY - keyboardTopY + 16 // 加16pt的间距,看着更舒服 tableView.contentOffset = CGPoint(x: 0, y: tableView.contentOffset.y + neededOffset) } // 同时调整tableView的contentInset,避免键盘挡住底部内容 tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardFrame.height, right: 0) tableView.scrollIndicatorInsets = tableView.contentInset } @objc private func keyboardWillHide(_ notification: Notification) { // 键盘收起时恢复原有的inset tableView.contentInset = .zero tableView.scrollIndicatorInsets = .zero }
步骤3:添加获取当前第一响应者的扩展
因为我们需要知道哪个textView正在被编辑,所以加个UIResponder的扩展:
extension UIResponder { static weak var currentFirstResponder: UIResponder? { _currentFirstResponder = nil UIApplication.shared.sendAction(#selector(findFirstResponder(_:)), to: nil, from: nil, for: nil) return _currentFirstResponder } private static weak var _currentFirstResponder: UIResponder? @objc private func findFirstResponder(_ sender: Any) { UIResponder._currentFirstResponder = self } }
方案2:配合iOS 11+的自动适配特性
从iOS11开始,UIScrollView(包括UITableView)新增了contentInsetAdjustmentBehavior属性,能帮我们自动处理部分偏移,结合上面的键盘逻辑,效果会更丝滑:
override func viewDidLoad() { super.viewDidLoad() // 开启自动适配 tableView.contentInsetAdjustmentBehavior = .always // 其他注册通知和自动行高的代码... }
方案3:监听textView内容变化,实时更新行高+调整滚动
因为你的textView是可调整大小的,每次输入内容都会让单元格高度变化,这时候tableView会重新布局,可能导致光标位置“跑掉”。所以可以在自定义cell里监听textView的contentSize变化,实时通知tableView更新行高,同时触发滚动调整:
自定义UITableViewCell
class ResizableTextViewCell: UITableViewCell { let textView = UITextView() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupTextView() } required init?(coder: NSCoder) { super.init(coder: coder) setupTextView() } private func setupTextView() { textView.delegate = self textView.isScrollEnabled = false // 禁止textView内部滚动,让单元格自动撑高 textView.font = UIFont.systemFont(ofSize: 16) // 添加到contentView并设置约束 contentView.addSubview(textView) textView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ textView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), textView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), textView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), textView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) ]) } // 通知tableView更新行高 private func updateTableViewRowHeight() { guard let tableView = superview as? UITableView, let indexPath = tableView.indexPath(for: self) else { return } tableView.beginUpdates() tableView.endUpdates() // 调用这个会触发tableView重新计算行高 // 发通知给控制器,让它调整滚动位置 NotificationCenter.default.post(name: NSNotification.Name("TextViewContentDidChange"), object: self) } } extension ResizableTextViewCell: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { updateTableViewRowHeight() } }
在控制器里监听通知,调整滚动
override func viewDidLoad() { super.viewDidLoad() // 监听textView内容变化的通知 NotificationCenter.default.addObserver(self, selector: #selector(adjustScrollPositionForTextView), name: NSNotification.Name("TextViewContentDidChange"), object: nil) // 其他代码... } @objc private func adjustScrollPositionForTextView() { // 这里可以复用方案1里的计算逻辑,重新检查光标位置并调整滚动 guard let activeTextView = UIResponder.currentFirstResponder as? UITextView else { return } // 手动触发一次键盘位置检查(如果键盘已经弹出) if let keyboardFrame = UIApplication.shared.windows.first?.windowScene?.keyboard?.frame { let textViewFrameInTableView = tableView.convert(activeTextView.frame, from: activeTextView.superview) let keyboardTopY = tableView.bounds.height - keyboardFrame.height let textViewBottomY = textViewFrameInTableView.origin.y + textViewFrameInTableView.height + tableView.contentOffset.y if textViewBottomY > keyboardTopY { let neededOffset = textViewBottomY - keyboardTopY + 16 tableView.contentOffset = CGPoint(x: 0, y: tableView.contentOffset.y + neededOffset) } } }
几个要注意的坑
- 一定要给tableView设置
rowHeight = UITableView.automaticDimension和estimatedRowHeight,否则单元格不会自动调整高度。 - 如果你的弹窗是present出来的,建议用
fullScreen的modalPresentationStyle,避免系统自带的布局干扰。 - 当textView刚成为第一响应者时,最好也触发一次滚动调整,避免一开始就被挡住。
内容的提问来源于stack exchange,提问作者Francis F
相关产品推荐
相关产品推荐

