How to keep the laziness of `Deferred` when combining it with another publisher?

Example:

struct TestState {
    var isEnabled = false
}

enum TestError: Swift.Error {
    case notEnabled
}

class TestController: ObservableObject {
    @Published var state: TestState = .init()
    @Published var next: Result<Int, TestError> = .failure(.notEnabled)
}

What I want to achieve:

  • If TestController.state.isEnabled is false, the publisher TestController.$next will always emit a Result.failure(TestError.notEnabled) value to its subscribers.
  • If TestController.state.isEnabled is true, the publisher TestController.$next will emit a randomized Int value wrapped in Result.success type to its subscribers.

What I did:

  • An Int publisher that wrapped in a Deferred to take advantage of its laziness so that each subscription generate a new value that is different from the previous one:

    let randomizer = Deferred<Just<Int>> {
        .init(.random(in: 0..<10))
    }
    
    let r1 = randomizer.sink { print("1st:", $0) }
    let r2 = randomizer.sink { print("2nd:", $0) }
    
    // running result
    // 1st: 8
    // 2nd: 6
    
  • Subscribe the TestController.$state in TestController.init to swap the publishing source of TestController.$next

    class TestController: ObservableObject {
        @Published var state: TestState = .init()
        @Published var next: Result<Int, TestError> = .failure(.notEnabled)
        private var cancellables: Set<AnyCancellable> = []
    
        init<P: Publisher>(
            randomizer: Deferred<P>
        ) where P.Output == Int, P.Failure == Never {
            self.$state
                .map(\.isEnabled)
                .removeDuplicates()
    ->          .combineLatest(randomizer)
                .sink { [weak self] isEnabled, value in
                    self.map {
                        $0.next = isEnabled ? .success(value) : .failure(.notEnabled)
                    }
                }
                .store(in: &self.cancellables)
        }
    }
    
  • With this implementation, the TestController.$next always publishes a same value when TestController.state.isEnabled is true, which I assume expected since there is only one subscription to the randomizer:

    let controller = TestController(randomizer: randomizer)
    let cancellable = controller.$next
        .sink { value in
            print("Result:", value)
        }
    
    
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        controller.state.isEnabled = true
    }
    
    DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
        controller.state.isEnabled = false
    }
    
    DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
        controller.state.isEnabled = true
    }
    
    // running result
    // Result: failure(__lldb_expr_121.TestError.notEnabled)
    // Result: success(6)
    // Result: failure(__lldb_expr_121.TestError.notEnabled)
    // Result: success(6)
    

Other Attempt Made:

  • I have made another implementation:

    class TestController: ObservableObject {
        @Published var state: TestState = .init()
        @Published var next: Result<Int, TestError> = .failure(.notEnabled)
        private var cancellables: Set<AnyCancellable> = []
    
        init<P: Publisher>(
            randomizer: Deferred<P>
        ) where P.Output == Int, P.Failure == Never {
            self.$state
                .map(\.isEnabled)
                .removeDuplicates()
                .sink { [weak self] isEnabled in
                    guard isEnabled else {
                        self?.next = .failure(.notEnabled)
                        return
                    }
    
                    randomizer.sink { value in
                        self?.next = .success(value)
                    }
    
                }
                .store(in: &self.cancellables)
        }
    }
    
  • it does give me the result I want:

    // running result
    // Result: failure(__lldb_expr_121.TestError.notEnabled)
    // Result: success(2)
    // Result: failure(__lldb_expr_121.TestError.notEnabled)
    // Result: success(8)
    
  • But the nested .sink looks ugly.

    So I am wondering, is there an operator or another way of implementation that provides the combination and keep the laziness of the Deferred at the same time?

The real life scenario

As you may have suspected, the randomizer of type Deferred<Just<Int>> is not what I am dealing with in real life, rather, I am given an API looks like this:

func load() -> Future<Int, Never>

To simplify the question, I used the randomizer to describe the problem.

Please help, thanks!