如何实现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
相关产品推荐
相关产品推荐

