2
\$\begingroup\$

I'm finding it difficult to decide on the best architectural pattern for a SwiftUI app but, for the moment, I'm sticking with MVVM as I found this a good fit conceptually.

I'm trying to avoid the SwiftUI layer knowing too much about the business logic and navigation structure but am struggling to decide how best to achieve this.

Here is the prototype view model (without implementation for the navigation):

@dynamicMemberLookup
final class PatientListViewModel: ViewModelType {
    struct State {
        var patients: [PatientSummaryViewModel]
    }
    @Published var state: State = State(patients: []) {
        willSet { print("Received: \(newValue)") }
    }
    
    enum Input {
        case onNewPatient
    }
    
    private let service: PatientServiceType
    private var cancellables = Set<AnyCancellable>()
    
    init(service: PatientServiceType) {
        self.service = service
        
        service.fetchAll()
            .receive(on: DispatchQueue.main)
            .map { patients in
                patients.map {
                    PatientSummaryViewModel(patient: $0)
                }
            }
            .sink { [weak self] patientViewModel in
                self?.state.patients = patientViewModel
            }
            .store(in: &cancellables)
    }
    
    func action(_ input: Input) {
        switch input {
        case .onNewPatient:
            print("Create new view and associated viewModel here but how does SwiftUI access it?")
        }
    }
    
    subscript<Value>(dynamicMember keyPath: KeyPath<State, Value>) -> Value {
        state[keyPath: keyPath]
    }
}

Associated SwiftUI view:

struct PatientListView: View {
    @ObservedObject var viewModel: PatientListViewModel
    
    var body: some View {
        NavigationView {
            List(viewModel.patients) { viewModel in
                
                NavigationLink(
                    destination:
                        PatientSummaryView(viewModel: viewModel),
                    label: {
                        PatientCell(patient: viewModel.patient)
                    })
                
            }
            .navigationBarItems(leading: EditButton(),
                                trailing: Button(action: {
                                    // call to viewmodel to get new view and correctly constructed viewModel
                                },
                                                 label: { Image(systemName: "plus")
                                                    .font(.title2)
                                                 }))
            .navigationBarTitle("Patients")
        }
    }
}

At this point in time, there are several solutions that present themselves:

  1. Modify action(_ input:) to create the View in situ, returning it to the View that called it. Not all actions will need navigation to take place and so I'd need to add @discardableresult with many actions having to return EmptyView() and then there's the little dance I'd need to do with type erasing the views to consider

  2. Modify the view model State internal struct to include a "next view" property that is updated by action(_:) when new views are required. Although the idea of keeping state in one place appeals this feels a little clunky somehow

  3. Finally I have some vague ideas about a callback/closure that is passed to the view model and used to construct the required view but I'm struggling to visualise this as I still find closure syntax confusing, doubly so when working with Views

Ideas, pointers and criticism welcome! ;-)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.