'Combine equivalent of RxSwift's bind(to:)

I'm an experienced RxSwift user, and had a good working MVVM structure in RxSwift. I'm new to Combine, but I can't for the love of God figure out how to do something similar in Combine. The biggest blocker is a bind(to:) equivalent in Combine. I don't know how to chain the result of 1 variable to another.

Here is what I would do in RxSwift:

protocol UseCase {
  func execute(id: Int) -> Single<CustomClass>
}

class DefaultUseCase: UseCase {
  func execute(id: Int) -> Single<CustomClass> {
    // Do network call and return in Single format
  }
}


class ViewModel {

  struct Input {
    let load = PublishRelay<Void>()
  }

  struct Output {
    let isButtonEnabled: Driver<Bool>
  }

  let disposeBag = DisposeBag()
  let input = Input()
  let output: Output

  init(id: Int, useCase: UseCase = DefaultUseCase()) {
    let isButtonEnabled = BehaviorRelay<Bool>(value: false)

    let action = Action<Void, CustomClass> { id in 
      return useCase.execute(id: id)
    }

    self.output = Output(isButtonEnabled: isButtonEnabled.asDriver())

    input
      .load
      .bind(to: useCase.inputs)
      .disposed(by: disposeBag)

    action
      .elements
      .map { // map CustomClass to Bool }
      .bind(to: isButtonEnabled)
      .disposed(by: disposeBag)
  }
}

The action class is from this framework: https://github.com/RxSwiftCommunity/Action

I cannot figure out how to do something similar in Combine, I've already read some tutorials, but it doesn't make sense to me. It looks like you need what feels like a thousand variables to just put through 1 value to your view/viewController with a viewModel.

I a seeking a piece of code that does exactly the same as the piece of RxSwift code above, but in Combine with some explanation.



Solution 1:[1]

First, let's simplify your view model:

class ViewModel {
    
    struct Input {
        let load = PublishRelay<Void>()
    }
    
    struct Output {
        let isButtonEnabled: Driver<Bool>
    }
    
    let input = Input()
    let output: Output
    
    init(id: Int, useCase: UseCase = DefaultUseCase()) {
        let isButtonEnabled = input.load
            .flatMapLatest { [useCase] in
                useCase.execute(id: id)
                    .map { _ in /* map CustomClass to Bool */ true }
                    .catchAndReturn(false)
            }
            .asDriver(onErrorRecover: { _ in fatalError() })

        self.output = Output(isButtonEnabled: isButtonEnabled)
    }
}

I'm not a fan of your Input struct here but I'm working with it...

Once you do that, it's easy to see how to translate it:

class ViewModel? {
    struct Input {
        let load = PassthroughSubject<Void, Never>()
    }
    struct Output {
        let isButtonEnabled: AnyPublisher<Bool, Never>
    }

    let input = Input()
    let output: Output

    init(id: Int, useCase: UseCase) {
        let isButtonEnabled = input.load
            .map { [useCase] in
                useCase.execute(id: id)
                    .map { _ in /* map CustomClass to Bool */ true }
                    .catch { _ in Just(false) }
            }
            .switchToLatest()
            .eraseToAnyPublisher()
        
        self.output = Output(isButtonEnabled: isButtonEnabled)
    }
}

UPDATE IN RESPONSE TO COMMENTS

Here is how you would go about using the use case response for multiple outputs (This compiles in iOS 13):

class ViewModel? {
    struct Input {
        let load = PassthroughSubject<Void, Never>()
    }
    struct Output {
        let isButtonEnabled: AnyPublisher<Bool, Never>
        let somethingElse: AnyPublisher<String, Never>
    }

    let input = Input()
    let output: Output

    init(id: Int, useCase: UseCase) {
        let result = input.load
            .map { [useCase] in
                useCase.execute(id: id)
                    .catch { _ in Empty() }
            }
            .switchToLatest()
            .share()

        let isButtonEnabled = result
            .map { _ in /* map CustomClass to Bool */ true }
            .eraseToAnyPublisher()

        let somethingElse = result
            .map { _ in /* map CustomClass to String */ "" }
            .eraseToAnyPublisher()

        self.output = Output(
            isButtonEnabled: isButtonEnabled,
            somethingElse: somethingElse
        )
    }
}

Of course, a lot depends on how you want to handle errors. The above swallows them, but you might want to expose them for yet another output.

All of this is turning into a general tutorial rather than answering a question though.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1