Skip to content

Commit 4fb9885

Browse files
Optimize navigation (#238)
1 parent 4dd7e6b commit 4fb9885

File tree

10 files changed

+119
-246
lines changed

10 files changed

+119
-246
lines changed

Modules/AppFeature/Sources/AppFeature.swift

Lines changed: 34 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import GameSessionFeature
1313
public enum AppFeature {
1414
public struct State: Equatable {
1515
var path: [Destination]
16+
var isSettingsPresented: Bool
17+
1618
var home: HomeFeature.State
17-
var gameSession: GameSessionFeature.State
19+
var gameSession: GameSessionFeature.State?
1820
var settings: SettingsFeature.State?
1921

2022
public enum Destination: Hashable {
@@ -23,14 +25,16 @@ public enum AppFeature {
2325

2426
public init(
2527
path: [Destination] = [],
28+
isSettingsPresented: Bool = false,
2629
home: HomeFeature.State = .init(),
27-
gameSession: GameSessionFeature.State = .init(),
30+
gameSession: GameSessionFeature.State? = nil,
2831
settings: SettingsFeature.State? = nil
2932
) {
3033
self.path = path
34+
self.isSettingsPresented = isSettingsPresented
3135
self.home = home
32-
self.settings = settings
3336
self.gameSession = gameSession
37+
self.settings = settings
3438
}
3539
}
3640

@@ -50,52 +54,26 @@ public enum AppFeature {
5054
reducerMain,
5155
pullback(
5256
HomeFeature.reducer,
53-
state: { _ in
54-
\.home
55-
},
56-
action: { globalAction in
57-
if case let .home(localAction) = globalAction {
58-
return localAction
59-
}
60-
return nil
61-
},
62-
embedAction: {
63-
.home($0)
64-
}
57+
state: { _ in \.home },
58+
action: { if case let .home(action) = $0 { action } else { nil } },
59+
embedAction: Action.home
6560
),
6661
pullback(
6762
GameSessionFeature.reducer,
68-
state: { _ in
69-
\.gameSession
70-
},
71-
action: { globalAction in
72-
if case let .gameSession(localAction) = globalAction {
73-
return localAction
74-
}
75-
return nil
76-
},
77-
embedAction: {
78-
.gameSession($0)
79-
}
63+
state: { $0.gameSession != nil ? \.gameSession! : nil },
64+
action: { if case let .gameSession(action) = $0 { action } else { nil } },
65+
embedAction: Action.gameSession
8066
),
8167
pullback(
8268
SettingsFeature.reducer,
83-
state: {
84-
$0.settings != nil ? \.settings! : nil
85-
},
86-
action: { globalAction in
87-
if case let .settings(localAction) = globalAction {
88-
return localAction
89-
}
90-
return nil
91-
},
92-
embedAction: {
93-
.settings($0)
94-
}
69+
state: { $0.settings != nil ? \.settings! : nil },
70+
action: { if case let .settings(action) = $0 { action } else { nil } },
71+
embedAction: Action.settings
9572
)
9673
)
9774
}
9875

76+
// swiftlint:disable:next cyclomatic_complexity
9977
private static func reducerMain(
10078
into state: inout State,
10179
action: Action,
@@ -104,12 +82,20 @@ public enum AppFeature {
10482
switch action {
10583
case .setPath(let path):
10684
state.path = path
85+
if path.contains(.gameSession) && state.gameSession == nil {
86+
state.gameSession = .init()
87+
}
88+
if !path.contains(.gameSession) && state.gameSession != nil {
89+
state.gameSession = nil
90+
}
10791
return .none
10892

109-
case .setSettingsPresented(let presented):
110-
if presented {
93+
case .setSettingsPresented(let isPresented):
94+
state.isSettingsPresented = isPresented
95+
if isPresented && state.settings == nil {
11196
state.settings = .init()
112-
} else {
97+
}
98+
if !isPresented && state.settings != nil {
11399
state.settings = nil
114100
}
115101
return .none
@@ -120,12 +106,6 @@ public enum AppFeature {
120106
case .home(.delegate(.play)):
121107
return .run { .setPath([.gameSession]) }
122108

123-
case .home:
124-
return .none
125-
126-
case .settings:
127-
return .none
128-
129109
case .gameSession(.delegate(.settings)):
130110
return .run { .setSettingsPresented(true) }
131111

@@ -134,6 +114,12 @@ public enum AppFeature {
134114

135115
case .gameSession:
136116
return .none
117+
118+
case .home:
119+
return .none
120+
121+
case .settings:
122+
return .none
137123
}
138124
}
139125
}

Modules/AppFeature/Sources/AppView.swift

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ public struct AppView: View {
1515
public typealias ViewStore = Store<AppFeature.State, AppFeature.Action>
1616

1717
@StateObject private var store: ViewStore
18-
@State private var path: [AppFeature.State.Destination] = []
19-
@State private var isSettingsPresented: Bool = false
2018

2119
@Environment(\.theme) private var theme
2220

@@ -27,50 +25,17 @@ public struct AppView: View {
2725
}
2826

2927
public var body: some View {
30-
NavigationStack(path: $path) {
28+
NavigationStack(path: store.binding(\.path, send: { .setPath($0) })) {
3129
HomeView {
32-
store.projection(
33-
state: \.home,
34-
action: { .home($0) }
35-
)
30+
store.projection(state: \.home, action: { .home($0) })
3631
}
3732
.navigationDestination(for: AppFeature.State.Destination.self) {
3833
viewForDestination($0)
3934
}
4035
}
41-
.sheet(isPresented: $isSettingsPresented) {
36+
.sheet(isPresented: store.binding(\.isSettingsPresented, send: { .setSettingsPresented($0) })) {
4237
SettingsView {
43-
store.projection(
44-
state: \.settings,
45-
action: { .settings($0) }
46-
)
47-
}
48-
}
49-
// Fix Error `Update NavigationAuthority bound path tried to update multiple times per frame`
50-
.onReceive(store.$state) { state in
51-
let newPath = state.path
52-
if newPath != path {
53-
path = newPath
54-
}
55-
56-
let newIsSettingsPresented = state.settings != nil
57-
if newIsSettingsPresented != isSettingsPresented {
58-
isSettingsPresented = newIsSettingsPresented
59-
}
60-
}
61-
.onChange(of: path) { _, newPath in
62-
if newPath != store.state.path {
63-
Task {
64-
await store.dispatch(.setPath(newPath))
65-
}
66-
}
67-
}
68-
.onChange(of: isSettingsPresented) { _, newIsSettingsPresented in
69-
let isSettingsPresented = store.state.settings != nil
70-
if newIsSettingsPresented != isSettingsPresented {
71-
Task {
72-
await store.dispatch(.setSettingsPresented(newIsSettingsPresented))
73-
}
38+
store.projection(state: \.settings, action: { .settings($0) })
7439
}
7540
}
7641
.onReceive(store.dispatchedAction) { event in
@@ -83,10 +48,7 @@ public struct AppView: View {
8348
switch destination {
8449
case .gameSession:
8550
GameSessionView {
86-
store.projection(
87-
state: \.gameSession,
88-
action: { .gameSession($0) }
89-
)
51+
store.projection(state: \.gameSession, action: { .gameSession($0) })
9052
}
9153
}
9254
}

Modules/GameSessionFeature/Sources/GameSessionFeature.swift

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,9 @@ public enum GameSessionFeature {
4747
reducerSound,
4848
pullback(
4949
GameFeature.reducer,
50-
state: {
51-
$0.game != nil ? \.game! : nil
52-
},
53-
action: { globalAction in
54-
if case let .game(localAction) = globalAction {
55-
return localAction
56-
}
57-
return nil
58-
},
59-
embedAction: {
60-
.game($0)
61-
}
50+
state: { $0.game != nil ? \.game! : nil },
51+
action: { if case let .game(action) = $0 { action } else { nil } },
52+
embedAction: Action.game
6253
)
6354
)
6455
}
@@ -70,38 +61,26 @@ public enum GameSessionFeature {
7061
) -> Effect<Action> {
7162
switch action {
7263
case .didAppear:
73-
return .run {
74-
.setGame(dependencies.createGame())
75-
}
64+
return .run { .setGame(dependencies.createGame()) }
7665

7766
case .didTapQuit:
78-
return .run {
79-
.delegate(.quit)
80-
}
67+
return .run { .delegate(.quit) }
8168

8269
case .didTapSettings:
83-
return .run {
84-
.delegate(.settings)
85-
}
70+
return .run { .delegate(.settings) }
8671

8772
case .didTapCard(let card):
8873
guard let controlledPlayer = state.controlledPlayer else {
8974
return .none
9075
}
91-
return .run {
92-
.game(.preparePlay(card, player: controlledPlayer))
93-
}
76+
return .run { .game(.preparePlay(card, player: controlledPlayer)) }
9477

9578
case .didChoose(let option, let chooser):
96-
return .run {
97-
.game(.choose(option, player: chooser))
98-
}
79+
return .run { .game(.choose(option, player: chooser)) }
9980

10081
case .setGame(let game):
10182
state.game = game
102-
return .run {
103-
.game(.startTurn(player: game.startOrder[0]))
104-
}
83+
return .run { .game(.startTurn(player: game.startOrder[0])) }
10584

10685
case .game:
10786
return .none

Modules/HomeFeature/Sources/HomeFeature.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,10 @@ public enum HomeFeature {
6161
}
6262

6363
case .didTapPlay:
64-
return .run {
65-
.delegate(.play)
66-
}
64+
return .run { .delegate(.play) }
6765

6866
case .didTapSettings:
69-
return .run {
70-
.delegate(.settings)
71-
}
67+
return .run { .delegate(.settings) }
7268

7369
case .delegate:
7470
return .none
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// Store+Binding.swift
3+
// WildWestOnline
4+
//
5+
// Created by Hugues Stéphano TELOLAHY on 10/12/2025.
6+
//
7+
import SwiftUI
8+
9+
public extension Store {
10+
func binding<Value>(
11+
_ keyPath: KeyPath<State, Value>,
12+
send valueToAction: @escaping (Value) -> Action
13+
) -> Binding<Value> {
14+
.init(
15+
get: { self.state[keyPath: keyPath] },
16+
set: { newValue in
17+
Task {
18+
await self.dispatch(valueToAction(newValue))
19+
}
20+
}
21+
)
22+
}
23+
}

Modules/Redux/Tests/Misc/GlobalFeature.swift

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,15 @@ enum GlobalFeature {
2121
combine(
2222
pullback(
2323
CounterFeature.reducer,
24-
state: { _ in
25-
\.counter
26-
},
27-
action: { globalAction in
28-
if case let .counter(localAction) = globalAction { return localAction }
29-
return nil
30-
},
31-
embedAction: GlobalFeature.Action.counter
24+
state: { _ in \.counter },
25+
action: { if case let .counter(action) = $0 { action } else { nil } },
26+
embedAction: Action.counter
3227
),
3328
pullback(
3429
FlagFeature.reducer,
35-
state: { _ in
36-
\.flag
37-
},
38-
action: { globalAction in
39-
if case let .flag(localAction) = globalAction { return localAction }
40-
return nil
41-
},
42-
embedAction: GlobalFeature.Action.flag
30+
state: { _ in \.flag },
31+
action: { if case let .flag(action) = $0 { action } else { nil } },
32+
embedAction: Action.flag
4333
)
4434
)
4535
}

Modules/SettingsFeature/Sources/Home/SettingsHomeFeature.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,10 @@ public enum SettingsHomeFeature {
101101
}
102102

103103
case .didTapFigures:
104-
return .run {
105-
.delegate(.selectedFigures)
106-
}
104+
return .run { .delegate(.selectedFigures) }
107105

108106
case .didTapCollectibles:
109-
return .run {
110-
.delegate(.selectedCollectibles)
111-
}
107+
return .run { .delegate(.selectedCollectibles) }
112108

113109
case .delegate:
114110
break

0 commit comments

Comments
 (0)