TabView - selected index

Is it possible to bind to the selection of a tab in TabView? I have tried to do it like this, but the index is always == 1 in the reducer.

struct MainViewState : Equatable {
    var currentTab = 1

    var meUser:UserDetails
    var profile: ProfileState

    public init(meUser: UserDetails) {
        self.meUser = meUser
        self.profile = ProfileState(user: meUser)
    }
}

enum MainViewAction: Equatable {
    case profile(ProfileAction)
    case selectedTab(Int)
}

struct MainViewEnvironment {
    public var mainQueue: AnySchedulerOf<DispatchQueue>
}

struct MainView: View {

    let store:Store<MainViewState, MainViewAction>

    var body: some View {
        WithViewStore(self.store) { viewStore in
            TabView(selection: viewStore.binding(
                get: \.currentTab,
                send: MainViewAction.selectedTab)) {

                    ContentView()
                    .tabItem {
                        Image("user")
                    }

                    Text("Lots of users")
                    .tabItem {
                        Image("users")
                    }

                    Text("Settings for the application")
                    .tabItem {
                        Image(systemName: "3.square.fill")
                        Text("Settings")
                    }
                }
        }
    }
}

let mainViewReducer: Reducer<MainViewState, MainViewAction, MainViewEnvironment> =
    Reducer { state, action, environment in
        switch action {

        case let .selectedTab(index):
            state.currentTab = index
            return .none

        default:
            return .none

        }
}

I‘m not sure but the .tag(...) on the .tabItem(...) is missing and could cause this problem.

Yes, that seems to be part of the problem. If I add .tag(1), .tag(2)... I get the correct index to the reducer, but the content is still missing. The text "Lots of users" does not display.

Hi @aarsland

Would a working example help? Can you try this to see if it sheds some light?

The tag was indeed missing in your example, but I'm not sure why the content is not showing for you after adding them.

Here goes:

In the SceneDelegate.swift of a clean Xcode Project:

let contentView = ContentView(
  store: Store(
    initialState: ContentState(),
    reducer: contentReducer,
    environment: ContentEnvironment()
  )
)

Then replace the ContentView.swift with:

import SwiftUI
import ComposableArchitecture

enum Tab {
  case tab1
  case tab2
  case tab3
}

struct ContentState: Equatable {
  var selectedTab: Tab = .tab1
}

indirect enum ContentAction: Equatable {
  case tabSelected(Tab)
}

struct ContentEnvironment {}

let contentReducer: Reducer<ContentState, ContentAction, ContentEnvironment> = Reducer.combine(
  Reducer { state, action, _ in
    switch action {
    case let .tabSelected(tab):
      state.selectedTab = tab
      return .none
    }
  }
)

struct ContentView: View {
  let store: Store<ContentState, ContentAction>
  
  var body: some View {
    WithViewStore(self.store) { viewStore in
      TabView(
        selection: viewStore.binding(
          get: { $0.selectedTab },
          send: ContentAction.tabSelected
        )
      ) {
        NavigationView {
          Text("Tab Content 1")
        }.tabItem {
          Text("Tab 1")
        }.tag(Tab.tab1)
        
        NavigationView {
          Text("Tab Content 2")
        }.tabItem {
          Text("Tab 2")
        }.tag(Tab.tab2)
        
        NavigationView {
          Text("Tab Content 3")
        }.tabItem {
          Text("Tab 3")
        }.tag(Tab.tab3)
      }
    }
  }
}

When I added .tag to each view in the TabView I was able to click around on each tab and see their respective views:

ContentView()
  .tabItem {
    Image("user")
}
.tag(1) // <---

Text("Lots of users")
  .tabItem {
    Image("users")
}
.tag(2) // <---

Text("Settings for the application")
  .tabItem {
    Image(systemName: "3.square.fill")
    Text("Settings")
}
.tag(3) // <---

Did this not work for you?

Thanks for the help. It seems like I have done something else wrong. The component is nested under a login page, with a pullback on the reducer

IfLetStore(self.store.scope(state: \.loggedInApplication, action: RootAction.loggedInApplication)) { store in
  MainView(store: store)
}

If I change it to be standalone, it works as soon I added the tags:

IfLetStore(self.store.scope(state: \.loggedInApplication, action: RootAction.loggedInApplication)) { store in
    MainView(store: Store(
      initialState: MainViewState(meUser: viewStore.meUser!), 
      reducer: mainViewReducer, 
      environment: MainViewEnvironment(mainQueue: DispatchQueue.main.eraseToAnyScheduler()))
    )
}

I think that the composable architecture framework looks really nice, but I have difficulties to understand how to distribute shared state in a clean way. For example how to share a logged in user object among several sub pages.

@aarsland have you seen the TicTacToe Example? It's quite wonderful, and may help you start with the initial composition, with some parent (Global State) component that manages other child components, and then:

You could evolve that example with a computed property that gets some state from your parent component, like this:

struct GlobalState {
    var some = ""
    // See here
    var localState: LocalState {
        get {
            LocalState(some: some)
        }
        set {
            some = newValue.some
        }
    }
}

enum GlobalAction {
    case localView(LocalAction)
}

Then when presenting the local view, any changes made to that localStore will be propagated back to the Global Store, retaining those changes in the Global State.

And vice-versa.. Changes happening in the Global State will propagate to the Local, scoped state.

LocalView(
    store: self.store.scope(
        state: { $0.localState },
        action: {.localView($0) }
    )
)

Thank you for your answer! Yes, I have seen that example and it looks ok, but what if the navigation is more complex - like:

If there is 3 screens:
A - A login page where the user object is set.
B - A page under A that shows a list of some sumaries of something - this page do not need the user that is set in A.
C - A page that shows some detailes for an element from the list in page B. This page needs the user object.

Should then screen B also have the user data to pass it on to C?

Terms of Service

Privacy Policy

Cookie Policy