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

如何实现类似Facebook Messenger的TableView联动键盘平滑控制?

实现Facebook Messenger式键盘与TableView滚动联动效果

这个需求我之前也研究过,只靠系统默认的键盘通知和becomeFirstResponder()/resignFirstResponder()确实做不到同步跟随滚动的效果,得手动介入键盘的位置控制和滚动联动逻辑。下面给你一套可行的实现方案,分步骤拆解:

一、提前准备:禁用系统自动调整&获取键盘高度

首先要阻止系统自动修改TableView的内边距,否则会干扰我们的自定义联动逻辑;同时提前获取键盘的高度,方便后续计算位置。

import UIKit

class ChatViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var inputTextField: UITextField!
    
    // 保存键盘高度
    private var keyboardHeight: CGFloat = 0
    // 记录滚动开始时的contentOffset,用于计算滚动偏移量
    private var initialScrollOffsetY: CGFloat = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self
        
        // 禁用系统自动调整TableView内边距,避免干扰自定义逻辑
        tableView.contentInsetAdjustmentBehavior = .never
        
        // 注册键盘通知,获取键盘高度
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow(_:)),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide(_:)),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
        
        // 可选:给输入框添加自定义工具栏(比如添加发送按钮等)
        let inputToolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 50))
        inputToolbar.barStyle = .default
        let sendButton = UIBarButtonItem(title: "发送", style: .done, target: self, action: #selector(sendMessage))
        inputToolbar.items = [UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), sendButton]
        inputTextField.inputAccessoryView = inputToolbar
    }
    
    // 获取键盘高度
    @objc private func keyboardWillShow(_ notification: Notification) {
        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        keyboardHeight = keyboardFrame.height
    }
    
    @objc private func keyboardWillHide(_ notification: Notification) {
        keyboardHeight = 0
    }
    
    @objc private func sendMessage() {
        // 发送消息逻辑
        inputTextField.resignFirstResponder()
    }
}

二、监听TableView滚动,同步调整键盘位置

核心逻辑是在滚动过程中实时计算键盘的目标位置,直接调整系统键盘的frame(这里需要注意iOS版本适配),同时同步修改TableView的内边距,避免内容被键盘遮挡。

extension ChatViewController {
    // 记录滚动开始时的偏移量
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        initialScrollOffsetY = scrollView.contentOffset.y
        // 如果键盘未唤起,先让输入框成为第一响应者弹出键盘
        if !inputTextField.isFirstResponder {
            inputTextField.becomeFirstResponder()
        }
    }
    
    // 滚动过程中实时调整键盘位置
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard keyboardHeight > 0 else { return }
        
        // 计算滚动的偏移量:向上滚动时delta为正,键盘需要向上移动
        let scrollDelta = scrollView.contentOffset.y - initialScrollOffsetY
        // 计算键盘的目标Y坐标:初始位置是屏幕底部减去键盘高度,向上滚动时减去delta
        let targetKeyboardY = view.frame.height - keyboardHeight - scrollDelta
        
        // 限制键盘的移动范围:不能完全超出屏幕(收起),也不能超过完全弹出的位置
        let clampedKeyboardY = max(min(targetKeyboardY, view.frame.height - keyboardHeight), view.frame.height)
        
        // 找到系统键盘的视图(iOS 13+适配场景窗口)
        if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
           let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }),
           let keyboardView = keyWindow.subviews.first(where: { String(describing: $0.classForCoder).contains("Keyboard") }) {
            // 无动画调整键盘位置,实现和滚动完全同步
            UIView.performWithoutAnimation {
                keyboardView.frame = CGRect(
                    x: 0,
                    y: clampedKeyboardY,
                    width: keyboardView.frame.width,
                    height: keyboardView.frame.height
                )
            }
            
            // 同步调整TableView内边距,避免内容被键盘遮挡
            tableView.contentInset = UIEdgeInsets(
                top: 0,
                left: 0,
                bottom: view.frame.height - clampedKeyboardY,
                right: 0
            )
            tableView.scrollIndicatorInsets = tableView.contentInset
        }
    }
    
    // 滚动结束后,让键盘自动归位到完全显示或完全收起状态
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        let currentKeyboardBottom = view.frame.height - tableView.contentInset.bottom
        let midPoint = view.frame.height - keyboardHeight / 2
        
        if currentKeyboardBottom < midPoint {
            // 滚动结束时键盘超过一半显示,自动完全弹出
            UIView.animate(withDuration: 0.3) {
                self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: self.keyboardHeight, right: 0)
                self.tableView.scrollIndicatorInsets = self.tableView.contentInset
                
                if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
                   let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }),
                   let keyboardView = keyWindow.subviews.first(where: { String(describing: $0.classForCoder).contains("Keyboard") }) {
                    keyboardView.frame = CGRect(
                        x: 0,
                        y: self.view.frame.height - self.keyboardHeight,
                        width: keyboardView.frame.width,
                        height: keyboardView.frame.height
                    )
                }
            }
        } else {
            // 滚动结束时键盘超过一半收起,自动完全隐藏
            UIView.animate(withDuration: 0.3, animations: {
                self.tableView.contentInset = .zero
                self.tableView.scrollIndicatorInsets = .zero
                
                if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
                   let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }),
                   let keyboardView = keyWindow.subviews.first(where: { String(describing: $0.classForCoder).contains("Keyboard") }) {
                    keyboardView.frame = CGRect(
                        x: 0,
                        y: self.view.frame.height,
                        width: keyboardView.frame.width,
                        height: keyboardView.frame.height
                    )
                }
            }, completion: { _ in
                // 隐藏后让输入框失去第一响应
                self.inputTextField.resignFirstResponder()
            })
        }
    }
}

三、注意事项与潜在风险

  1. 系统键盘视图的兼容性:通过类名查找系统键盘视图的方式,可能会在iOS版本更新后失效(苹果可能修改键盘的类名)。如果想要更稳定的方案,可以考虑自定义输入栏,将输入栏和系统键盘视为一个整体,通过调整输入栏的位置间接带动键盘移动,不过效果会稍差一些。
  2. iOS版本适配:代码中已经适配了iOS 13+的场景窗口逻辑,低版本可以直接使用UIApplication.shared.keyWindow。
  3. 滚动边界处理:一定要限制键盘的移动范围,避免出现键盘部分显示或超出屏幕的异常状态。

内容的提问来源于stack exchange,提问作者ltewarp0lleh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 04:03:45