Search code examples
swiftrx-swift

RxSwift Subject.isDisposed maybe not reflecting the actual state?


Consider the following code:

class RxDisposeTest : XCTestCase {
    
    func test_dispose() {
        var scope = DisposeBag()
        var subjects = [PublishSubject<String>]()
        let upstream = PublishSubject<Int>()
        
        upstream
            .flatMapLatest { _ in
                let newSubject = PublishSubject<String>()
                subjects.append(newSubject)
                return newSubject.debug("\(Unmanaged.passUnretained(newSubject).toOpaque())").do(onDispose: { print("onDispose :: ", newSubject.isDisposed) })
            }
            .subscribe()
            .disposed(by: scope)
        
        upstream.onNext(1)
        upstream.onNext(2)
        
        XCTAssertEqual(subjects.dropLast().last?.isDisposed, true) // fails
        XCTAssertEqual(subjects.last?.isDisposed, false)
        
        scope = .init()
        
        XCTAssertEqual(subjects.last?.isDisposed, true) // fails
    }
    
}

Running this test prints out the following:

Test Case '-[CommonTests.RxDisposeTest test_dispose]' started.
2023-06-21 11:36:30.625: 0x0000600002c34f00 -> subscribed
2023-06-21 11:36:30.625: 0x0000600002c34f00 -> isDisposed
onDispose ::  false
2023-06-21 11:36:30.626: 0x0000600002c34580 -> subscribed
RxDisposeTest.swift:257: error: -[CommonTests.RxDisposeTest test_dispose] : XCTAssertEqual failed: ("Optional(false)") is not equal to ("Optional(true)")
2023-06-21 11:36:32.679: 0x0000600002c34580 -> isDisposed
onDispose ::  false
RxDisposeTest.swift:262: error: -[CommonTests.RxDisposeTest test_dispose] : XCTAssertEqual failed: ("Optional(false)") is not equal to ("Optional(true)")

As you can see, checking isDisposed inside the onDispose hook returns false. Also, debug() correctly prints out that the first PublishSubject is disposed as the second one takes over inside flatMapLatest. Is this expected behavior?

The version of RxSwift is 5.1.2.


Solution

  • Yes, this is expected behavior. That particular subscription to the subject has been disposed, but the subject itself is still viable and can accept onNext events.

    Remember, a Subject can have many subscriptions attached to it. Just because a particular subscription is disposed doesn't mean the entire subject is. A Subject is only disposed when its dispose method is called.

    Check out this test:

    func test_dispose() {
        var scope = DisposeBag()
        var subjects = [PublishSubject<String>]()
        let upstream = PublishSubject<Int>()
        
        scope.insert(upstream
            .flatMapLatest { [weak scope] count in
                let newSubject = PublishSubject<String>()
                subjects.append(newSubject)
                scope?.insert(newSubject) // here we are inserting the subject into the dispose bag.
                return newSubject
                    .debug("subscription \(count)")
            }
            .subscribe()
        )
        
        upstream.onNext(1)
        upstream.onNext(2)
    
        XCTAssertEqual(subjects.count, 2)
        XCTAssertFalse(subjects.map(\.isDisposed).allSatisfy { $0 })
        scope = .init()
        XCTAssertTrue(subjects.map(\.isDisposed).allSatisfy { $0 })
    }