In RxSwift it's pretty easy to bind a Driver
or an Observable
in a View Model
to some observer in a ViewController
(i.e. a UILabel
).
I usually prefer to build a pipeline, with observables created from other observables, instead of "imperatively" pushing values, say via a PublishSubject
).
Let's use this example: update a UILabel
after fetching some data from the network
RxSwift + RxCocoa example
final class RxViewModel {
private var dataObservable: Observable<Data>
let stringDriver: Driver<String>
init() {
let request = URLRequest(url: URL(string:"https://www.google.com")!)
self.dataObservable = URLSession.shared
.rx.data(request: request).asObservable()
self.stringDriver = dataObservable
.asDriver(onErrorJustReturn: Data())
.map { _ in return "Network data received!" }
}
}
final class RxViewController: UIViewController {
private let disposeBag = DisposeBag()
let rxViewModel = RxViewModel()
@IBOutlet weak var rxLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
rxViewModel.stringDriver.drive(rxLabel.rx.text).disposed(by: disposeBag)
}
}
Combine + UIKit example
In a UIKit-based project it seems like you can keep the same pattern:
- view model exposes publishers
- view controller binds its UI elements to those publishers
final class CombineViewModel: ObservableObject {
private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
var stringPublisher: AnyPublisher<String, Never>
init() {
self.dataPublisher = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.eraseToAnyPublisher()
self.stringPublisher = dataPublisher
.map { (_, _) in return "Network data received!" }
.replaceError(with: "Oh no, error!")
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
final class CombineViewController: UIViewController {
private var cancellableBag = Set<AnyCancellable>()
let combineViewModel = CombineViewModel()
@IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
combineViewModel.stringPublisher
.flatMap { Just($0) }
.assign(to: \.text, on: self.label)
.store(in: &cancellableBag)
}
}
What about SwiftUI?
SwiftUI relies on property wrappers like @Published
and protocols like ObservableObject
, ObservedObject
to automagically take care of bindings (As of Xcode 11b7).
Since (AFAIK) property wrappers cannot be "created on the fly", there's no way you can re-create the example above using to the same pattern. The following does not compile
final class WrongViewModel: ObservableObject {
private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
@Published var stringValue: String
init() {
self.dataPublisher = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.eraseToAnyPublisher()
self.stringValue = dataPublisher.map { ... }. ??? <--- WRONG!
}
}
The closest I could come up with is subscribing in your view model (UGH!) and imperatively update your property, which does not feel right and reactive at all.
final class SwiftUIViewModel: ObservableObject {
private var cancellableBag = Set<AnyCancellable>()
private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
@Published var stringValue: String = ""
init() {
self.dataPublisher = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.eraseToAnyPublisher()
dataPublisher
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {_ in }) { (_, _) in
self.stringValue = "Network data received!"
}.store(in: &cancellableBag)
}
}
struct ContentView: View {
@ObservedObject var viewModel = SwiftUIViewModel()
var body: some View {
Text(viewModel.stringValue)
}
}
Is the "old way of doing bindings" to be forgotten and replaced, in this new UIViewController-less world?
After posting previous answer read this article: https://nalexn.github.io/swiftui-observableobject/
and decide to do same way. Use @State and don't use @Published
General ViewModel protocol:
ViewModel class:
SwiftUI View:
An elegant way I found is to replace the error on the publisher with
Never
and to then useassign
(assign
only works ifFailure == Never
).In your case...
I ended up with some compromise. Using
@Published
in viewModel but subscribing in SwiftUI View. Something like this:and SwiftUI:
I think the missing piece here is that you are forgetting that your SwiftUI code is functional. In the MVVM paradigm, we split the functional part into the view model and keep the side effects in the view controller. With SwiftUI, the side effects are pushed even higher into the UI engine itself.
I haven't messed much with SwiftUI yet so I can't say I understand all the ramifications yet, but unlike UIKit, SwiftUI code doesn't directly manipulate screen objects, instead it creates a structure that will do the manipulation when passed to the UI engine.