Replacing root view in TCA

How would I go about replacing the root view using TCA? The simplest scenario I can think of is to display a Login screen/flow for fresh install and display home after logging in. As well as displaying login flow after logging out. Is simple condition with bools/values from AppState enough?

struct AppView: View {
  var body: some View {
    if viewStore.loggedIn {
      HomeView()
    } else {
      LoginView()
  }
}
1 Like

I do not see any issue with the solution you're suggesting, and that's how I usually do it as well.

There’s nothing wrong with this approach, but I’d suggest an enum might be a better way to model distinct states, even two, because it allows you to use those enum states to own the root state for each “mode”, for example:

enum AppState {
  case loggedOut(LoggedOutState)
  case loggedIn(LoggedInState)
}

Now, TCA doesn’t yet have great support for scoping on enum state because it is based around key paths but you can work around this by adding computed optional properties to your enum for each nested state for now.

With this in place your app scene just needs to be a WithViewStore containing two IfLetStore views, each one scoped to the logged in and logged out state respectively. Your logged out and logged in views get their own scoped stores which lets you treat the two states as distinct modules.

You could model this with two optional properties on a struct (your view would be the same as described above), with or without a Boolean property, but the enum approach means you have a compile time guarantee that you can only be in one of those states, not both or neither.

2 Likes

@lukeredpath care to expand on your suggestion with some pseudo code of this enum optional property modeling? I think it's really interesting. Do you suggest using CasePaths for this?

So the basic idea is that you model some parts of your state as enums - this inherently makes the state embedded within each enum case optional at the level above because your state can only be in one of those cases at a time. As a simple example of an app that can be in either a logged in or logged out state, with no state independent of this (i.e. all state is nested below one of these states), you could make your root state an enum:

enum AppState {
  case loggedIn(LoggedInState)
  case loggedOut(LoggedOutState)
}

enum AppAction {
  case loggedIn(LoggedInAction)
  case loggedOut(LoggedOutAction)
}

Alternatively, you may have some parts of your app state that are global concerns regardless of the logged in state, so you might make this session state a child of your AppState:

struct AppState {
  var sessionState: SessionState
}

enum SessionState {
  case loggedIn(LoggedInState)
  case loggedOut(LoggedOutState)
}

It really depends on what you need.

To actually make use of this state easily, you really need the Path/OptionalPath abstraction in the optional-paths branch of TCA which allows you to pullback your state using either a WritableKeyPath or a CasePath - this change allows you to both use enums as state and structs as action if you wanted.

Without this, you need a bit of boilerplate in order to get access to your nested enum states using a key path:

enum AppState {
  // ...

  var loggedInState: LoggedInState {
    get { extract(case: AppState.loggedIn, from: self) }
    set { if let value = newValue { self = .loggedIn(value) } }
  }
}

With this in place you can now pull back your reducers along a case path:

let loggedInReducer = Reducer<LoggedInState, LoggedInAction, LoggedInEnvironment> {
  // etc.
  return .none
}

let appReducer = Reducer< AppState, AppAction, AppEnvironment>.combine(
  loggedInReducer.pullback(
    state: /AppState.loggedIn,
    action: /AppAction.loggedIn
  )
)

Finally you can put this all together in your root app view using IfLetStore to switch between the logged in and logged out view depending on the current enum state:

struct AppView: View {
  let store: Store<AppState, AppAction>

  var body: some View {
    IfLetStore(
      store.scope(
          state: /AppState.loggedIn,
          action: AppAction.loggedIn
      ),
      then: LoggedInView.init(store:)
    )
    IfLetStore(
      store.scope(
          state: /AppState.loggedOut,
          action: AppAction.loggedOut
      ),
      then: LoggedOutView.init(store:)
    )
  }
}

// for example:
struct LoggedInView: View {
  let store: Store<LoggedInState, LoggedInAction>

  var body: some View { }
}

Note that I was wrong in my previous post - you don't need a WithViewStore in the root view in this instance because the view isn't actually using any state from the root store, its just using it to provide scoped stores to the IfLetStore views.

If you'd like to see an exploration of a SwitchStore view that is a more enum-oriented take on IfLetStore, take a look at this discussion.

Hope that helps.

As an aside - I haven't found a good way to animate between two mutually exclusively views using two separate IfLetStore views (or even a single IfLetStore using then: and else: views) - if anyone has any tips for this please let me know.

1 Like