List of pages with different state

I am building a feature that uses has some state and a set of different pages it will render in a PageViewer which allows users to swipe between pages:

struct AppState {
  var activity: Activity
  var pages: [Page]
  var currentPage: Int

  var pager: PagerState {
        get { .init(pages: self.pages.count, currentPage: self.currentPage) }
        set { (self.pages, self.currentPage) = (self.pages, newValue.currentPage) }
    }
}

enum AppAction {
  case activity(ActivityAction)
  case pager(PagerAction)
  case page(PageAction)
}

struct PagerState {
  var pages: Int
  var currentPage: Int
}

enum PagerAction {
  case changePage(idx: Int)
}

The Page type is an enum because there are multiple types of pages. This also means each page has different action types and its own reducer

enum Page {
  case chartPage(ChartPage)
  case mapPage(MapPage)
}

enum PageAction {
  case chart(ChartPageAction)
  case map(MapPageAction)

Now, the problem I have is that each page also needs access to the activity state in their store, as they display data calculated from it. For example, the ChartPage looks like this:

struct ChartPage {
  var activity: Activity
}

I then render the pages as such... this could be improved a bit but it works for now:

struct AppViewView: View {
    let store: Store<RecorderState, RecorderAction>
    
    var pagerStore: Store<PagerState, PagerAction> {
        store.scope(state: \.pager, action: { RecorderAction.pager($0) })
    }
        
    var body: some View {
        Pager(store: pagerStore) {
            ForEachStore(
                store.scope(state: \.pages, action: RecorderAction.page),
                content: PageView.init(store:))
         }
    }
}

struct PageView: View {
    let store: Store<Page, PageAction>
    
    var body: some View {
        IfLetStore(store.scope(state: \.dataPage, action:PageAction.data),
                   then: { store in ChartPageView(store: store) }
        )
        IfLetStore(store.scope(state: \.mapPage, action: PageAction.map),
                   then: { store in MapPageView(store: store) }
        )
    }
}

The issue comes when I'm actually building my store. I need each page to have the activity scoped from the outer state, but I'm not sure how to do that. This is in my SceneDelegate:

let activity = Activity()
let store = Store(
            initialState: AppState(
                activity: activity,
                pages: [
                    Page.dataPage(DataPage(activity: activity)),
                    Page.mapPage(MapPage(activity: Activity)),
                ],
                currentPage: 0
            ),
            reducer: appReducer,
            environment: AppEnvironment()
        )

As you can imagine, the activity is a value type and therefore not shared between the AppState, DataPage, and MapPage. The pages have to be constructed here, and ideally they would use store.scope to get some smaller set of state than the whole activity even. I can't quite figure out how to do this.

It gets even more complicated when I want to allow users to add and remove pages dynamically from the array. Am I missing something fundamental here? Any suggestions how to do this better?

Thanks all

I suppose that this issue is possibly happening because I'm mixing two types of state: The PageConfig which indicates what the pages will be and the settings for each page, and the Page which is created at runtime from the PageConfig as the actual pages. If I separated them this way then AppState.pages would initially be empty while AppState.pageConfig would have an array of PageConfig. Then I would create the AppState.pages in onAppear.

Does that make more sense, or is it making this more complicated than it needs to be?

Terms of Service

Privacy Policy

Cookie Policy