I’m using many of the patterns described in the RxSwift book to develop an app and have run into a problem where one of the dependencies for an Action (that would subsequently have it’s input bound to a text field) is based on the latest value of an Observable property in the view model. I’ve taken to structuring my view model on a suggestions from Clean Architecture which I rather like as it makes it explicit which properties are inputs and which are outputs.
The code examples aren’t perfect I know but should give enough context for the problem.
ViewModel
final class ObservationsViewModel: ObservationViewModelType {
// MARK:- Protocol conformance
typealias Dependencies = HasSceneCoordinator & HasPatientService
struct Input {
// var patient: AnyObserver<Patient>
}
struct Output {
let name: Driver<String>
let created: Driver<String>
let checked: Driver<String>
}
struct Actions {
let addObservation: (Observable<Patient>) -> Action<String, Void>
}
// MARK:- Public interface
let input: Input
let output: Output
lazy var action = Actions(addObservation: self.addObservation)
// MARK:- Private properties
private let dependencies: Dependencies
private let patientSubject = ReplaySubject<Patient>.create(bufferSize: 1)
private let patient: BehaviorRelay<Patient>
private let disposeBag = DisposeBag()
// MARK:- Initialiser
init(dependencies: Dependencies) {
self.dependencies = dependencies
let patientName = patientSubject
.flatMap { patient in // have to flatMap otherwise you get Observable<Observable<String>>!
return patient.rx.observe(String.self, "name")
}
.flatMap { $0 == nil ? Observable.empty() : Observable.just($0!) }
.asDriver(onErrorJustReturn: "ERROR")
let patientCreated = patientSubject
.flatMap { patient in
return patient.rx.observe(Date.self, "created")
}
.map { Utilities.createFormattedStringFrom(date: $0)}
.asDriver(onErrorJustReturn: "ERROR")
let patientChecked = patientSubject
.flatMap { patient in
return patient.rx.observe(Date.self, "checked")
}
.map { Utilities.createFormattedStringFrom(date: $0)}
.asDriver(onErrorJustReturn: "ERROR")
self.input = Input() // No inputs from viewController in this instance
self.output = Output(name: patientName, created: patientCreated, checked: patientChecked)
}
#warning("Implement adding an observation - I don't think this Action is correctly definied...")
// MARK:- Actions
private lazy var addObservation: (Observable<Patient>) -> Action<String, Void> = { [unowned self] patient in
return Action { text in
patient.subscribe(onNext: { patient in
return self.dependencies.patientService.addObservation(patient: patient, text: text).map { _ in }
}
.disposed(by: self.disposeBag)
)}(self.patient.asObservable())
}
View controller binding
func bindViewModel() {
viewModel.output.name
.drive(nameLabel.rx.text)
.disposed(by: disposeBag)
viewModel.output.created
.drive(createdLabel.rx.text)
.disposed(by: disposeBag)
viewModel.output.checked
.drive(checkedLabel.rx.text)
.disposed(by: disposeBag)
addObservationButton.rx.tap
.withLatestFrom(observationTextField.rx.text.orEmpty)
.subscribe(viewModel.action.addObservation.inputs)
.disposed(by: disposeBag)
}
The code above doesn’t work (perhaps unsurprisingly!). An easy way would be to dispense with Action entirely but I’d like to stick with it and get my head around this issue.
PS - apologies for the formatting. I can’t get the block indent to work properly! (edit: now fixed thanks to a timely assist…)