'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 |