Group Group Group Group Group Group Group Group Group

Basing Actions on an Observable

#1

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…)

#2

I can’t help with the RxSwift, but I can help with the code formatting. :slight_smile:

Don’t use blockquote or preformatted text for code. Instead, embed it between two rows of 3 backticks:

```
    // put all your code here, between the two rows of ```
```

It will do a nice job with any block of code.

final class ObservationsViewModel: ObservationViewModelType {
  // MARK:- Protocol conformance
  typealias Dependencies = HasSceneCoordinator & HasPatientService
  struct Input {
    // var patient: AnyObserver
  }
  struct Output {
    let name: Driver
    let created: Driver
    let checked: Driver
  }
  struct Actions {
    let addObservation: (Observable) -> Action<String, Void>
  }
// ...
#3

Thanks! That will certainly make things easier for people to interpret. I’ll see if I can edit the post…