UIScrollView如何实现类似WhatsApp拖动未触键盘就启动交互式收起
预期实现效果(与WhatsApp一致)

从动图中可以看到,WhatsApp的键盘收起操作在拖动触碰到底部栏边缘时就会触发,无需接触到键盘边缘。请问需要使用什么技术方案才能实现该行为?
补充说明
我们的底部工具栏会跟随键盘移动,因为底部工具栏的底部与安全区(Safe Area)底部之间设置了约束。我们通过在全局Window上添加手势识别器来调整底部约束的constant值,相关实现代码片段如下:
@objc private func didPan(_ sender: UIPanGestureRecognizer){ if keyboardHeight > 0 { let mainScrollView = editable.mainScrollView let isScrolling = (mainScrollView.isDragging || mainScrollView.isDecelerating) if isScrolling { if let mainScrollViewGlobalOrigin = mainScrollView.globalOrigin { let point = sender.location(in: sender.view!) // Take safe area into consideration, like iPhone 12 Pro Max. let key = UIWindow.key let bottomSafeArea = key?.safeAreaInsets.bottom ?? 0 let dy = point.y - ( mainScrollViewGlobalOrigin.y + mainScrollView.frame.height + toolbarHeightLayoutConstraint.constant + bottomSafeArea - bottomLayoutConstraint.constant - self.keyboardHeight ) if dy > 0 { bottomLayoutConstraint.constant = -(keyboardHeight - dy) } } } } }
解决方案
你可以在现有手势逻辑的基础上做如下改造,即可实现预期交互效果:
- 允许多手势同时识别
实现UIGestureRecognizerDelegate代理方法,允许你添加的全局pan手势和UIScrollView自带的滚动手势同时响应:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true }
- 改造手势处理逻辑,手动同步键盘位移
在原有didPan方法基础上新增键盘位置控制逻辑,不需要依赖系统默认的键盘交互触发规则:
// 新增变量存储手势开始时的键盘初始Y坐标 private var initialKeyboardY: CGFloat = 0 @objc private func didPan(_ sender: UIPanGestureRecognizer){ guard keyboardHeight > 0, let mainScrollView = editable.mainScrollView else { return } let isScrolling = (mainScrollView.isDragging || mainScrollView.isDecelerating) guard isScrolling else { return } // 获取键盘所在的独立Window guard let keyboardWindow = UIApplication.shared.windows.first(where: { $0.windowLevel == .keyboard }) else { return } switch sender.state { case .began: // 手势启动时记录键盘初始位置 initialKeyboardY = keyboardWindow.frame.minY case .changed: guard let mainScrollViewGlobalOrigin = mainScrollView.globalOrigin, let keyWindow = UIWindow.key else { return } let point = sender.location(in: sender.view!) let bottomSafeArea = keyWindow.safeAreaInsets.bottom ?? 0 // 自定义触发阈值:底部工具栏的上边缘Y坐标 let triggerThresholdY = mainScrollViewGlobalOrigin.y + mainScrollView.frame.height let dy = point.y - triggerThresholdY guard dy > 0 else { return } // 同步更新底部工具栏约束和键盘位置 bottomLayoutConstraint.constant = -(keyboardHeight - dy) keyboardWindow.frame.origin.y = initialKeyboardY + dy case .ended, .cancelled: let velocity = sender.velocity(in: sender.view).y let currentKeyboardOffset = keyboardWindow.frame.minY - initialKeyboardY // 判断收起逻辑:拖动超过键盘一半高度 或 向下滑动速度足够大就收起,否则回弹 let shouldDismiss = currentKeyboardOffset > keyboardHeight / 2 || velocity > 300 UIView.animate(withDuration: 0.25) { if shouldDismiss { self.bottomLayoutConstraint.constant = 0 keyboardWindow.frame.origin.y = UIScreen.main.bounds.height self.view.endEditing(true) } else { self.bottomLayoutConstraint.constant = -self.keyboardHeight keyboardWindow.frame.origin.y = self.initialKeyboardY } self.view.layoutIfNeeded() } default: break } }
- 可选兼容方案
如果你不想直接操作系统键盘Window,也可以监听UIResponder.keyboardWillChangeFrameNotification通知,在系统回调中同步更新底部工具栏约束,效果完全一致。
内容的提问来源于stack exchange,提问作者Cheok Yan Cheng
相关产品推荐
相关产品推荐

