How to trigger action from alert callback created in ReducerProtocol

Hi, I'm facing a tricky but somehow common problem. I have a Reducer implementing ReducerProtocol. At one point, one action will display a custom alert (is just a View with a presentation animation) to show a message alongside with some buttons. Pressing those buttons should trigger other actions.

My problem is that I'm creating the state to display my custom alert inside the Reducer, like this:

struct State: Equatable {
    var customAlertState: CustomAlertState?
}

var body: some ReducerProtocol<State, Action> {
    Reduce { state, action in
    swith action {
    // Some other actions here

    case .showError:
      return .task {
        let state = CustomAlertState(title: String, action: {
          // How to call .doSomething action from here?
        }
        return .showCustomAlert(state)
      }

    case .showCustomAlert(let customAlertState):
        // This triggers the alert display on the view
        state.customAlertState = customAlertState
        return .none
    }

    case .doSomething:
      // This action should be called from the CustomAlert
      return .none
    }
}

There is no problem displaying the alert, but I'm not sure how to comunicate user interaction with the alert back to the reducer so can react with the proper action.
Also, not sure if this is the proper approach using TCA or is a better way to do it. Any help is welcome.

Thanks.

Hey! I'd suggest you to use AlertState. Please take a look at this blog post from Point Free, might be useful: The Composable Architecture and SwiftUI Alerts

Hi @otondin I checked AlertState but since I'm not using a default alert, I think it won't work for me.

I see. You can implement a CustomAlertReducer, with your alert feature own state and actions, and scope on your parent reducer using the Scope ReduceProtocol type.

That's a good idea. I'll follow that path. Thanks @otondin

1 Like

Unfortunatelly this approach raises the same problem, how to comunicate the alert block with the reducer action, doesn't matter if is a parent or child reducer. Problem is the same :frowning:

I see, but have you already tried to send an action from the alert action callback like:

{ viewStore.send(.alertButtonTapped) }

That way you would send an action from the view to the store.

Is that possible since i’m building the callback block inside the reducer? Sorry if sounds like a silly question, i’m still in my first TCA steps.

yeap, that way you can return an Effect from the alert action callback, instead of sending an action from the viewStore to the store, once you're already within the reducer system.

Can you write a small example code about how to do it?

Sure! But please put your alert implementation, that way I'd be able to send you some better-fit example.

Don’t have the source right now with me but basically is what I posted on my initial question. On Reducer scope I compose a () -> Void block which is later injected in a view with a Button as its action.

So maybe you need to pass a substore to your "custom alert":

struct State: ReducerProtocol {
  var customAlertState: CustomAlertState?

  enum Action: Equatable {
    case showCustomAlert
    case customAlert(CustomAlertState.Action)
  }
}

struct CustomAlertState: ReducerProtocol {
  enum Action: Equatable {
    case doSomething
    case dismiss
  }
  ...
}

struct MyView: View {
  let store: StoreOf<State>

  var body: some View {
    ZStack {
      IfLetStore(self.store.scope(state: \.customAlertState, action: { .customAlert($0) })) { store in
        MyAlertView(store: store)
      }
      MainView()
    }
  }
}

struct MyAlertView: View {
  let store: StoreOf<CustomAlertState>

  var body: some View {
    WithViewStore(self.store, observe: { $0 }) { viewStore in
      HStack {
        Button("OK") {
          viewStore.send(.doSomething)
        }
        Button("Cancel") {
          viewStore.send(.dismiss)
        }
      }
    }
  }
}

var body: some ReducerProtocol<State, Action> {
    Reduce { state, action in
    switch action {
    case .showError:
      state.customAlertState = CustomAlertState(title: ...)
      return .none
    case .customAlert(.dismiss):
      state.customAlertState = nil
      return .none
    case .customAlert(.doSomething):
      // This action should be called from the CustomAlert
      return .none
    }
}

If you prefer to avoid doing the substore you can also pass the data to the alert view and do this in the view instead of the IfLetStore:

if let customAlertState = viewStore.customAlertState {
  MyAlertView(state: customAlertState,
    onDoSomething: { viewStore.send(.doSomething) },
    onDismiss: { viewStore.send(.dismiss) }
  )
}
1 Like

Hi @victor I think that won't work in my case. I'll extend my question with more code.

First I have this type as my CustomAlertView's state

public struct CustomAlertState: Equatable {
    
    let title: String
    let subtitle: String
    let actions: [CustomAlertAction]    

    public init(
        title: String,
        subtitle: String,
        actions: [CustomAlertAction]
    ) {
        self.title = title
        self.subtitle = subtitle
        self.actions = actions
    }

Notice the CustomAlertAction type, who has all info needed by the CustomAlertView's buttons

public struct CustomAlertAction {

    let title: String
    var action: () -> Void
    
    public init(
        title: String,
        action: @escaping () -> Void
    ) {
        self.title = title
        self.action = action
    }

}

And in the CustomAlertView I have several CustomAlertButton like this



struct CustomAlertButton: View {
    
    let action: CustomAlertAction
    
    var body: some View {
        Button {
            action.action() // Naming could be better
        } label: {
            // This is the UI look
        }
    }
}

In my CustomAlertView these buttons are created using a simple loop calling CustomAlertButton(action: action)

Notice that all these views and states are disconnected from TCA at all, and there is no reference to any reducer, store or binding. I'm doing this way because CustomAlertView should work within a pure SwiftUI context too.

Now, when trying to create my array of CustomAlertAction in my reducer, that's when all my problems appear. Here's a more accurate version of my code:

struct State: Equatable {
    var customAlertState: CustomAlertState?
}

var body: some ReducerProtocol<State, Action> {
    Reduce { state, action in
    swith action {
    // Some other actions here

    case .showError:
      return .task {
        let action = CustomAlertAction(title: "Action 1", action: { 
           // ACTION BLOCK
           // How to call .doSomething action from here?
           // Can I access the store from here?
        })
        let state = CustomAlertState(title: String, actions: [action])
        return .showCustomAlert(state)
      }

    case .showCustomAlert(let customAlertState):
        // This triggers the alert display on the view
        state.customAlertState = customAlertState
        return .none
    }

    case .doSomething:
      // This action should be called from the CustomAlert
      return .none
    }
}

As you can see, since the action block is a () -> Void block, can't do any return inside the ACTION BLOCK. Obviously, I could create a TCA aware CustomAlertAction, something like this

public struct CustomAlertAction<Action> {

    let title: String
    var action: () -> Action
    
    public init(
        title: String,
        action: @escaping () -> Action
    ) {
        self.title = title
        self.action = action
    }
}

But then I'll have to create TCA aware versions for CustomAlertState and CustomAlertView, duplicating all my UI code in order to keep compatibility with pure SwiftUI.

Not sure if what I want is achievable or if I need to go full TCA with my CustomAlertView.
I think all my problems come because I'm setting the CustomAlertState within the Reducer, I'm I correct?

I hope this give you all the context needed.

Thanks.

Ok, I think I figured out my solution. In order to keep my CustomAlertView independent from TCA I created a bridge state type in my reducer instead of using CustomAlertState directly. Then, using that new state type I conform my CustomAlertState INSIDE the view who presents the CustomAlertView, where I have access to the ViewStore and then can create my CustomAlertAction calling the viewStore send within the block.

Thanks to all for your time and ideas, you put me in the right path.

1 Like

Cool! You can also share your solution with us, if you are willing to.

Sure, it is something like this

struct State: Equatable {
   var bridgeState: BridgeAlertState? 
   // This is a struct with the data needed to create the CustomAlertState on the view
}

var body: some ReducerProtocol<State, Action> {
    Reduce { state, action in
    switch action {
    // Some other actions here

    case .showError:
        let state = BridgeAlertState(text: "Some title", moreText: "Some subtitle")
        return Effect(value: .showCustomAlert(state))
      
    case .showCustomAlert(let bridgeState):
        // This triggers the alert display on the view
        state.bridgeState = bridgeState
        return .none
    }

    case .doSomething:
      // This action is called from the view presenting my 
      return .none
    }
}

Now on my presenting view:

struct PresentingView: View {
    var body: some View {
        WithViewStore(store) { viewStore in
            VStack {
              // Some UI
            }
            .customMethodAlert(
                with: viewStore,
                state: viewStore.binding(
                    get: \.bridgeState,
                    send: AuthReducer.Action.showCustomAlert
                )
            )
        }
    }
}

fileprivate extension View {
    
    func customMethodAlert(
        with viewStore: ViewStoreOf<MyReducer>,
        state: Binding<BridgeAlertState?>
    ) -> some View {
        modifier(CustomAlertModifier(viewStore: viewStore, state: state))
    }
}

struct CustomAlertModifier: ViewModifier {
    let viewStore: ViewStoreOf<MyReducer>
    let state: Binding<BridgeAlertState?>

    func body(content: Content) -> some View {
        ZStack {
            content
            CustomAlert(state: customAlertState(with: viewStore))
        }
    }
    
    private func customAlertState(with viewStore: ViewStoreOf<AuthReducer>) -> CustomAlertState? {
      // Here I create the CustomAlertState using some internal login
      // and calling viewStore(.send(.doSomething)) if needed.
    }

And that's it.