From 4748cd984b11afd99909a7cfac191dd8d9d83d6e Mon Sep 17 00:00:00 2001 From: Stephano Telolahy Date: Wed, 10 Dec 2025 21:42:10 +0100 Subject: [PATCH 1/5] wip --- Modules/AppFeature/Sources/AppFeature.swift | 29 +++++++++++++----- Modules/AppFeature/Sources/AppView.swift | 33 ++------------------- Modules/Redux/Sources/Store+Binding.swift | 23 ++++++++++++++ 3 files changed, 46 insertions(+), 39 deletions(-) create mode 100644 Modules/Redux/Sources/Store+Binding.swift diff --git a/Modules/AppFeature/Sources/AppFeature.swift b/Modules/AppFeature/Sources/AppFeature.swift index 6dc6a76e7..a784b49c5 100644 --- a/Modules/AppFeature/Sources/AppFeature.swift +++ b/Modules/AppFeature/Sources/AppFeature.swift @@ -13,8 +13,9 @@ import GameSessionFeature public enum AppFeature { public struct State: Equatable { var path: [Destination] + var isSettingsPresented: Bool var home: HomeFeature.State - var gameSession: GameSessionFeature.State + var gameSession: GameSessionFeature.State? var settings: SettingsFeature.State? public enum Destination: Hashable { @@ -23,14 +24,16 @@ public enum AppFeature { public init( path: [Destination] = [], + isSettingsPresented: Bool = false, home: HomeFeature.State = .init(), - gameSession: GameSessionFeature.State = .init(), + gameSession: GameSessionFeature.State? = nil, settings: SettingsFeature.State? = nil ) { self.path = path + self.isSettingsPresented = isSettingsPresented self.home = home - self.settings = settings self.gameSession = gameSession + self.settings = settings } } @@ -65,8 +68,8 @@ public enum AppFeature { ), pullback( GameSessionFeature.reducer, - state: { _ in - \.gameSession + state: { + $0.gameSession != nil ? \.gameSession! : nil }, action: { globalAction in if case let .gameSession(localAction) = globalAction { @@ -96,6 +99,7 @@ public enum AppFeature { ) } + // swiftlint:disable:next cyclomatic_complexity private static func reducerMain( into state: inout State, action: Action, @@ -104,14 +108,23 @@ public enum AppFeature { switch action { case .setPath(let path): state.path = path + if state.path.contains(.gameSession) && state.gameSession == nil { + state.gameSession = .init() + } + if !state.path.contains(.gameSession) && state.gameSession != nil { + state.gameSession = nil + } return .none - case .setSettingsPresented(let presented): - if presented { + case .setSettingsPresented(let isPresented): + state.isSettingsPresented = isPresented + if isPresented && state.settings == nil { state.settings = .init() - } else { + } + if !isPresented && state.settings != nil { state.settings = nil } + return .none case .home(.delegate(.settings)): diff --git a/Modules/AppFeature/Sources/AppView.swift b/Modules/AppFeature/Sources/AppView.swift index f8524cfbf..38eec6c05 100644 --- a/Modules/AppFeature/Sources/AppView.swift +++ b/Modules/AppFeature/Sources/AppView.swift @@ -15,8 +15,6 @@ public struct AppView: View { public typealias ViewStore = Store @StateObject private var store: ViewStore - @State private var path: [AppFeature.State.Destination] = [] - @State private var isSettingsPresented: Bool = false @Environment(\.theme) private var theme @@ -27,7 +25,7 @@ public struct AppView: View { } public var body: some View { - NavigationStack(path: $path) { + NavigationStack(path: store.binding(\.path, send: { .setPath($0) })) { HomeView { store.projection( state: \.home, @@ -38,7 +36,7 @@ public struct AppView: View { viewForDestination($0) } } - .sheet(isPresented: $isSettingsPresented) { + .sheet(isPresented: store.binding(\.isSettingsPresented, send: { .setSettingsPresented($0) })) { SettingsView { store.projection( state: \.settings, @@ -46,33 +44,6 @@ public struct AppView: View { ) } } - // Fix Error `Update NavigationAuthority bound path tried to update multiple times per frame` - .onReceive(store.$state) { state in - let newPath = state.path - if newPath != path { - path = newPath - } - - let newIsSettingsPresented = state.settings != nil - if newIsSettingsPresented != isSettingsPresented { - isSettingsPresented = newIsSettingsPresented - } - } - .onChange(of: path) { _, newPath in - if newPath != store.state.path { - Task { - await store.dispatch(.setPath(newPath)) - } - } - } - .onChange(of: isSettingsPresented) { _, newIsSettingsPresented in - let isSettingsPresented = store.state.settings != nil - if newIsSettingsPresented != isSettingsPresented { - Task { - await store.dispatch(.setSettingsPresented(newIsSettingsPresented)) - } - } - } .onReceive(store.dispatchedAction) { event in print(event) } diff --git a/Modules/Redux/Sources/Store+Binding.swift b/Modules/Redux/Sources/Store+Binding.swift new file mode 100644 index 000000000..33ed5df58 --- /dev/null +++ b/Modules/Redux/Sources/Store+Binding.swift @@ -0,0 +1,23 @@ +// +// Store+Binding.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 10/12/2025. +// +import SwiftUI + +public extension Store { + func binding( + _ keyPath: KeyPath, + send valueToAction: @escaping (Value) -> Action + ) -> Binding { + .init( + get: { self.state[keyPath: keyPath] }, + set: { newValue in + Task { + await self.dispatch(valueToAction(newValue)) + } + } + ) + } +} From 928ccb037ea41a1c33cec67ebbe554246c53472b Mon Sep 17 00:00:00 2001 From: Stephano Telolahy Date: Wed, 10 Dec 2025 22:16:39 +0100 Subject: [PATCH 2/5] wip --- Modules/AppFeature/Sources/AppFeature.swift | 18 +++++----- .../Sources/GameSessionFeature.swift | 24 ++++--------- Modules/HomeFeature/Sources/HomeFeature.swift | 8 ++--- .../Sources/Home/SettingsHomeFeature.swift | 8 ++--- .../Sources/SettingsFeature.swift | 36 ++++++++++++------- .../Sources/SettingsView.swift | 18 +--------- 6 files changed, 44 insertions(+), 68 deletions(-) diff --git a/Modules/AppFeature/Sources/AppFeature.swift b/Modules/AppFeature/Sources/AppFeature.swift index a784b49c5..d13a48d2f 100644 --- a/Modules/AppFeature/Sources/AppFeature.swift +++ b/Modules/AppFeature/Sources/AppFeature.swift @@ -14,6 +14,7 @@ public enum AppFeature { public struct State: Equatable { var path: [Destination] var isSettingsPresented: Bool + var home: HomeFeature.State var gameSession: GameSessionFeature.State? var settings: SettingsFeature.State? @@ -108,10 +109,10 @@ public enum AppFeature { switch action { case .setPath(let path): state.path = path - if state.path.contains(.gameSession) && state.gameSession == nil { + if path.contains(.gameSession) && state.gameSession == nil { state.gameSession = .init() } - if !state.path.contains(.gameSession) && state.gameSession != nil { + if !path.contains(.gameSession) && state.gameSession != nil { state.gameSession = nil } return .none @@ -124,7 +125,6 @@ public enum AppFeature { if !isPresented && state.settings != nil { state.settings = nil } - return .none case .home(.delegate(.settings)): @@ -133,12 +133,6 @@ public enum AppFeature { case .home(.delegate(.play)): return .run { .setPath([.gameSession]) } - case .home: - return .none - - case .settings: - return .none - case .gameSession(.delegate(.settings)): return .run { .setSettingsPresented(true) } @@ -147,6 +141,12 @@ public enum AppFeature { case .gameSession: return .none + + case .home: + return .none + + case .settings: + return .none } } } diff --git a/Modules/GameSessionFeature/Sources/GameSessionFeature.swift b/Modules/GameSessionFeature/Sources/GameSessionFeature.swift index df5d5f377..3e3e01721 100644 --- a/Modules/GameSessionFeature/Sources/GameSessionFeature.swift +++ b/Modules/GameSessionFeature/Sources/GameSessionFeature.swift @@ -70,38 +70,26 @@ public enum GameSessionFeature { ) -> Effect { switch action { case .didAppear: - return .run { - .setGame(dependencies.createGame()) - } + return .run { .setGame(dependencies.createGame()) } case .didTapQuit: - return .run { - .delegate(.quit) - } + return .run { .delegate(.quit) } case .didTapSettings: - return .run { - .delegate(.settings) - } + return .run { .delegate(.settings) } case .didTapCard(let card): guard let controlledPlayer = state.controlledPlayer else { return .none } - return .run { - .game(.preparePlay(card, player: controlledPlayer)) - } + return .run { .game(.preparePlay(card, player: controlledPlayer)) } case .didChoose(let option, let chooser): - return .run { - .game(.choose(option, player: chooser)) - } + return .run { .game(.choose(option, player: chooser)) } case .setGame(let game): state.game = game - return .run { - .game(.startTurn(player: game.startOrder[0])) - } + return .run { .game(.startTurn(player: game.startOrder[0])) } case .game: return .none diff --git a/Modules/HomeFeature/Sources/HomeFeature.swift b/Modules/HomeFeature/Sources/HomeFeature.swift index 44f342e33..76bca180c 100644 --- a/Modules/HomeFeature/Sources/HomeFeature.swift +++ b/Modules/HomeFeature/Sources/HomeFeature.swift @@ -61,14 +61,10 @@ public enum HomeFeature { } case .didTapPlay: - return .run { - .delegate(.play) - } + return .run { .delegate(.play) } case .didTapSettings: - return .run { - .delegate(.settings) - } + return .run { .delegate(.settings) } case .delegate: return .none diff --git a/Modules/SettingsFeature/Sources/Home/SettingsHomeFeature.swift b/Modules/SettingsFeature/Sources/Home/SettingsHomeFeature.swift index 3ad906758..46a47298a 100644 --- a/Modules/SettingsFeature/Sources/Home/SettingsHomeFeature.swift +++ b/Modules/SettingsFeature/Sources/Home/SettingsHomeFeature.swift @@ -101,14 +101,10 @@ public enum SettingsHomeFeature { } case .didTapFigures: - return .run { - .delegate(.selectedFigures) - } + return .run { .delegate(.selectedFigures) } case .didTapCollectibles: - return .run { - .delegate(.selectedCollectibles) - } + return .run { .delegate(.selectedCollectibles) } case .delegate: break diff --git a/Modules/SettingsFeature/Sources/SettingsFeature.swift b/Modules/SettingsFeature/Sources/SettingsFeature.swift index deb105c3b..f12fcb602 100644 --- a/Modules/SettingsFeature/Sources/SettingsFeature.swift +++ b/Modules/SettingsFeature/Sources/SettingsFeature.swift @@ -12,8 +12,8 @@ public enum SettingsFeature { var path: [Destination] var home: SettingsHomeFeature.State - var figures: SettingsFiguresFeature.State - var collectibles: SettingsCollectiblesFeature.State + var figures: SettingsFiguresFeature.State? + var collectibles: SettingsCollectiblesFeature.State? public enum Destination: Hashable, Sendable { case figures @@ -23,8 +23,8 @@ public enum SettingsFeature { public init( path: [Destination] = [], home: SettingsHomeFeature.State = .init(), - figures: SettingsFiguresFeature.State = .init(), - collectibles: SettingsCollectiblesFeature.State = .init() + figures: SettingsFiguresFeature.State? = nil, + collectibles: SettingsCollectiblesFeature.State? = nil ) { self.path = path self.home = home @@ -34,8 +34,10 @@ public enum SettingsFeature { } public enum Action { + // View case setPath([State.Destination]) + // Internal case home(SettingsHomeFeature.Action) case figures(SettingsFiguresFeature.Action) case collectibles(SettingsCollectiblesFeature.Action) @@ -61,8 +63,8 @@ public enum SettingsFeature { ), pullback( SettingsFiguresFeature.reducer, - state: { _ in - \.figures + state: { + $0.figures != nil ? \.figures! : nil }, action: { globalAction in if case let .figures(localAction) = globalAction { @@ -76,8 +78,8 @@ public enum SettingsFeature { ), pullback( SettingsCollectiblesFeature.reducer, - state: { _ in - \.collectibles + state: { + $0.collectibles != nil ? \.collectibles! : nil }, action: { globalAction in if case let .collectibles(localAction) = globalAction { @@ -100,15 +102,25 @@ public enum SettingsFeature { switch action { case .setPath(let path): state.path = path + if path.contains(.figures) && state.figures == nil { + state.figures = .init() + } + if !path.contains(.figures) && state.figures != nil { + state.figures = nil + } + if path.contains(.collectibles) && state.collectibles == nil { + state.collectibles = .init() + } + if !path.contains(.collectibles) && state.collectibles != nil { + state.collectibles = nil + } return .none case .home(.delegate(.selectedCollectibles)): - state.path = [.collectibles] - return .none + return .run { .setPath([.collectibles]) } case .home(.delegate(.selectedFigures)): - state.path = [.figures] - return .none + return .run { .setPath([.figures]) } case .home: return .none diff --git a/Modules/SettingsFeature/Sources/SettingsView.swift b/Modules/SettingsFeature/Sources/SettingsView.swift index 45f753533..e58e911f8 100644 --- a/Modules/SettingsFeature/Sources/SettingsView.swift +++ b/Modules/SettingsFeature/Sources/SettingsView.swift @@ -18,10 +18,8 @@ public struct SettingsView: View { _store = StateObject(wrappedValue: store()) } - @State private var path: [SettingsFeature.State.Destination] = [] - public var body: some View { - NavigationStack(path: $path) { + NavigationStack(path: store.binding(\.path, send: { .setPath($0) })) { SettingsHomeView { store.projection( state: \.home, @@ -35,20 +33,6 @@ public struct SettingsView: View { viewForDestination($0) } } - // Fix Error `Update NavigationAuthority bound path tried to update multiple times per frame` - .onReceive(store.$state) { state in - let newPath = state.path - if newPath != path { - path = newPath - } - } - .onChange(of: path) { _, newPath in - if newPath != store.state.path { - Task { - await store.dispatch(.setPath(newPath)) - } - } - } .presentationDetents([.large]) } From f81d36d412a53d9ab8f84143e8e5872e188c2ac9 Mon Sep 17 00:00:00 2001 From: Stephano Telolahy Date: Wed, 10 Dec 2025 22:31:22 +0100 Subject: [PATCH 3/5] wip --- Modules/AppFeature/Sources/AppView.swift | 15 +++------------ .../SettingsFeature/Sources/SettingsView.swift | 15 +++------------ 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/Modules/AppFeature/Sources/AppView.swift b/Modules/AppFeature/Sources/AppView.swift index 38eec6c05..65d837ecb 100644 --- a/Modules/AppFeature/Sources/AppView.swift +++ b/Modules/AppFeature/Sources/AppView.swift @@ -27,10 +27,7 @@ public struct AppView: View { public var body: some View { NavigationStack(path: store.binding(\.path, send: { .setPath($0) })) { HomeView { - store.projection( - state: \.home, - action: { .home($0) } - ) + store.projection(state: \.home, action: { .home($0) }) } .navigationDestination(for: AppFeature.State.Destination.self) { viewForDestination($0) @@ -38,10 +35,7 @@ public struct AppView: View { } .sheet(isPresented: store.binding(\.isSettingsPresented, send: { .setSettingsPresented($0) })) { SettingsView { - store.projection( - state: \.settings, - action: { .settings($0) } - ) + store.projection(state: \.settings, action: { .settings($0) }) } } .onReceive(store.dispatchedAction) { event in @@ -54,10 +48,7 @@ public struct AppView: View { switch destination { case .gameSession: GameSessionView { - store.projection( - state: \.gameSession, - action: { .gameSession($0) } - ) + store.projection(state: \.gameSession, action: { .gameSession($0) }) } } } diff --git a/Modules/SettingsFeature/Sources/SettingsView.swift b/Modules/SettingsFeature/Sources/SettingsView.swift index e58e911f8..358e24b57 100644 --- a/Modules/SettingsFeature/Sources/SettingsView.swift +++ b/Modules/SettingsFeature/Sources/SettingsView.swift @@ -21,10 +21,7 @@ public struct SettingsView: View { public var body: some View { NavigationStack(path: store.binding(\.path, send: { .setPath($0) })) { SettingsHomeView { - store.projection( - state: \.home, - action: { .home($0) } - ) + store.projection(state: \.home, action: { .home($0) }) } #if os(iOS) || os(tvOS) || os(visionOS) .navigationBarTitleDisplayMode(.inline) @@ -40,18 +37,12 @@ public struct SettingsView: View { switch destination { case .figures: SettingsFiguresView { - store.projection( - state: \.figures, - action: { .figures($0) } - ) + store.projection(state: \.figures, action: { .figures($0) }) } case .collectibles: SettingsCollectiblesView { - store.projection( - state: \.collectibles, - action: { .collectibles($0) } - ) + store.projection(state: \.collectibles, action: { .collectibles($0) }) } } } From 2922bb31d0166938d42d2622d04fafaf4bcebbf2 Mon Sep 17 00:00:00 2001 From: Stephano Telolahy Date: Wed, 10 Dec 2025 22:42:48 +0100 Subject: [PATCH 4/5] wip --- .../Sources/Home/SettingsHomeView.swift | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/Modules/SettingsFeature/Sources/Home/SettingsHomeView.swift b/Modules/SettingsFeature/Sources/Home/SettingsHomeView.swift index aff183474..20c374f40 100644 --- a/Modules/SettingsFeature/Sources/Home/SettingsHomeView.swift +++ b/Modules/SettingsFeature/Sources/Home/SettingsHomeView.swift @@ -60,14 +60,8 @@ struct SettingsHomeView: View { Image(systemName: "person.2") Stepper( "Players count: \(store.state.playersCount)", - value: Binding( - get: { store.state.playersCount }, - set: { newValue in - Task { - await store.dispatch(.didUpdatePlayersCount(newValue)) - } - } - ).animation(), + value: store.binding(\.playersCount, send: { .didUpdatePlayersCount($0) }) + .animation(), in: store.state.minPlayersCount...store.state.maxPlayersCount ) } @@ -101,14 +95,8 @@ struct SettingsHomeView: View { private var simulationView: some View { HStack { Image(systemName: "record.circle") - Toggle(isOn: Binding( - get: { store.state.simulation }, - set: { _ in - Task { - await store.dispatch(.didToggleSimulation) - } - } - ).animation()) { + Toggle(isOn: store.binding(\.simulation, send: { _ in .didToggleSimulation }) + .animation()) { Text("Simulation") } } @@ -120,14 +108,7 @@ struct SettingsHomeView: View { VStack(alignment: .leading) { Text("Sound volume") Slider( - value: Binding( - get: { store.state.musicVolume }, - set: { newValue in - Task { - await store.dispatch(.didUpdateMusicVolume(newValue)) - } - } - ), + value: store.binding(\.musicVolume, send: { .didUpdateMusicVolume($0) }), in: 0.0...1.0 ) } From 905718a4a030d2c9e4ee327a330d8404b5b21b7c Mon Sep 17 00:00:00 2001 From: Stephano Telolahy Date: Wed, 10 Dec 2025 22:55:49 +0100 Subject: [PATCH 5/5] wip --- Modules/AppFeature/Sources/AppFeature.swift | 45 ++++--------------- .../Sources/GameSessionFeature.swift | 15 ++----- Modules/Redux/Tests/Misc/GlobalFeature.swift | 22 +++------ .../Sources/SettingsFeature.swift | 45 ++++--------------- 4 files changed, 27 insertions(+), 100 deletions(-) diff --git a/Modules/AppFeature/Sources/AppFeature.swift b/Modules/AppFeature/Sources/AppFeature.swift index d13a48d2f..1a3b0eb91 100644 --- a/Modules/AppFeature/Sources/AppFeature.swift +++ b/Modules/AppFeature/Sources/AppFeature.swift @@ -54,48 +54,21 @@ public enum AppFeature { reducerMain, pullback( HomeFeature.reducer, - state: { _ in - \.home - }, - action: { globalAction in - if case let .home(localAction) = globalAction { - return localAction - } - return nil - }, - embedAction: { - .home($0) - } + state: { _ in \.home }, + action: { if case let .home(action) = $0 { action } else { nil } }, + embedAction: Action.home ), pullback( GameSessionFeature.reducer, - state: { - $0.gameSession != nil ? \.gameSession! : nil - }, - action: { globalAction in - if case let .gameSession(localAction) = globalAction { - return localAction - } - return nil - }, - embedAction: { - .gameSession($0) - } + state: { $0.gameSession != nil ? \.gameSession! : nil }, + action: { if case let .gameSession(action) = $0 { action } else { nil } }, + embedAction: Action.gameSession ), pullback( SettingsFeature.reducer, - state: { - $0.settings != nil ? \.settings! : nil - }, - action: { globalAction in - if case let .settings(localAction) = globalAction { - return localAction - } - return nil - }, - embedAction: { - .settings($0) - } + state: { $0.settings != nil ? \.settings! : nil }, + action: { if case let .settings(action) = $0 { action } else { nil } }, + embedAction: Action.settings ) ) } diff --git a/Modules/GameSessionFeature/Sources/GameSessionFeature.swift b/Modules/GameSessionFeature/Sources/GameSessionFeature.swift index 3e3e01721..14dfc9972 100644 --- a/Modules/GameSessionFeature/Sources/GameSessionFeature.swift +++ b/Modules/GameSessionFeature/Sources/GameSessionFeature.swift @@ -47,18 +47,9 @@ public enum GameSessionFeature { reducerSound, pullback( GameFeature.reducer, - state: { - $0.game != nil ? \.game! : nil - }, - action: { globalAction in - if case let .game(localAction) = globalAction { - return localAction - } - return nil - }, - embedAction: { - .game($0) - } + state: { $0.game != nil ? \.game! : nil }, + action: { if case let .game(action) = $0 { action } else { nil } }, + embedAction: Action.game ) ) } diff --git a/Modules/Redux/Tests/Misc/GlobalFeature.swift b/Modules/Redux/Tests/Misc/GlobalFeature.swift index db1dea51f..4569525ff 100644 --- a/Modules/Redux/Tests/Misc/GlobalFeature.swift +++ b/Modules/Redux/Tests/Misc/GlobalFeature.swift @@ -21,25 +21,15 @@ enum GlobalFeature { combine( pullback( CounterFeature.reducer, - state: { _ in - \.counter - }, - action: { globalAction in - if case let .counter(localAction) = globalAction { return localAction } - return nil - }, - embedAction: GlobalFeature.Action.counter + state: { _ in \.counter }, + action: { if case let .counter(action) = $0 { action } else { nil } }, + embedAction: Action.counter ), pullback( FlagFeature.reducer, - state: { _ in - \.flag - }, - action: { globalAction in - if case let .flag(localAction) = globalAction { return localAction } - return nil - }, - embedAction: GlobalFeature.Action.flag + state: { _ in \.flag }, + action: { if case let .flag(action) = $0 { action } else { nil } }, + embedAction: Action.flag ) ) } diff --git a/Modules/SettingsFeature/Sources/SettingsFeature.swift b/Modules/SettingsFeature/Sources/SettingsFeature.swift index f12fcb602..af3650305 100644 --- a/Modules/SettingsFeature/Sources/SettingsFeature.swift +++ b/Modules/SettingsFeature/Sources/SettingsFeature.swift @@ -48,48 +48,21 @@ public enum SettingsFeature { reducerMain, pullback( SettingsHomeFeature.reducer, - state: { _ in - \.home - }, - action: { globalAction in - if case let .home(localAction) = globalAction { - return localAction - } - return nil - }, - embedAction: { - .home($0) - } + state: { _ in \.home }, + action: { if case let .home(action) = $0 { action } else { nil } }, + embedAction: Action.home ), pullback( SettingsFiguresFeature.reducer, - state: { - $0.figures != nil ? \.figures! : nil - }, - action: { globalAction in - if case let .figures(localAction) = globalAction { - return localAction - } - return nil - }, - embedAction: { - .figures($0) - } + state: { $0.figures != nil ? \.figures! : nil }, + action: { if case let .figures(action) = $0 { action } else { nil } }, + embedAction: Action.figures ), pullback( SettingsCollectiblesFeature.reducer, - state: { - $0.collectibles != nil ? \.collectibles! : nil - }, - action: { globalAction in - if case let .collectibles(localAction) = globalAction { - return localAction - } - return nil - }, - embedAction: { - .collectibles($0) - } + state: { $0.collectibles != nil ? \.collectibles! : nil }, + action: { if case let .collectibles(action) = $0 { action } else { nil } }, + embedAction: Action.collectibles ) ) }