Search code examples
iosswiftrx-swiftcombine

Combine assign publisher to PassthroughSubject


I have following RxSwift view model code:

private(set) var num = BehaviorRelay<Int>(value: 1)
private let indexTrigger = PublishRelay<Int>()
private let disposeBag = DisposeBag()

private func setupBindings() {
   //...
   self.num.distinctUntilChanged().bind(to: self.indexTrigger).disposed(by: self.disposeBag)
}

func numSelected(num: Int) {
   self.num.accept(num)
}

This code is working fine and does what I want. I'm trying to do same, but with swift Combine framework with following code:

@Published private(set) var num: Int = 1
private let indexTrigger = PassthroughSubject<Int, Never>()
private var subscriptions = Set<AnyCancellable>()

private func setupBindings() {
   //...
   self.$num.removeDuplicates().sink(receiveValue: { [weak self] num in
      self.indexTrigger.send(num)
   }).store(in: &self.subscriptions)
}

func numSelected(num: Int) {
   self.num = num
}

So RxSwift binding looks much clean and simple and without need of weak. I tried to check assign(on:) method in combine, but seems it is not the one. Is there way to do same thing?


Solution

  • It looks like you're trying to "chain" publishers. The way to chain publishers is not to have two separate publishers with the second one poked by a .sink and a .send; it is to use .flatMap in the pipeline.

    In your comment, you said that what you really want to do in the second publisher is "trigger request". That sounds like networking. So in your .flatMap you would provide a publisher that does the networking.

    Here is a toy example, just to show you the form:

    import UIKit
    import Combine
    
    func delay(_ delay:Double, closure:@escaping ()->()) {
        let when = DispatchTime.now() + delay
        DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
    }
    
    class ViewController: UIViewController {
    
        @Published var num: Int = 1
        var storage = Set<AnyCancellable>()
        
        func setupPipeline() {
            $num.removeDuplicates()
                .flatMap { self.makeNetworkingFuture($0) }
                .sink { print($0) }
                .store(in: &self.storage)
        }
        
        func makeNetworkingFuture(_ i: Int) -> AnyPublisher<Int,Never> {
            URLSession.shared.dataTaskPublisher(for: URL(string:"https://www.example.com")!)
                .map{_ in i}
                .replaceError(with: -1000)
                .eraseToAnyPublisher()
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            self.setupPipeline()
            delay(1) { self.num = 1 }
            delay(2) { self.num = 1 }
            delay(3) { self.num = 2 }
            delay(4) { self.num = 2 }
            delay(5) { self.num = 3 }
        }
    }
    

    That's a very silly example, in the sense that although we do some networking, we don't show the result of that networking. I'm just outputting an Int (the same Int that came down the pipeline from num). But when you run the example, what you will see in the output is 1, then 2, then 3, proving that we are removing duplicates, and networking is actually taking place on each of those occasions, as you can demonstrate to yourself in other ways.