Note: basic familiarity with TCA is assumed. Extensive tutorials are at https://www.pointfree.co
To get confirmationDialog
View modifier working with TCA 1.0 syntax, the below code compiles and works. It may not be ideal, as the TCA guys have not covered best-practices with confirmationDialog
yet. It requires implementation of PresentationState
and PresentationAction
semantics.
in Feature / Reducer
struct State: Equatable {
@PresentationState var logoutAndDeleteDialog: ConfirmationDialogState<Action>?
...
}
Use a dedicated case to 'namespace' all actions that might be sent from the ConfirmationDialogState<Action>
buttons. (Those actions get automatically embedded in a .presented
case when sent to the parent Reducer from the ConfirmationDialogState view. More on that later.)
enum Action: Equatable {
case logout(PresentationAction<Action>)
case deleteData(Bool)
. . .
}
Create a ConfirmationDialogState
struct to display the confirmation view. Add any desired Action
buttons to the buttons
property. (There may be a better way to do this: I used a property in the reducer.)
let conf: ConfirmationDialogState<Action> = { var conf = ConfirmationDialogState<Action>(title: {TextState("Logout")})
conf.buttons = [ButtonState<Action>(action: .deleteData(true), label: {TextState("Logout and Delete Data")})]
return conf
}()
The ConfirmationDialogState
View sends Actions
to the Reducer
via buttons. (In this instance, a .deleteData Action button is created in the ConfirmationDialogState initializer above.)
That Action
must be caught in Reducer
switch
statement as an an associated value of a .presented case.
Where did the invisible .presented
case come from? It's a result of annotating the ConfirmationDialogState
with @PresentationState
. This automatically embeds all Actions sent by a ConfirmationDialogState
in a .presented
case. The exception is the .dismiss action, which not embedded. (Tricky, and not necessarily intuitive.)
There is likely some overarching logic the TCA guys are trying to enforce in separating Parent Actions from Child Actions. With confirmationDialogs, this logical separation is handled by embedding ConfirmationDialogState
actions in a .presented
case, making explicit that any actions coming into the reducer are from a Child-type view.
reduce(into state: inout State, action: Action) -> ComposableArchitecture.Effect<Action>
switch action {
//note that `.deleteData` Action has been wrapped first in `.presented`, then in `.logout`. `.presented` because the dialog is marked with a PresentationState property wrapper, and `.logout` because in the View below the `Store` is scoped to embed actions local to the dialog into the .logout Action case, found in the parent Reducer. This is all a little mind-bending. At least it was for me.
case let .logout(.presented(.deleteData(bool))):
//handle data deletion
//unexpected behavior if I did not include .dismiss in switch statement and nil out confirmationDialog state manually
case .logout(.dismiss):
state.logoutAndDeleteDialog = nil
...
}
in View with Store
n.b. the dialog state keypath name must be precded by $, //e.g. .$logoutAndDeleteDialog
This is due to the @PresentationState property wrapper being added to the ConfirmationDialogState-type property. So the wrapper, and not the value, is being accessed by the architecture in the scope
method.
Text(vs.userName).confirmationDialog(store: store.scope(state: \.$logoutAndDeleteDialog, action: {.logout($0)}))