Search code examples
swiftrx-swiftcombine

Swift Combine: Check if Subject has observer?


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?


Solution

  • 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)
        }
    }