Is it possible to use viewStore.binding to present sheet?

I am trying to show a modal sheet. I can do this with a @State property, but I need to set the state for this boolean in my reducer. Have anyone done something like this with success?

.sheet(isPresented: viewStore.binding(
    get: { $0.showEditIntro },
    send: ProfileAction.editIntro
)) {
    Text("Show this text when showEditIntro == true")
}

I use a button to toggle the value for the variable, and I can see that it gets updated, but the sheet does not appear. Could not find any example on presenting sheet in TCA repo.

Could you provide some more code?
I have a working example, but there is no real difference between it and the binding on navigation:

.sheet(isPresented: self.viewStore.binding(
    get: { $0.isDateViewPresented },
    send:  NewEntryAction.setDateView(isPresented:))
) { ... }

Action:

enum NewEntryAction: Equatable {
    case setDateView(isPresented: Bool)
    ...
}

In the reducer:

case let .setDateView(isPresented: isPresented):
    state.isDateViewPresented = isPresented
    return .none

The state:

public struct NewEntryState: Equatable {
    var isDateViewPresented: Bool = false
    ...
}

Hope that helps :slight_smile:

1 Like

Yeah this is definitely possible, and we have two case studies that do just this:

And as @moebius says, if you share some of your action/reducer code it might be easier to see what is going wrong :slightly_smiling_face:

1 Like

Thank you very much! It was all my fault - had a sloppy implementation of:

static func == (lhs: ProfileState, rhs: ProfileState) -> Bool 

When learning something new, it always helps to know that it should be possible! TCA looks great - keep up the good work!

1 Like

Is there any example of doing this with the .sheet(item: <Binding>) variant? You explaind this generally with SwiftUI in the episode: Episode #109: Composable SwiftUI Bindings: The Point

We don't define a helper sheet method in TCA right now, but you have a couple options:

  1. Avoid the extra state but still use sheet(isPresented:) by deriving the view store binding in terms of nil state:

    .sheet(
      isPresented: viewStore.binding(
        get: { $0.optionalCounter != nil },
        send: LoadThenPresentAction.setSheet(isPresented:)
      )
    ) {
      IfLetStore(...)
    }
    
  2. Use sheet(item:) with a view store binding but ignore the argument passed to the sheet's destination block:

    enum LoadThenPresentAction {
      ...
      case setSheet(item: CounterState?)
    }
    
    .sheet(
      item: viewStore.binding(
        get: \.optionalCounter,
        send: LoadThenPresentAction.setSheet(item:)
      )
    ) { _ in
      IfLetStore(...)
    }
    

Many times it is useful to separate presentation state from the optionality of the state itself, though, for example if you have some global app state that can be presented and you don't want that state's optionality to drive presentation.

1 Like
       .sheet(
          isPresented: viewStore.binding(
            get: { $0.isSheetPresented },
            send: Campaigns.ViewAction.setSheet(isPresented:)
          )
        ) {
          if viewStore.isNewCampaign {
            IfLetStore(
              self.store.scope(
                state: { $0.selection }, action: CampaignsAction.campaign),
              then: NewCampaign.init(store:),
              else: ActivityIndicator()
            )
          } else {
            Text("Show Campaign")
          }
        }

[...]

    case .setSelection(isPresented: true):
      guard state.selection != nil else { return .init(value: .newCampaign) }
      state.isSelectionPresented = true
      return .none

    case .setSelection(isPresented: false):
      state.isNewCampaign = false
      state.isSelectionPresented = false
      return .none

I don't understand how to the prevent the binding from sending a second .setSelection(isPresented: false) after I send .setSelection(isPresented: false) myself.

1 Like