Swift Combine: Check if Subject has observer?

北慕城南 提交于 2020-01-24 23:10:48

问题


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?


回答1:


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<Self> {
            return Publishers.SubscribersCounter<Self>(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<Upstream> : 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<S>(subscriber: S) where S : Subscriber,
            Upstream.Failure == S.Failure, Upstream.Output == S.Input {
                self.increase()
                upstream.receive(subscriber: SubscribersCounterSubscriber<S>(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<S> : Subscriber where S: Subscriber {
            let counter: SubscribersCounter<Upstream>
            let subscriber: S

            init (counter: SubscribersCounter<Upstream>, subscriber: S) {
                self.counter = counter
                self.subscriber = subscriber
            }

            deinit {
                Swift.print(">> Subscriber deinit")
            }

            func receive(subscription: Subscription) {
                subscriber.receive(subscription: SubscribersCounterSubscription<Upstream>(counter: counter, subscription: subscription))
            }

            func receive(_ input: S.Input) -> Subscribers.Demand {
                return subscriber.receive(input)
            }

            func receive(completion: Subscribers.Completion<S.Failure>) {
                subscriber.receive(completion: completion)
            }

            typealias Input = S.Input
            typealias Failure = S.Failure
        }

        // own subcription is needed to handle cancel and decrease
        private class SubscribersCounterSubscription<Upstream>: Subscription where Upstream: Publisher {
            let counter: SubscribersCounter<Upstream>
            let wrapped: Subscription

            private var cancelled = false
            init(counter: SubscribersCounter<Upstream>, 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()
                }
            }
        }
    }
}



回答2:


Some time after posting my question I wrote this simple extension. Much simpler than @Asperi's solution. Not sure about disadvantages/advantages between the two solutions besides simplicity (of mine).


private enum CounterChange: Int, Equatable {
    case increased = 1
    case decreased = -1
}

extension Publisher {
    func trackNumberOfSubscribers(
        _ notifyChange: @escaping (Int) -> Void
    ) -> AnyPublisher<Output, Failure> {

        var counter = NSNumber.init(value: 0)
        let nsLock = NSLock()

        func updateCounter(_ change: CounterChange, notify: (Int) -> Void) {
            nsLock.lock()
            counter = NSNumber(value: counter.intValue + change.rawValue)
            notify(counter.intValue)
            nsLock.unlock()
        }

        return handleEvents(
            receiveSubscription: { _ in updateCounter(.increased, notify: notifyChange) },
            receiveCompletion: { _ in updateCounter(.decreased, notify: notifyChange) },
            receiveCancel: { updateCounter(.decreased, notify: notifyChange) }
        ).eraseToAnyPublisher()
    }
}

Here are some tests:

import XCTest
import Combine

final class PublisherTrackNumberOfSubscribersTest: TestCase {

    func test_four_subscribers_complete_by_finish() {
        doTest { publisher in
            publisher.send(completion: .finished)
        }
    }

    func test_four_subscribers_complete_by_error() {
        doTest { publisher in
            publisher.send(completion: .failure(.init()))
        }
    }

}

private extension PublisherTrackNumberOfSubscribersTest {
    struct EmptyError: Swift.Error {}
    func doTest(_ line: UInt = #line, complete: (PassthroughSubject<Int, EmptyError>) -> Void) {
        let publisher = PassthroughSubject<Int, EmptyError>()

        var numberOfSubscriptions = [Int]()
        let trackable = publisher.trackNumberOfSubscribers { counter in
            numberOfSubscriptions.append(counter)
        }

        func subscribe() -> Cancellable {
            return trackable.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
        }

        let cancellable1 = subscribe()
        let cancellable2 = subscribe()
        let cancellable3 = subscribe()
        let cancellable4 = subscribe()

        XCTAssertNotNil(cancellable1, line: line)
        XCTAssertNotNil(cancellable2, line: line)
        XCTAssertNotNil(cancellable3, line: line)
        XCTAssertNotNil(cancellable4, line: line)

        cancellable1.cancel()
        cancellable2.cancel()

        complete(publisher)
        XCTAssertEqual(numberOfSubscriptions, [1, 2, 3, 4, 3, 2, 1, 0], line: line)
    }
}



来源:https://stackoverflow.com/questions/58364345/swift-combine-check-if-subject-has-observer

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!