Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 34 additions & 48 deletions Modules/AppFeature/Sources/AppFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ 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 {
Expand All @@ -23,14 +25,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
}
}

Expand All @@ -50,52 +54,26 @@ 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: { _ in
\.gameSession
},
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
)
)
}

// swiftlint:disable:next cyclomatic_complexity
private static func reducerMain(
into state: inout State,
action: Action,
Expand All @@ -104,12 +82,20 @@ public enum AppFeature {
switch action {
case .setPath(let path):
state.path = path
if path.contains(.gameSession) && state.gameSession == nil {
state.gameSession = .init()
}
if !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
Expand All @@ -120,12 +106,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) }

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

case .gameSession:
return .none

case .home:
return .none

case .settings:
return .none
}
}
}
48 changes: 5 additions & 43 deletions Modules/AppFeature/Sources/AppView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ public struct AppView: View {
public typealias ViewStore = Store<AppFeature.State, AppFeature.Action>

@StateObject private var store: ViewStore
@State private var path: [AppFeature.State.Destination] = []
@State private var isSettingsPresented: Bool = false

@Environment(\.theme) private var theme

Expand All @@ -27,50 +25,17 @@ 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,
action: { .home($0) }
)
store.projection(state: \.home, action: { .home($0) })
}
.navigationDestination(for: AppFeature.State.Destination.self) {
viewForDestination($0)
}
}
.sheet(isPresented: $isSettingsPresented) {
.sheet(isPresented: store.binding(\.isSettingsPresented, send: { .setSettingsPresented($0) })) {
SettingsView {
store.projection(
state: \.settings,
action: { .settings($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
}

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))
}
store.projection(state: \.settings, action: { .settings($0) })
}
}
.onReceive(store.dispatchedAction) { event in
Expand All @@ -83,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) })
}
}
}
Expand Down
39 changes: 9 additions & 30 deletions Modules/GameSessionFeature/Sources/GameSessionFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)
}
Expand All @@ -70,38 +61,26 @@ public enum GameSessionFeature {
) -> Effect<Action> {
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
Expand Down
8 changes: 2 additions & 6 deletions Modules/HomeFeature/Sources/HomeFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions Modules/Redux/Sources/Store+Binding.swift
Original file line number Diff line number Diff line change
@@ -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<Value>(
_ keyPath: KeyPath<State, Value>,
send valueToAction: @escaping (Value) -> Action
) -> Binding<Value> {
.init(
get: { self.state[keyPath: keyPath] },
set: { newValue in
Task {
await self.dispatch(valueToAction(newValue))
}
}
)
}
}
22 changes: 6 additions & 16 deletions Modules/Redux/Tests/Misc/GlobalFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading