Swift自定义控件添加双UITapGestureRecognizer及通用控件Selector失效问题
问题1:如何使用Swift为自定义控件分配两个不同的UITapGestureRecognizer?
给自定义控件添加多个UITapGestureRecognizer完全可行,核心是要处理好手之间的识别优先级,避免互相抢事件。我给你整理了具体的实现步骤和注意点:
第一步:创建两个手势并绑定处理方法
先分别为不同的点击动作创建手势实例,比如一个处理单次点击,一个处理双击,然后绑定对应的响应方法:class CustomControl: UIView { override init(frame: CGRect) { super.init(frame: frame) setupGestures() } required init?(coder: NSCoder) { super.init(coder: coder) setupGestures() } private func setupGestures() { // 单次点击手势 let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleSingleTap)) singleTap.numberOfTapsRequired = 1 addGestureRecognizer(singleTap) // 双击手势 let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) doubleTap.numberOfTapsRequired = 2 addGestureRecognizer(doubleTap) // 关键设置:让单次点击等双击识别失败后再触发 singleTap.require(toFail: doubleTap) } @objc private func handleSingleTap() { print("触发单次点击操作") } @objc private func handleDoubleTap() { print("触发双击操作") } }如果是两个单次点击手势(比如不同区域响应)
可以通过UIGestureRecognizerDelegate的代理方法区分点击位置,决定哪个手势响应:extension CustomControl: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { let touchPoint = touch.location(in: self) // 比如左半区域响应第一个手势,右半区域响应第二个 if gestureRecognizer == firstTapGesture { return touchPoint.x < bounds.width / 2 } else { return touchPoint.x >= bounds.width / 2 } } }
问题2:通用动态控件结合使用时Selector失效的问题
从你的描述来看,这个问题大概率和Selector方法的访问权限、手势重复添加冲突或者UIScrollView的触摸拦截有关,我给你逐一分析解决方案:
1. 检查Selector对应方法的访问权限
如果你的目标方法是private权限,在独立使用控件时可能还能正常调用,但嵌套在UIScrollView这类容器中时,运行时可能找不到该方法。建议把方法权限改为@objc internal或@objc public:
// 错误示例:private方法可能导致Selector失效 @objc private func handleViewTap() { backgroundColor = .systemBlue } // 正确示例:调整权限 @objc internal func handleViewTap() { backgroundColor = .systemBlue }
2. 避免手势重复添加
如果你的通用类在layoutSubviews这类会多次调用的方法中添加手势,会导致手势重复添加,互相冲突。建议在初始化时只添加一次手势,并且先移除已有手势再添加:
class GenericControl: UIView { private var target: AnyObject? private var tapAction: Selector? init(target: AnyObject?, action: Selector) { self.target = target self.tapAction = action super.init(frame: .zero) setupTapGesture() } required init?(coder: NSCoder) { super.init(coder: coder) setupTapGesture() } private func setupTapGesture() { // 先移除已有手势,避免重复添加 gestureRecognizers?.forEach { removeGestureRecognizer($0) } guard let target = target, let action = tapAction else { return } let tapGesture = UITapGestureRecognizer(target: target, action: action) tapGesture.cancelsTouchesInView = false // 不阻止其他控件的触摸事件 addGestureRecognizer(tapGesture) // 内置的背景变化逻辑直接写在类里,不用依赖外部Selector let internalTap = UITapGestureRecognizer(target: self, action: #selector(handleInternalTap)) internalTap.cancelsTouchesInView = false addGestureRecognizer(internalTap) } @objc private func handleInternalTap() { UIView.animate(withDuration: 0.2) { self.backgroundColor = self.backgroundColor == .white ? .lightGray : .white } } }
3. 解决UIScrollView的触摸拦截问题
UIScrollView默认会拦截触摸事件来处理滚动,可能导致内部控件的手势无法触发。可以做两个调整:
- 给UIScrollView设置
delaysContentTouches = false,让它立刻传递触摸事件给子控件 - 给通用控件的手势设置代理,允许和UIScrollView的手势同时识别:
// 在UIScrollView所在的控制器中设置 scrollView.delaysContentTouches = false // 给GenericControl添加代理实现 extension GenericControl: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return otherGestureRecognizer.view is UIScrollView } }
额外推荐:用闭包替代Selector(更Swift化)
Selector是OC的遗留特性,在Swift中用闭包传递点击事件更安全,不会出现运行时找不到方法的问题,代码也更简洁:
class GenericControl: UIView { private var tapClosure: (() -> Void)? init(tapClosure: (() -> Void)?) { self.tapClosure = tapClosure super.init(frame: .zero) setupTapGesture() } required init?(coder: NSCoder) { super.init(coder: coder) setupTapGesture() } private func setupTapGesture() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) addGestureRecognizer(tapGesture) } @objc private func handleTap() { // 执行内置背景变化 backgroundColor = .lightGray // 执行外部传入的闭包逻辑 tapClosure?() } } // 使用时直接传入闭包 let control = GenericControl(tapClosure: { print("外部点击逻辑触发") })
内容的提问来源于stack exchange,提问作者Benjamin RD
相关产品推荐
相关产品推荐

