0

I'm wanting to utilise a architectural design that enables me to clearly designate input and outputs in my view model (How To Feed ViewModels) but am curious as to how I can best integrate the "working" part of the view model into this structure.

I have tended to use Actions (perhaps not very elegantly) to bind UI elements to the work they need to perform. The problem of course is that some of these Actions rely on view model properties so I can't create them in init() in the same was as Input and Outputs as the properties are not yet initialised. It's possible to work around this by defining them as private lazy vars and then exposing them via a struct that essentially presents a public interface to Action. It doesn't seem to flow very well though and I'm learning that if you're expending a lot of effort to get structure to bend to your will, it's probably a code smell. Code example below - suggestions welcome :-)

protocol PatientListViewModelType: ViewModelType { }

final class PatientListViewModel: PatientListViewModelType {
    // MARK:- Protocol conformance
    typealias Dependencies =  HasPatientService

    struct Input {
        let patient: AnyObserver<Patient>
    }
    struct Output {
        let sectionedPatients: Observable<[PatientSection]>
        let patient: Observable<Patient>
    }
    let input: Input
    let output: Output

    struct Actions {
        let deletePatient: Action<Patient, Void>
        let togglePatient: (Patient) -> CocoaAction
        let updatePatient: (Patient) -> Action<String, Void>
    }

    lazy var action: Actions = Actions(deletePatient: self.deletePatient,
                                       togglePatient: self.togglePatient,
                                       updatePatient: self.updatePatient)

    // MARK: Setup
    private let dependencies: Dependencies
    private let patientSubject = ReplaySubject<Patient>.create(bufferSize: 1)

    // MARK:- Init
    init(dependencies: Dependencies) {
        self.dependencies = dependencies

        let sectionedPatients =
            dependencies.patientService.patients()
                .map { results -> [PatientSection] in
                    let scheduledPatients = results
                        .filter("checked == nil")
                        .sorted(byKeyPath: "created", ascending: false)

                    let admittedPatients = results
                        .filter("checked != nil")
                        .sorted(byKeyPath: "checked", ascending: false)

                    return [
                        PatientSection(model: "Scheduled Patients", items: scheduledPatients.toArray()),
                        PatientSection(model: "Admitted Patients", items: admittedPatients.toArray())
                    ]
        }

        self.output = Output(sectionedPatients: sectionedPatients,
                             patient: patientSubject.asObservable() )
        // this is immediately overriden during binding to VC - it just allows us to exit the init without errors
        self.input = Input(patient: patientSubject.asObserver())


    }

    // MARK:- Actions        
    private lazy var deletePatient: Action<Patient, Void> = { (service: PatientServiceType) in
        return Action { patient in
            return service.delete(realmObject: patient)
        }
    }(self.dependencies.patientService)

    lazy var togglePatient: (Patient) -> CocoaAction = { [unowned self] (patient: Patient) -> CocoaAction in
        return CocoaAction {
            return self.dependencies.patientService.toggle(patient: patient).map { _ in }
        }
    }

    private lazy var updatePatient: (Patient) -> Action<String, Void> = { [unowned self] (patient: Patient) in
        return Action { newName in
            return self.dependencies.patientService.update(patient: patient, name: newName).map { _ in }
        }
    }
}
rustproofFish
  • 931
  • 10
  • 32

1 Answers1

0

The answer is actually simple enough once I got a chance to sit down and have a play. I've put Actions into the Output struct (as the seemed to be the most logical place) rather than creating a dedicated interface as before. The next question of course is whether Actions are the best fit for the problem but I'll deal with that later...

final class PatientListViewModel: PatientListViewModelType {
    // MARK:- Protocol conformance
    typealias Dependencies =  HasPatientService

    struct Input {
        let patient: AnyObserver<Patient>
    }
    let input: Input

    struct Output {
        let sectionedPatients: Observable<[PatientSection]>
        let patient: Observable<Patient>
        let deletePatient: Action<Patient, Void>
        let togglePatient: (Patient) -> CocoaAction
        let updatePatient: (Patient) -> Action<String, Void>
    }
    let output: Output

    // MARK: Setup
    private let dependencies: Dependencies
    private let patientSubject = ReplaySubject<Patient>.create(bufferSize: 1)

    // MARK:- Init
    init(dependencies: Dependencies) {
        self.dependencies = dependencies

        let sectionedPatients =
            dependencies.patientService.patients()
                .map { results -> [PatientSection] in
                    let scheduledPatients = results
                        .filter("checked == nil")
                        .sorted(byKeyPath: "created", ascending: false)

                    let admittedPatients = results
                        .filter("checked != nil")
                        .sorted(byKeyPath: "checked", ascending: false)

                    return [
                        PatientSection(model: "Scheduled Patients", items: scheduledPatients.toArray()),
                        PatientSection(model: "Admitted Patients", items: admittedPatients.toArray())
                    ]
        }

        let deletePatient: Action<Patient, Void> = { patientService in
            return Action { patient in
                return patientService.delete(realmObject: patient)
            }
        }(dependencies.patientService)

        let togglePatient: (Patient) -> CocoaAction = { patient in
            return CocoaAction {
                return dependencies.patientService.toggle(patient: patient)
                    .map { _ in }
            }
        }

        let updatePatient: (Patient) -> Action<String, Void> = { patient in
            return Action { newName in
                return dependencies.patientService.update(patient: patient, name: newName)
                    .map { _ in }
            }
        }

        // this is immediately overriden during binding to VC - it just allows us to exit the init without errors
        self.input = Input(patient: patientSubject.asObserver())

        self.output = Output(sectionedPatients: sectionedPatients,
                             patient: patientSubject.asObservable(),
                             deletePatient: deletePatient,
                             togglePatient: togglePatient,
                             updatePatient: updatePatient)
    }
rustproofFish
  • 931
  • 10
  • 32