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

如何仅将顶层视图的垂直平移手势传递至下方视图

需求:仅传递垂直平移手势到底层滚动视图

场景说明

  • 两个同级视图共享同一父视图:顶层视图包含按钮、水平滚动集合视图等可交互子视图,底层是垂直滚动视图,顶层部分遮挡底层
  • 核心要求:仅将顶层视图上的垂直平移手势传递至下方滚动视图,其他所有触摸类型(点击、水平滚动等)由顶层视图及其子视图正常响应

现有方案的问题

方案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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 23:51:37