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

如何实现UIPanGestureRecognizer在多子视图上的跨视图回调?

嘿,这个需求我刚好碰到过,有几个比硬算frame优雅得多的实现方式,给你捋捋:

优雅实现滑动经过子视图触发操作的方案

方案一:利用系统hitTest+路径追踪(适合已有自定义视图)

不用自己手动计算子视图的frame,直接借助UIKit的hitTest(_:with:)方法让系统帮你定位当前手势命中的视图。为了避免快速滑动跳过中间子视图,还可以结合路径追踪,检查滑动线段经过的所有子视图:

首先在自定义View里定义几个追踪用的属性:

class TicTacToeView: UIView {
    private var currentSelectedSubview: UIView?
    private var lastGestureLocation: CGPoint?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupPanGesture()
        setupSubviews() // 你的9个子视图布局逻辑
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupPanGesture()
        setupSubviews()
    }
    
    private func setupPanGesture() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        addGestureRecognizer(panGesture)
    }
}

然后实现手势处理方法:

@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
    let currentLocation = gesture.location(in: self)
    
    switch gesture.state {
    case .began:
        // 记录初始位置,处理第一个命中的子视图
        lastGestureLocation = currentLocation
        guard let hitView = hitTest(currentLocation, with: nil), subviews.contains(hitView) else { return }
        performAction(for: hitView)
        currentSelectedSubview = hitView
        
    case .changed:
        guard let lastLoc = lastGestureLocation else { return }
        
        // 创建滑动路径的线段
        let swipePath = UIBezierPath()
        swipePath.move(to: lastLoc)
        swipePath.addLine(to: currentLocation)
        
        // 遍历所有子视图,检查路径是否穿过该视图
        for subview in subviews {
            let subviewFramePath = UIBezierPath(rect: subview.frame)
            if swipePath.intersects(subviewFramePath), subview != currentSelectedSubview {
                performAction(for: subview)
                currentSelectedSubview = subview
            }
        }
        lastGestureLocation = currentLocation
        
    case .ended, .cancelled:
        // 重置追踪状态
        currentSelectedSubview = nil
        lastGestureLocation = nil
        
    default:
        break
    }
}

// 你自己的操作逻辑,比如根据子视图的tag或者索引执行对应动作
private func performAction(for subview: UIView) {
    print("触发子视图操作:\(subview.tag)")
    // 这里写你的具体业务代码
}

这个方案的好处是完全利用系统API,不用自己维护frame计算,代码可读性和可维护性都很高,还能处理快速滑动的场景。

方案二:用UICollectionView简化布局与手势处理(推荐新实现)

如果你的井字棋布局还没完全定型,直接用UICollectionView会更省心——它帮你搞定子视图(Cell)的布局、复用,甚至可以直接通过indexPathForItem(at:)获取手势位置对应的Cell:

首先设置CollectionView的布局为3列的FlowLayout:

class TicTacToeViewController: UIViewController {
    private var collectionView: UICollectionView!
    private var currentIndexPath: IndexPath?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCollectionView()
        setupPanGesture()
    }
    
    private func setupCollectionView() {
        let layout = UICollectionViewFlowLayout()
        let itemSize = (view.bounds.width - 2*10)/3 // 假设间距是10
        layout.itemSize = CGSize(width: itemSize, height: itemSize)
        layout.minimumLineSpacing = 10
        layout.minimumInteritemSpacing = 10
        layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.register(TicTacToeCell.self, forCellWithReuseIdentifier: "Cell")
        collectionView.dataSource = self
        collectionView.delegate = self
        view.addSubview(collectionView)
    }
    
    private func setupPanGesture() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        collectionView.addGestureRecognizer(panGesture)
    }
}

// 实现UICollectionViewDataSource
extension TicTacToeViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 9
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! TicTacToeCell
        // 配置Cell样式
        return cell
    }
}

然后处理手势:

@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
    let currentLocation = gesture.location(in: collectionView)
    
    switch gesture.state {
    case .began:
        guard let indexPath = collectionView.indexPathForItem(at: currentLocation) else { return }
        performAction(for: indexPath)
        currentIndexPath = indexPath
        
    case .changed:
        guard let lastIndexPath = currentIndexPath else { return }
        
        // 同样可以加路径追踪优化快速滑动的情况,逻辑和方案一类似
        let lastLocation = collectionView.layoutAttributesForItem(at: lastIndexPath)?.center ?? currentLocation
        let swipePath = UIBezierPath()
        swipePath.move(to: lastLocation)
        swipePath.addLine(to: currentLocation)
        
        // 遍历所有可见Cell,或者所有Item,检查路径是否穿过
        for indexPath in collectionView.indexPathsForVisibleItems {
            guard let attributes = collectionView.layoutAttributesForItem(at: indexPath) else { continue }
            let cellPath = UIBezierPath(rect: attributes.frame)
            if swipePath.intersects(cellPath), indexPath != lastIndexPath {
                performAction(for: indexPath)
                currentIndexPath = indexPath
            }
        }
        
    case .ended, .cancelled:
        currentIndexPath = nil
        
    default:
        break
    }
}

private func performAction(for indexPath: IndexPath) {
    print("触发第\(indexPath.item)个Cell的操作")
    // 业务逻辑代码
}

用CollectionView的好处是后期如果要修改布局(比如改成4x4),只需要调整FlowLayout的参数即可,完全不用修改手势逻辑,扩展性极强。

额外注意事项

  • 如果你的子视图有自己的手势,需要设置UIGestureRecognizerDelegate的gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)方法返回true,避免手势冲突。
  • 路径追踪的逻辑可以根据需求调整精度,比如用CGPath的intersection方法做更精确的判断。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 10:15:56