Swift Combine: Check if Subject has observer?

后端 未结 2 2004
我寻月下人不归
我寻月下人不归 2021-01-24 10:13

In RxSwift we can check if a *Subject has any observer, using hasObserver, how can I do this in Combine on e.g. a PassthroughSubject?

2条回答
  •  甜味超标
    2021-01-24 10:53

    No one time needed this... Apple does not provide this by API, and, actually, I do not recommend such thing, because it is like manually checking value of retainCount in pre-ARC Objective-C for some decision in code.

    Anyway it is possible. Let's consider it as a lab exercise. Hope someone find this helpful.

    Disclaimer: below code was not tested with all Publisher(s) and not safe as for some real-world project. It is just approach demo.

    So, as there are many kind of publishers and all of them are final and private and, moreover there might be come via type-eraser, we needed generic thing applying to any publisher, thus operator

    extension Publisher {
        public func countingSubscribers(_ callback: ((Int) -> Void)? = nil)
            -> Publishers.SubscribersCounter {
                return Publishers.SubscribersCounter(upstream: self, callback: callback)
        }
    }
    

    Operator gives us possibility to inject in any place of of publishers chain and provide interesting value via callback. Interesting value in our case will be count of subscribers.

    As operator is injected in both Upstream & Downstream we need bidirectional custom pipe implementation, ie. custom publisher, custom subscriber, custom subscription. In our case they must be transparent, as we don't need to modify streams... actually it will be Combine-proxy.

    Posible usage:
    1) when SubscribersCounter publisher is last in chain, the numberOfSubscribers property can be used directly

    let publisher = NotificationCenter.default
        .publisher(for: UIApplication.didBecomeActiveNotification)
        .countingSubscribers()
    ...
    publisher.numberOfSubscribers
    

    2) when it somewhere in the middle of the chain, then receive callback about changed subscribers count

    let publisher = URLSession.shared
            .dataTaskPublisher(for: URL(string: "https://www.google.com")!)
            .countingSubscribers({ count in print("Observers: \(count)") })
            .receive(on: DispatchQueue.main)
            .map { _ in "Data received" }
            .replaceError(with: "An error occurred")
    

    Here is implementation:

    import Combine
    
    extension Publishers {
    
        public class SubscribersCounter : Publisher where Upstream : Publisher {
    
            private(set) var numberOfSubscribers = 0
    
            public typealias Output = Upstream.Output
            public typealias Failure = Upstream.Failure
    
            public let upstream: Upstream
            public let callback: ((Int) -> Void)?
    
            public init(upstream: Upstream, callback: ((Int) -> Void)?) {
                self.upstream = upstream
                self.callback = callback
            }
    
            public func receive(subscriber: S) where S : Subscriber,
                Upstream.Failure == S.Failure, Upstream.Output == S.Input {
                    self.increase()
                    upstream.receive(subscriber: SubscribersCounterSubscriber(counter: self, subscriber: subscriber))
            }
    
            fileprivate func increase() {
                numberOfSubscribers += 1
                self.callback?(numberOfSubscribers)
            }
    
            fileprivate func decrease() {
                numberOfSubscribers -= 1
                self.callback?(numberOfSubscribers)
            }
    
            // own subscriber is needed to intercept upstream/downstream events
            private class SubscribersCounterSubscriber : Subscriber where S: Subscriber {
                let counter: SubscribersCounter
                let subscriber: S
    
                init (counter: SubscribersCounter, subscriber: S) {
                    self.counter = counter
                    self.subscriber = subscriber
                }
    
                deinit {
                    Swift.print(">> Subscriber deinit")
                }
    
                func receive(subscription: Subscription) {
                    subscriber.receive(subscription: SubscribersCounterSubscription(counter: counter, subscription: subscription))
                }
    
                func receive(_ input: S.Input) -> Subscribers.Demand {
                    return subscriber.receive(input)
                }
    
                func receive(completion: Subscribers.Completion) {
                    subscriber.receive(completion: completion)
                }
    
                typealias Input = S.Input
                typealias Failure = S.Failure
            }
    
            // own subcription is needed to handle cancel and decrease
            private class SubscribersCounterSubscription: Subscription where Upstream: Publisher {
                let counter: SubscribersCounter
                let wrapped: Subscription
    
                private var cancelled = false
                init(counter: SubscribersCounter, subscription: Subscription) {
                    self.counter = counter
                    self.wrapped = subscription
                }
    
                deinit {
                    Swift.print(">> Subscription deinit")
                    if !cancelled {
                        counter.decrease()
                    }
                }
    
                func request(_ demand: Subscribers.Demand) {
                    wrapped.request(demand)
                }
    
                func cancel() {
                    wrapped.cancel()
                    if !cancelled {
                        cancelled = true
                        counter.decrease()
                    }
                }
            }
        }
    }
    

提交回复
热议问题