You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于键盘显示状态自动滚动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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 04:26:56