AppAction executes twice with IfLetStore when dismissing a View with the Button

Hello everyone. Please forgive me if something is not clear, I am a very early junior level. I will gladly clarify. Thanks for the help.

I've got this view with the IfLetStore:

WithViewStore(self.store) { viewStore in
         ScrollView {
            HStack {
               LazyVGrid(columns: columns, content: {
                  ForEachStore(
                     self.store.scope(
                        state: \.frameworks,
                        action: AppAction.framework(index:frameworkAction:)
                     ),
                     content: SmallView.init(store:)
                  )
               })
            }
            .sheet(
               item: viewStore.binding(
                  get: \.selectedFramework,
                  send: .frameworkDetailView(.didCloseFramework)
               )) { _ in 
               IfLetStore(
                  self.store.scope(
                     state: \.selectedFramework,
                     action: AppAction.frameworkDetailView
                  ),
                  then: FrameworkDetailView.init(store:)
               )
            }
         }
      }

Hopefully, as you can see, the presentation of FrameworkDetailView relies on AppState's property selectedFramework. All here is correct and works (unless you have some annotations).

The trick is to dismiss that FrameworkDetailView properly. When dismissing by sliding it down that fires off:

case .frameworkDetailView(.didCloseFramework):
         state.selectedFramework = nil
         return .none

But the FrameworkDetailView has a CrossButton that also fires off that FrameworkAction that fires off AppAction

struct CrossButton: View {
   let store: Store<Framework, FrameworkAction>
   
    var body: some View {
      WithViewStore(self.store) { viewStore in
         Button {
            viewStore.send(.didCloseFramework)
           } label: {
               Image(systemName: "xmark")
                   .foregroundColor(Color(.label))
                   .imageScale(.large)
                   .frame(width: 44, height: 44, alignment: .center)
        }
      }
    }
}

So when the CrosssButton is tapped, that's what happens:

  1. I tap the CrossButton that is a part of FrameworkDetailView
Button {
           viewStore.send(.didCloseFramework)
          } label: { 
  1. AppAction executes and sets selectedFramework to nil
 case .frameworkDetailView(.didCloseFramework):
         state.selectedFramework = nil
         return .none
  1. .sheet's send parameter fires off
.sheet(
               item: viewStore.binding(
                  get: \.selectedFramework,
                  send: .frameworkDetailView(.didCloseFramework)
               )) { _ in 
  1. AppAction executes and sets selectedFramework to nil. AGAIN
 case .frameworkDetailView(.didCloseFramework):
         state.selectedFramework = nil
         return .none

I am not exactly sure why it happens. I would have never noticed that if testing Composable Architecture wasn't that great and meticulous.

Repository:

That action is being fired twice because you defined it in two places: on button tap and on binding value change for the .sheet modifier.

The way I would go about it is to use a separate action for .sheet so that I would know that user dismissed the sheet by swiping down (e.g. for analytics). If you added, say .sheetWentDown event for the send: parameter you'd see the following log in the console (if you added debugActions() on the top level reducer):

.frameworkDetailView(.didCloseFramework)
.sheetWentDown

When it comes to AppState manipulation I would set selectedFramework to nil in the .sheetWentDown because otherwise you would might get the exception about an action being run on a state (selectedFramework) that is nil.

Hey Eimantas, thanks for getting back to me.

When I create .sheetWentDown where I set selectedFramework to nil, and set that action for send:

item: viewStore.binding(
                  get:  \.selectedFramework,
                  send: .frameworkSheetWentDown
               )) { _ in 

Tapping CrossButton will also fire off .frameworkSheetWentDown. It does not directly calling that action, but I think when case .frameworkDetailView(.didCloseFramework): (fired off by CrossButton) mutates selectedFramework, .sheetWentDown will fire off too

That will still leave me with two actions being fired off where one does nothing. I would like to avoid that. Unless I am doing something wrong right now?

It seems that any configuration that I tried will fire off the same action twice, or two actions where one of those two will do no permutation to the state.

Thank you.

Correct. The action that you send in .sheet modifier will ALWAYS fire. There is no way around it, because you're changing the value that sheet binding relies on showing.

Would the same thing happened without Composable Architecture?

I don't understand the question, because Composable Architecture is the one providing you with runtime to send the actions :) without it you would have to use environment variable (presentationMode AFAIR) in the sheet's view to dismiss the sheet from inside and it would just change the binding's value outside of it. I think ¯\_(ツ)_/¯

Terms of Service

Privacy Policy

Cookie Policy