将来自不同层级结构的点击手势传递给一个视图

移动开发 2026-07-09

我有两个重叠的UIViews,它们都是同一个父视图的子视图。

后面的视图有一个UITapGestureRecognizer,前面的视图有另一个手势识别器,例如UILongPressGestureRecognizer或 UIPanGestureRecognizer。

我的目标是:

  • 如果用户执行的是前视图处理的手势,前视图应该处理它;
  • 如果用户执行的是另外一个手势,例如点击,后面的视图应该接收它;
  • 前视图不应该阻塞它不处理的手势。

我最初的问题是拖拽手势引起的,但同样的问题也会出现在长按手势上。

我尝试将cancelsTouchesInView设置为false,也尝试使用UIGestureRecognizerDelegate,但前视图仍然在命中测试中获胜。因此,在重叠区域内点击时,后面的视图的点击手势不会被触发。

下面是一个在前视图上使用长按手势的最小示例:

import SwiftUI
import UIKit

struct TestGestureConflict: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> TestGestureConflictViewController {
        TestGestureConflictViewController()
    }

    func updateUIViewController(_ uiViewController: TestGestureConflictViewController, context: Context) {}
}

final class TestGestureConflictViewController: UIViewController {
    private let tapView = UIView()
    private let longPressView = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .systemBackground
        configureTapView()
        configureLongPressView()
        configureGestures()
    }

    private func configureTapView() {
        tapView.translatesAutoresizingMaskIntoConstraints = false
        tapView.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.35)
        tapView.layer.borderColor = UIColor.systemBlue.cgColor
        tapView.layer.borderWidth = 2

        view.addSubview(tapView)

        NSLayoutConstraint.activate([
            tapView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            tapView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            tapView.widthAnchor.constraint(equalToConstant: 120),
            tapView.heightAnchor.constraint(equalToConstant: 120)
        ])
    }

    private func configureLongPressView() {
        longPressView.translatesAutoresizingMaskIntoConstraints = false
        longPressView.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.35)
        longPressView.layer.borderColor = UIColor.systemOrange.cgColor
        longPressView.layer.borderWidth = 2

        view.addSubview(longPressView)

        NSLayoutConstraint.activate([
            longPressView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            longPressView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            longPressView.widthAnchor.constraint(equalToConstant: 220),
            longPressView.heightAnchor.constraint(equalToConstant: 220)
        ])
    }

    private func configureGestures() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
        tapGesture.delegate = self
        tapView.addGestureRecognizer(tapGesture)

        let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
        longPressGesture.cancelsTouchesInView = false
        longPressGesture.delegate = self
        longPressView.addGestureRecognizer(longPressGesture)
    }

    @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
        guard gestureRecognizer.state == .ended else { return }
        print("tap")
    }

    @objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
        switch gestureRecognizer.state {
        case .began:
            print("long press began")

        case .ended, .cancelled:
            print("long press ended")

        default:
            break
        }
    }
}

extension TestGestureConflictViewController: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        // This is called for the long press gesture, but returning true or false
        // does not make the tapView behind receive the tap in the overlapping area.
        return true
    }
}

在这个示例中,longPressView被添加在tapView上方。

期望行为:

  • 橙色视图内的长按 → 由longPressView处理;
  • 重叠的蓝色/橙色区域内的点击 → 由tapView处理。

实际行为:

  • 前视图上的长按能正常工作;
  • 点击无法到达后面的视图;
  • cancelsTouchesInView = false也无法解决问题,因为前视图仍然是命中测试的结果。

我的理解是,这实际上并不是关于UIPanGestureRecognizer或 UILongPressGestureRecognizer的问题,而是关于UIKit的命中测试:一旦前视图被选为命中测试的视图,后面的视图就不会参与触摸分发。

对于这种情况,正确的UIKit做法是什么?

我应该用以下哪种方法来解决?

  • 自定义hitTest(::) / point(inside:with:),
  • 一个UIGestureRecognizerDelegate,
  • 手动转发触摸事件,
  • 将手势识别器附加到一个公共父视图,
  • 还是改变视图层级结构?

有什么推荐的方式可以让前视图只处理特定手势,同时允许其他手势(例如点击)由后面的视图处理?

解决方案

核心问题不是手势识别冲突;而是命中测试/视图层级的问题。位于前视图后面的同级视图永远不会接收到触摸,因为UIKit会先选择一个单独的命中测试视图。cancelsTouchesInView = false 仅在触摸已经命中测试后再进行交付时生效;它不会让UIKit去搜索下面的同级视图。

最推荐的做法是 - 将手势识别器放在同一层级结构中

而不是:

parent
 ├── tapView       (tap recognizer)
 └── longPressView (long press recognizer, above)

改为:

parent
 └── container
      ├── tapView
      └── frontOverlay

或者将两个识别器附加到一个公共父视图:

let tap = UITapGestureRecognizer(
    target: self,
    action: #selector(handleTap)
)

let longPress = UILongPressGestureRecognizer(
    target: self,
    action: #selector(handleLongPress)
)

containerView.addGestureRecognizer(tap)
containerView.addGestureRecognizer(longPress)

tap.delegate = self
longPress.delegate = self

然后控制识别:

func gestureRecognizer(
    _ gestureRecognizer: UIGestureRecognizer,
    shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
    true
}

这是可行的原因是在同一响应者层级上附着的手势识别器可以协作。

第二种不那么推荐的做法是 -

在前视图上使用自定义手势识别器

对于你的具体情况,这通常是最简洁的做法:

  • 将一个pan / 长按识别器放在公共父视图上;
  • 检测它是否转变成你要的手势;
  • 如果它仍然是点击手势,就让点击手势识别器获胜。

示例:

longPress.require(toFail: tap)

或者:

func gestureRecognizer(
    _ gestureRecognizer: UIGestureRecognizer,
    shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
    if gestureRecognizer === longPress,
       otherGestureRecognizer === tap {
        return true
    }

    return false
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章