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

自定义UIView的Combine扩展tabPublisher无法工作,求排查问题

问题:自定义UIView的tabPublisher手势识别器无法生效

我不清楚为什么自定义的UIView扩展中的tabPublisher无法正常工作。我查阅了Swift CombineCocoa库,想要实现UIView的Combine扩展(CombineCocoa并未提供UIView相关的扩展)。我参考了其他自定义UIView扩展的示例,但自己实现的代码没有效果。

我已参考CombineCocoa的CombineControlTarget.swift实现了以下代码:

extension Combine.Publishers {
    struct CustomTarget<Control: AnyObject>: Publisher {
        // swiftlint: disable nesting
        typealias Output = Void
        typealias Failure = Never
        // swiftlint: enable nesting
        let control: Control
        let addTargetAction: (Control, AnyObject, Selector) -> Void
        let removeTargetAction: (Control, AnyObject, Selector) -> Void
        
        init(
            control: Control,
            addTargetAction: @escaping (Control, AnyObject, Selector) -> Void,
            removeTargetAction: @escaping (Control, AnyObject, Selector) -> Void) {
            self.control = control
            self.addTargetAction = addTargetAction
            self.removeTargetAction = removeTargetAction
        }
        
        func receive<S>(subscriber: S) where S: Subscriber, S.Failure == Failure, S.Input == Output {
            let subscription = Subscription(
                subscriber: subscriber,
                control: control,
                addTargetAction: addTargetAction,
                removeTargetAction: removeTargetAction)
            subscriber.receive(subscription: subscription)
        }
    }
}

extension Combine.Publishers.CustomTarget {
    class Subscription<S: Subscriber, Control: AnyObject>: Combine.Subscription where S.Input == Void {
        private var subscriber: S?
        weak private var control: Control?
        
        let removeTargetAction: (Control, AnyObject, Selector) -> Void
        let action = #selector(handleAction)
        
        init(
            subscriber: S,
            control: Control,
            addTargetAction: @escaping (Control, AnyObject, Selector) -> Void,
            removeTargetAction: @escaping (Control, AnyObject, Selector) -> Void) {
            self.subscriber = subscriber
            self.control = control
            self.removeTargetAction = removeTargetAction
            addTargetAction(control, self, action)
        }
        
        func request(_ demand: Subscribers.Demand) {
            
        }
        
        func cancel() {
            subscriber = nil
            removeTargetAction(control!, self, action)
        }
        
        @objc func handleAction() {
            _ = subscriber?.receive()
        }
    }
}

extension UITapGestureRecognizer {
    var tapGesturePublisher: AnyPublisher<UITapGestureRecognizer, Never> {
        gesturePublisher(for: self)
    }
}

private func gesturePublisher<Gesture: UIGestureRecognizer>(for gesture: Gesture) -> AnyPublisher<Gesture, Never> {
    Publishers.CustomTarget(
        control: gesture,
        addTargetAction: { gesture, target, action in
            gesture.addTarget(target, action: action)},
        removeTargetAction: { gesture, target, action in
            gesture.removeTarget(target, action: action)
        })
    .subscribe(on: DispatchQueue.main)
    .map { gesture }
    .eraseToAnyPublisher()
}

以下是我自定义的UIView扩展代码:

extension UIView {
    var tapPublisher: AnyPublisher<UITapGestureRecognizer, Never> { // this is problem
        UITapGestureRecognizer().tapGesturePublisher
    }
}

该手势识别器并未生效,请问我的代码存在什么问题?


问题分析与解决方案

核心问题点

  • 手势未绑定到目标View:当前tapPublisher仅创建了UITapGestureRecognizer实例,但未调用addGestureRecognizer(_:)将手势添加到当前UIView上,手势无法监听View的点击事件。
  • 重复创建手势实例:每次访问tapPublisher属性都会生成新的手势对象,不仅浪费资源,还会导致订阅逻辑混乱。
  • 缺失手势生命周期管理:订阅取消时,仅移除了手势的target,但未将手势从View上移除,可能引发内存泄漏。

修正后的代码

修改UIView的扩展,确保手势被正确绑定到View、复用实例,同时在订阅取消时清理资源:

extension UIView {
    private struct AssociatedKeys {
        static var tapGestureKey = "tapGestureKey"
    }
    
    private var tapGesture: UITapGestureRecognizer {
        if let existingGesture = objc_getAssociatedObject(self, &AssociatedKeys.tapGestureKey) as? UITapGestureRecognizer {
            return existingGesture
        }
        let gesture = UITapGestureRecognizer()
        objc_setAssociatedObject(self, &AssociatedKeys.tapGestureKey, gesture, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        return gesture
    }
    
    var tapPublisher: AnyPublisher<UITapGestureRecognizer, Never> {
        // 确保View允许交互(部分UIView子类如UILabel默认关闭交互)
        self.isUserInteractionEnabled = true
        
        return Publishers.CustomTarget(
            control: tapGesture,
            addTargetAction: { [weak self] gesture, target, action in
                gesture.addTarget(target, action: action)
                self?.addGestureRecognizer(gesture)
            },
            removeTargetAction: { [weak self] gesture, target, action in
                gesture.removeTarget(target, action: action)
                self?.removeGestureRecognizer(gesture)
            })
        .subscribe(on: DispatchQueue.main)
        .map { self.tapGesture }
        .eraseToAnyPublisher()
    }
}

关键改进说明

  • 关联对象复用手势:通过objc_getAssociatedObject为每个UIView实例绑定唯一的手势对象,避免重复创建。
  • 绑定手势与View生命周期:在addTargetAction中添加手势到View,removeTargetAction中从View移除手势,确保手势随订阅状态正确管理。
  • 自动启用交互:设置isUserInteractionEnabled = true,避免因View默认交互关闭导致手势不响应。

内容的提问来源于stack exchange,提问作者안은노

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 14:54:56