如何仅将顶层视图的垂直平移手势传递至下方视图
需求:仅传递垂直平移手势到底层滚动视图
场景说明
- 两个同级视图共享同一父视图:顶层视图包含按钮、水平滚动集合视图等可交互子视图,底层是垂直滚动视图,顶层部分遮挡底层
- 核心要求:仅将顶层视图上的垂直平移手势传递至下方滚动视图,其他所有触摸类型(点击、水平滚动等)由顶层视图及其子视图正常响应
现有方案的问题
方案1:重写hitTest(_:with:)
通过让顶层视图仅响应子视图的触摸,自身不接收事件:
class TopView: UIView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let superHitTest = super.hitTest(point, with: event) return (superHitTest === self) ? nil : superHitTest } }
问题:起始于子视图的垂直平移手势无法传递到底层滚动视图,导致用户垂直滑动时出现无效区域。
方案2:重写point(inside:with:)+手动添加手势识别器
让顶层视图不接收任何触摸,再通过父视图添加手势处理特定交互:
class TopView: UIView { override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return false } } class ViewController: UIViewController { var tapGestureRecognizer: UITapGestureRecognizer? override func viewDidLoad() { super.viewDidLoad() let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap(_:))) view.addGestureRecognizer(tapGestureRecognizer) self.tapGestureRecognizer = tapGestureRecognizer } @objc func onTap(_ recognizer: UITapGestureRecognizer) { // 自定义命中测试逻辑 } }
问题:需要手动处理所有顶层子视图的交互(比如水平集合视图的滚动),逻辑复杂度大幅提升。
最优解决方案:基于手势识别器代理的事件分流
通过给顶层视图添加垂直平移手势识别器,结合代理方法区分手势类型,实现精准的事件传递:
1. 顶层视图实现
class TopView: UIView, UIGestureRecognizerDelegate { // 弱引用底层滚动视图,避免循环引用 weak var underlyingScrollView: UIScrollView? override init(frame: CGRect) { super.init(frame: frame) setupVerticalPanGesture() } required init?(coder: NSCoder) { super.init(coder: coder) setupVerticalPanGesture() } private func setupVerticalPanGesture() { let verticalPan = UIPanGestureRecognizer(target: self, action: #selector(handleVerticalPan(_:))) verticalPan.delegate = self // 不取消子视图的触摸事件,确保其他交互优先响应 verticalPan.cancelsTouchesInView = false addGestureRecognizer(verticalPan) } @objc private func handleVerticalPan(_ gesture: UIPanGestureRecognizer) { guard let scrollView = underlyingScrollView else { return } switch gesture.state { case .began: // 触发底层滚动视图的手势开始状态 scrollView.gestureRecognizers?.forEach { if let panGesture = $0 as? UIPanGestureRecognizer { panGesture.state = .began } } case .changed: // 将垂直位移传递给底层滚动视图 let translation = gesture.translation(in: scrollView) let newOffset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y - translation.y) scrollView.setContentOffset(newOffset, animated: false) gesture.setTranslation(.zero, in: scrollView) default: scrollView.gestureRecognizers?.forEach { if let panGesture = $0 as? UIPanGestureRecognizer { panGesture.state = gesture.state } } } } // 判断是否启动垂直平移手势:仅当垂直位移显著大于水平位移时触发 func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { return true } let translation = panGesture.translation(in: self) return abs(translation.y) > abs(translation.x) * 1.5 } // 允许顶层子视图的手势与垂直平移手势共存 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { // 优先让水平滚动类的子视图处理自身手势 if otherGestureRecognizer.view is UICollectionView, let otherPan = otherGestureRecognizer as? UIPanGestureRecognizer { let translation = otherPan.translation(in: self) return abs(translation.x) > abs(translation.y) } // 按钮等点击类手势优先由子视图处理 return otherGestureRecognizer.view != self } }
2. 视图控制器关联设置
class ViewController: UIViewController { @IBOutlet weak var topView: TopView! @IBOutlet weak var underlyingScrollView: UIScrollView! override func viewDidLoad() { super.viewDidLoad() // 关联顶层视图与底层滚动视图 topView.underlyingScrollView = underlyingScrollView // 确保底层滚动视图允许用户交互 underlyingScrollView.isUserInteractionEnabled = true } }
方案优势
- 精准分流:通过手势位移判断,仅传递垂直平移手势,不干扰顶层的点击、水平滚动等交互
- 覆盖全场景:起始于顶层子视图的垂直平移也能被识别并传递到底层滚动视图,消除无效区域
- 低侵入性:不需要修改
hitTest或point(inside:)的默认逻辑,避免破坏原有交互体系
内容的提问来源于stack exchange,提问作者willbattel
相关产品推荐
相关产品推荐

