Patterns for Shared State with Destination reducer/enum? #2651
Unanswered
matt-dewitt
asked this question in
Q&A
Replies: 1 comment
-
I've recently faced a similar issue and came up with the following: import ComposableArchitecture
import SwiftUI
struct RecursiveSharedState: Reducer {
struct State: Equatable {
var count: Int {
didSet { self.bind() }
}
var child: Child.State?
init(count: Int = 0, child: Child.State? = nil) {
defer { self.bind() }
self.count = count
self.child = child
}
private mutating func bind() {
let count = self.count
self.child?.count = count
}
}
enum Action: Equatable {
case child(Child.Action)
case startButtonTapped
case updateButtonTapped
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .startButtonTapped:
state.child = Child.State(count: state.count)
return .none
case .updateButtonTapped:
state.count = Int.random(in: 0..<10_000)
return .none
case .child:
return .none
}
}
.ifLet(\.child, action: /Action.child) {
Child()
}
.onChange(of: \.child?.count) { _, count in
if let count {
Reduce { state, _ in
state.count = count
return .none
}
}
}
}
}
struct Child: Reducer {
struct State: Equatable {
var count: Int {
didSet { self.bind() }
}
@PresentationState var route: Route.State?
init(count: Int, route: Route.State? = nil) {
self.count = count
self.route = route
}
private mutating func bind() {
try? (/Route.State.child).modify(&self.route) { [count] in
$0.count = count
}
}
}
enum Action: Equatable {
case nextButtonTapped
case route(PresentationAction<Route.Action>)
case updateButtonTapped
}
struct Route: Reducer {
enum State: Equatable {
case child(Child.State)
}
indirect enum Action: Equatable {
case child(Child.Action)
}
var body: some ReducerOf<Self> {
Scope(state: /State.child, action: /Action.child) {
Child()
}
}
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .nextButtonTapped:
state.route = .child(Child.State(count: state.count))
return .none
case .updateButtonTapped:
state.count = Int.random(in: 0..<10_000)
return .none
case .route:
return .none
}
}
.ifLet(\.$route, action: /Action.route) {
Route()
}
.onChange(of: { (/Route.State.child).extract(from: $0.route)?.count }) { _, count in
if let count {
Reduce { state, _ in
state.count = count
return .none
}
}
}
}
}
struct RecursiveSharedStateView: View {
let store: StoreOf<RecursiveSharedState>
var body: some View {
NavigationStack {
IfLetStore(self.store.scope(state: \.child, action: { .child($0) })) { store in
ChildView(store: store)
} else: {
Button("Start") { store.send(.startButtonTapped) }
}
}
.overlay(alignment: .bottom) {
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack {
Text("Root Count")
Text("\(viewStore.count)")
Button("Update") { viewStore.send(.updateButtonTapped) }
}
.padding()
.background(Color.white)
.transition(.opacity.animation(.default))
.clipped()
.shadow(color: .black.opacity(0.2), radius: 5, y: 5)
}
}
}
}
struct ChildView: View {
let store: StoreOf<Child>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack {
Text("Child Count")
Text("\(viewStore.count)")
Button("Next") { viewStore.send(.nextButtonTapped) }
Button("Update") { viewStore.send(.updateButtonTapped) }
}
}
.navigationDestination(
store: self.store.scope(state: \.$route, action: { .route($0) }),
state: /Child.Route.State.child,
action: Child.Route.Action.child
) { store in
ChildView(store: store)
}
}
}
#Preview {
RecursiveSharedStateView(
store: Store(initialState: RecursiveSharedState.State()) {
RecursiveSharedState()
._printChanges()
}
)
} This is pretty much an extended version of the shared state case study with navigation in mind. Dealing with shared state this way, one is probably well prepared with whats coming in the future. |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Are there any patterns or established best practices for handling shared state with child features presented through a destination reducer? I've got a pretty big app the makes use of a lot of shared state and we are starting on the path to update to the newer PresentationState/Action for navigating to destinations and I'm trying to figure out the best way to handle this with the new paradigm.
I've got a demo app attached where there's a parent feature that starts a timer and the hope is that when I present a child sheet, it continues to get updated as the timer ticks. This is a pretty basic example, but imagine features where there are multiple destinations and multiple pieces of shared state. What is the best practice for keeping these things in sync, especially when a feature that is presenting a child is itself a child and receiving shared state from further up? Thanks in advance!
For shared state, we currently use this pattern:
And with existing pullbacks and scoping this works fine with sheets and destinations, but I'm not seeing a way to make this play nicely with the new navigation APIs and the Destinations reducer.
Beta Was this translation helpful? Give feedback.
All reactions