如何实现类似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() }) } } }
三、注意事项与潜在风险
- 系统键盘视图的兼容性:通过类名查找系统键盘视图的方式,可能会在iOS版本更新后失效(苹果可能修改键盘的类名)。如果想要更稳定的方案,可以考虑自定义输入栏,将输入栏和系统键盘视为一个整体,通过调整输入栏的位置间接带动键盘移动,不过效果会稍差一些。
- iOS版本适配:代码中已经适配了iOS 13+的场景窗口逻辑,低版本可以直接使用
UIApplication.shared.keyWindow。 - 滚动边界处理:一定要限制键盘的移动范围,避免出现键盘部分显示或超出屏幕的异常状态。
内容的提问来源于stack exchange,提问作者ltewarp0lleh
相关产品推荐
相关产品推荐

