Could anyone advise how to implement a navigation stack where screens are referencing each other?
For example something like an App Store that has the following screens & transitions:
A list of apps allows navigating to an app view.
The app view allows navigating to a list of similar apps.
As we have a requirement of UI state to be represented in data, there is a problem there. States referencing each other, as a result, the app state structure has an undefined size.
The situation becomes even more problematic if we would try to modularize the app by moving each screen into its own library and try to isolate screens from each other to enable parallel compilation.
I should pull the project more often, have not seen this one. Indeed this is in line with a problem we have. I still not quite sure the modularization will be solved with this. The higher-order example has a self-referenced recursive state and do not bring any additional states and do not have modularization constraint at all. Meanwhile in the original provide example has two screens referencing each other. I will need to dig more into the example and try to re-purpose it for the scenario provided. Thanks!
If the state and actions need to be nested recursively, I think you should be able to introduce generic "holes" in a feature that embeds a feature from another module:
struct ModuleState<ExternalState> {
...
var externalState: ExternalState
}
enum ModuleAction<ExternalAction> {
...
case externalAction(ExternalAction)
}
struct ModuleEnvironment<ExternalEnvironment> {
...
var externalEnvironment: ExternalEnvironment
}
You'll have to decide on the trade-off between abstracting over features in this way or merely combining things into a single module.
If the state and actions of features are completely independent, you can pull them back to another module that coordinates everything in a parent state, action, environment, and view.
Another way of doing this is just using explicit types for holding navigation state of particular screen:
struct AState {
indirect enum Route {
case b(BState)
// Other possible routes from screen A
}
// ....
var route: Route?
}
struct BState {
indirect enum Route {
case a(AState)
// Other possible routes from screen B
}
// ....
var route: Route?
}
Some great info in this thread about recursive higher-order reducers and modularization!
I'm still struggling to figure out how to properly implement the original ask. I've successfully done recursive navigation (ScreenA to ScreenA) in other parts of the app thanks to the link by @stephencelis. Is the recurse function applicable to the use case of ScreenA that can go to ScreenB that can go to a new ScreenA? If so, I wasn't sure how to implement that. In the meantime, I'm getting a crash on launch of app. Any thoughts?
After taking a look at it and implementing something along those lines, I'm not sure it is what I'm looking for.
If we have case let .screenB(.screenA)): on ScreenA, we're taking care of the actions happening two levels deep. I'm looking to take care of the actions happening just one level in on ScreenB. Normally would be taken care of by combining the reducers but seeing the crash happen on init.
@stephencelis I've gone ahead a put together a sample project to better illustrate what is happening.
When putting together the project, it actually doesn't compile (unlike the project I'm working on) due to a circular reference. This of course makes sense and is the reason it is crashing in the app that compiles.
@stephencelis@nikitaame
I have a question,
I understand, From ScreenA to ScreenA , ScreenA to ScreenB to ScreenB,
How about ScreenA to [ScreenB] to ScreenA?
@yosuke I believe @nikitaame got it working by explicitly declaring the type on screenAReducer and then nesting the screenBReducer inside .recurse { ... } instead of using .combine(...).
So the screenAReducer from the example would look something like this:
let screenAReducer: Reducer<ScreenAState, ScreenAAction, ScreenAEnvironment> =
.recurse { `self`, state, action, environment in
switch action {
case .screenA:
return self.optional().pullback(...)
.run(&state, action, environment)
case .screenB:
return screenBReducer.optional().pullback(...)
.run(&state, action, environment)
...
}
}
Thank you,
This is my code of screenA has multiple screenB,
In my code, ProfileView has multiple TimelineLogCell and, TimelineLogCell has multiple ProfileView.
@yosuke When you invoke the recurse reducer you must describe to the self reducer exactly how to pull back through all of its domain layers before re-invoking the reducer. It's a bit of a mind-trip, but I believe you can change these lines:
To the following:
return self
// 1. Optionalize the profile domain for the timeline log cell
.optional()
// 2. Pull the optional profile back to the timeline log cell domain
.pullback(
state: \TimelineLogCellState.profileState,
action: /TimelineLogCellAction.profileAction,
environment: { _ in ProfileEnvironment() }
)
// 3. Pull each timeline log cell back to the profile domain to
// complete the recursion
.forEach(
state: \ProfileState.timelineLogCellStateList,
action: /ProfileAction.timelineLogCellAction(index:action:),
environment: { _ in TimelineLogCellEnvironment() }
)
.run(&state, action, env)