I'm attempting to build a test application using the ToDo example merged with the sample code on presenting a modal view but it's not going well...
The problem relates to my attempts to separate out AppState into separate domains. The ToDo items (Patients in my example) form part of the global state but I've tried to reference these in a list-specific domain. I'm unable to figure out the correct syntax to plug into ForEachStore (see the PatientListView file below). The state: parameter is fine (or, at least it appears so at the moment) but I'm struggling with the action: parameter. If anyone can point me in the right direction that would be great!
Full code (excluding the App and ModalView files) as follows:
// MARK:- APP STATE
struct AppState: Equatable {
var patients: [Patient] = []
var modal: ModalState? = nil
var isAddSheetPresented: Bool { self.modal != nil }
}
extension AppState {
var patientList: PatientListState {
get {
PatientListState(patients: self.patients, modal: self.modal)
}
set {
self.patients = newValue.patients
self.modal = newValue.modal
}
}
// TODO: PatientList needs, in essence, to be a non-optional version of ModalCompoundState as it needs the list of Patients and access to modalState so it can mutate it
// when the add button is pressed. There does seem to be an element of duplication here... but roll with it for now and see if it can be refactored
var modalCompound: ModalCompoundState? { // creates a compound state struct when the modal "add patient" view is displayed
get {
modal.map {
.init(patients: self.patients, modal: $0)
}
}
set {
self.patients = newValue?.patients ?? self.patients
self.modal = newValue?.modal
}
}
}
// MARK:- APP ACTION
enum AppAction: Equatable {
case patientList(PatientListAction)
case modal(ModalAction)
}
// MARK:- ENVIRONMENT
struct AppEnvironment {
var uuid: () -> UUID
static let live = AppEnvironment(uuid: UUID.init)
}
// MARK:- APP REDUCER
let appReducer: Reducer<AppState, AppAction, AppEnvironment> =
Reducer.combine(
patientListReducer
.pullback(
state: \.patientList,
action: /AppAction.patientList,
environment: { $0 }),
modalReducer
.optional()
.pullback(
state: \.modalCompound,
action: /AppAction.modal,
environment: { $0}
)
)
// MARK:- PATIENT LIST DOMAIN
struct PatientListState: Equatable {
var patients: [Patient] = []
var modal: ModalState? = nil
}
enum PatientListAction: Equatable {
case patient(index: Int, action: PatientRowAction) // AppAction isolates Patient at a specific index
case addButtonTapped
case addSheetDismissed
}
let patientListReducer: Reducer<PatientListState, PatientListAction, AppEnvironment> = Reducer.combine(
reducer,
patientRowReducer.forEach(
state: \.patients,
action: /PatientListAction.patient(index:action:),
environment: { _ in .live }
)
)
let reducer = Reducer<PatientListState, PatientListAction, AppEnvironment> { state, action, environment in
switch action {
case .patient(_, _):
return .none
case .addButtonTapped:
fatalError("Not implemented")
return .none
case .addSheetDismissed:
fatalError("Not implemented")
return .none
}
}
// MARK:- VIEW
struct PatientListView: View {
let store: Store<AppState, AppAction>
var body: some View {
NavigationView {
WithViewStore(store.scope(
state: \.patientList,
action: AppAction.patientList
)) { viewStore in
List {
ForEachStore(
store.scope(
state: \.patientList.patients,
action: AppAction.patientList(.patient(index:action:)),
content: { PatientRowView(store: $0) }
)
)
Text("-- End of list --")
}
.navigationBarTitle("Patients")
.navigationBarItems(
trailing: Button("Add") {
viewStore.send(.addButtonTapped)
}
)
}
}
}
}
// MARK:- SUBVIEW
enum PatientRowAction: Equatable {
case checkboxTapped
}
let patientRowReducer = Reducer<Patient, PatientRowAction, AppEnvironment> { state, action, environment in
switch action {
case .checkboxTapped:
state.isComplete.toggle()
return .none
}
}
struct PatientRowEnvironment { }
// MARK:- VIEW
struct PatientRowView: View {
let store: Store<Patient, PatientRowAction>
var body: some View {
WithViewStore(store) { patientViewStore in
HStack {
Text(patientViewStore.name)
Spacer()
Button(action: {
patientViewStore.send(.checkboxTapped)
}) {
Image(systemName: patientViewStore.isComplete ? "checkmark.square" : "square")
}
.buttonStyle(PlainButtonStyle())
}
.foregroundColor(patientViewStore.isComplete ? .gray : nil)
}
}
}