Passing a Store with .sheet

Hello everyone,

I am trying to understand Composable Architecture. For that, I am working on a little application that shows Apple's Frameworks in LazyVGrid, on tap gesture it shows the full description of one specific Framework.

So far I have managed to present modally a framework. Right now I am facing another issue.

I will try to put as little code as possible, but so it can still make sense

The structure of my app is:
AppState AppAction AppEnvironment

struct AppState: Equatable {
   var frameworks: [Framework] = MockData.frameworks
   var selectedFramework: Framework?
}

Then FrameworkAction, FrameworkEnvironment for each Framework

Here is my app Reducer

let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
   frameworkReducer.forEach(
      state:            \AppState.frameworks,
      action:           /AppAction.framework(index:frameworkAction:),
      environment:      { _ in FrameworkEnvironment() }
   ),
   Reducer { state, action, environment in
      switch action {
      case .framework(index: let index, frameworkAction: FrameworkAction.didTapFramework):
         state.selectedFramework = state.frameworks[index]
         return .none
         
      case .framework(index: let index, frameworkAction: let frameworkAction):
         return .none
         
      case .selectedFramework(let framework):
         state.selectedFramework = framework
         return .none
      }
   }
)

And here is the Content:

struct ContentView: View {
   let store: Store<AppState, AppAction>
   let columns = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
   var body: some View {
      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: AppAction.selectedFramework(viewStore.selectedFramework)
            ), content: { FrameworkDetailView(store: <#Store<Framework, FrameworkAction>#>, framework: $0) })
         }
      }
   }
}

In order to perform some FrameworkAction, in FrameworkDetailView, like dismissing that View or firing off the button that takes you to Apple Website through Safari, I need to pass Store<Framework, FrameworkAction> to FrameworkDetailView but I have no idea how. Not sure how to scope it.
Or maybe I should take a completely different approach to it?
Any help will be very appreciated!

Here is my repository if anyone has time to have a look:

As I understand the SmallView it's some kind of a preview that can open the details by a tap on it. I have the same case in my app and I handled it by adding a sheet directly to each preview (SmallView in your case) after that you can add a boolean variable to the Framework state to present a sheet. So it will look something like that:

enum FrameworkAction {
  //other actions
   case dismissDetails
}

struct Framework: Identifiable, Equatable {
   //other vars
   var showDetails: Bool = false
}

struct SmallView: View {
   let store: Store<Framework, FrameworkAction>
   var body: some View {
      ZStack {
         WithViewStore(self.store) { viewStore in
            VStack {
              .....
            }
            .padding()
            .onTapGesture {
               framework.send(.didTapFramework)
            }
            .sheet(isPresented: viewStore.binding(get: \.showDetails, send: .dismissDetails)) { 
                      FrameworkDetailView(store: store) 
              }
         }
      }
   }
}
1 Like

You can use an IfLetStore view to transform a store that holds onto optional state into one that holds onto honest state. We have some examples of this (here's one) in our isowords application. We will also have a lot more to say about this on Point-Free soon :slightly_smiling_face:

1 Like

Hey Bredox, thanks for your code, I will definitely have a look. What I really wanted tho is to make that whole operation happen with the item parameter not isPresented as I did not want to add that extra property to my Framework struct.

Hey Brandon, thanks for the solution, it all works beautifully. I will post it here, if you have any comments on what could be done better please tell me..

AppState and AppAction

struct AppState: Equatable {
   var frameworks: [Framework] = MockData.frameworks
   var selectedFramework: Framework?
}

enum AppAction: Equatable {
   case framework(index: Int, frameworkAction: FrameworkAction)
   case selectedFramework(Framework?)
   case dismissFrameworkDetailView
   case frameworkDetailView(FrameworkAction)
}

Piece of the appReducer taking part in .sheet( method

case .selectedFramework(let framework):
         state.selectedFramework = framework
         return .none
         
      case .dismissFrameworkDetailView:
         state.selectedFramework = nil
         return .none
         
      case .frameworkDetailView:
         return .none

Content of .sheet

.sheet(
               item: viewStore.binding(
                  get: \.selectedFramework,
                  send: .dismissFrameworkDetailView
               )) { _ in 
               IfLetStore(
                  self.store.scope(
                     state: \.selectedFramework,
                     action: AppAction.frameworkDetailView
                  ),
                  then: FrameworkDetailView.init(store:)
               )
            }
Terms of Service

Privacy Policy

Cookie Policy