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.