diff --git a/App/App/Localizable.xcstrings b/App/App/Localizable.xcstrings index c839414..5b3ff57 100644 --- a/App/App/Localizable.xcstrings +++ b/App/App/Localizable.xcstrings @@ -83,6 +83,22 @@ } } }, + "generic.game.launch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Play" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jouer" + } + } + } + }, "generic.player.type.aiFinnishHim" : { "extractionState" : "manual", "localizations" : { @@ -498,23 +514,6 @@ } } }, - "newGame.play" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Play" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jouer" - } - } - } - }, "newGame.player.name" : { "extractionState" : "manual", "localizations" : { diff --git a/App/App/View/IngameView.swift b/App/App/View/IngameView.swift index fd6bc77..37daadf 100644 --- a/App/App/View/IngameView.swift +++ b/App/App/View/IngameView.swift @@ -77,10 +77,7 @@ struct IngameView: View { } } - init(settings: NewGameVM, player1: PlayerSettingsVM, player2: PlayerSettingsVM) { - guard let vm = IngameVM(settings: settings, player1: player1, player2: player2) - else { fatalError("TODO: how to handle game setup failure") } - + init(vm: IngameVM) { self._vm = StateObject(wrappedValue: vm) } } @@ -155,5 +152,5 @@ private struct PlayerView: View { } #Preview { - IngameView(settings: NewGameVM(), player1: PlayerSettingsVM(type: .Human), player2: PlayerSettingsVM(type: .AISimpleNegaMax)) + IngameView(vm: IngameVM(settings: NewGameVM(), player1: PlayerSettingsVM(type: .Human), player2: PlayerSettingsVM(type: .AISimpleNegaMax))) } diff --git a/App/App/View/MainMenuView.swift b/App/App/View/MainMenuView.swift index 0d4d534..cb62827 100644 --- a/App/App/View/MainMenuView.swift +++ b/App/App/View/MainMenuView.swift @@ -1,6 +1,9 @@ import SwiftUI struct MainMenuView: View { + @State + private var currentGame: IngameVM? = nil + var body: some View { VStack(spacing: 20) { Spacer() @@ -13,16 +16,25 @@ struct MainMenuView: View { Spacer() - NavigationLink(destination: NewGameView().navigationTitle("newGame.title")) { + NavigationLink(destination: NewGameView(currentGame: $currentGame).navigationTitle("newGame.title")) { Label("mainMenu.button.newGame", systemImage: "play") } - NavigationLink(destination: SavedGamesView().navigationTitle("savedGames.title")) { + NavigationLink(destination: SavedGamesView(currentGame: $currentGame).navigationTitle("savedGames.title")) { Label("mainMenu.button.scoreboard", systemImage: "rosette") } Spacer() - } + }.navigationDestination(isPresented: self.$currentGame.map(get: { $0 != nil }, setWithContext: { + game, presented in + if presented { + game + } else { + nil + } + }), destination: { + NavigationLazyView(IngameView(vm: currentGame!)) + }) } } diff --git a/App/App/View/NavigationLazyView.swift b/App/App/View/NavigationLazyView.swift new file mode 100644 index 0000000..8566a17 --- /dev/null +++ b/App/App/View/NavigationLazyView.swift @@ -0,0 +1,13 @@ +import Foundation +import SwiftUI + +// https://stackoverflow.com/questions/57594159/swiftui-navigationlink-loads-destination-view-immediately-without-clicking/61234030#61234030 +struct NavigationLazyView: View { + let build: () -> Content + init(_ build: @autoclosure @escaping () -> Content) { + self.build = build + } + var body: Content { + build() + } +} diff --git a/App/App/View/NewGameView.swift b/App/App/View/NewGameView.swift index 68e1037..83dff5a 100644 --- a/App/App/View/NewGameView.swift +++ b/App/App/View/NewGameView.swift @@ -9,6 +9,9 @@ import SwiftUI import PhotosUI struct NewGameView: View { + @Binding + var currentGame: IngameVM? + @StateObject private var vm: NewGameVM = NewGameVM() @StateObject @@ -47,27 +50,14 @@ struct NewGameView: View { // // } } - }.toolbar { - NavigationLink { - NavigationLazyView(IngameView(settings: vm, player1: p1, player2: p2)) - } label: { - Label("newGame.play", systemImage: "play") - } + + Button("generic.game.launch", systemImage: "play") { + currentGame = IngameVM(settings: vm, player1: p1, player2: p2) + }.buttonStyle(.borderedProminent) } } } -// https://stackoverflow.com/questions/57594159/swiftui-navigationlink-loads-destination-view-immediately-without-clicking/61234030#61234030 -struct NavigationLazyView: View { - let build: () -> Content - init(_ build: @autoclosure @escaping () -> Content) { - self.build = build - } - var body: Content { - build() - } -} - private struct PlayerSectionView: View { private let sectionLabel: LocalizedStringKey private let pieceColor: Color @@ -135,5 +125,5 @@ extension RulesType { } #Preview { - NewGameView() + NewGameView(currentGame: .constant(nil)) } diff --git a/App/App/View/SavedGamesView.swift b/App/App/View/SavedGamesView.swift index 2198f5c..2cea61e 100644 --- a/App/App/View/SavedGamesView.swift +++ b/App/App/View/SavedGamesView.swift @@ -3,12 +3,15 @@ import SwiftUI struct SavedGamesView: View { @StateObject private var vm = SavedGamesVM() + + @Binding + var currentGame: IngameVM? var body: some View { TabView { VStack { Section("savedGames.section.unfinished") { - ScoreboardView(results: self.$vm.unfinished, "scoreboard.table.column.players") { + ScoreboardView(results: self.$vm.unfinished, currentGame: $currentGame, "scoreboard.table.column.players") { result in let n1 = if result.player1Name != nil { Text(result.player1Name!) @@ -28,7 +31,7 @@ struct SavedGamesView: View { VStack { Section("savedGames.section.finished") { - ScoreboardView(results: self.$vm.finished, "scoreboard.table.column.players") { + ScoreboardView(results: self.$vm.finished, currentGame: $currentGame, "scoreboard.table.column.players") { result in let n1 = if result.player1Name != nil { Text(result.player1Name!) @@ -60,12 +63,8 @@ struct SavedGamesView: View { // //} // } } - - init() { - - } } #Preview { - SavedGamesView() + SavedGamesView(currentGame: .constant(nil)) } diff --git a/App/App/View/ScoreboardView.swift b/App/App/View/ScoreboardView.swift index ad0d137..c21518d 100644 --- a/App/App/View/ScoreboardView.swift +++ b/App/App/View/ScoreboardView.swift @@ -1,10 +1,3 @@ -// -// ScoreboardView.swift -// App -// -// Created by etudiant2 on 28/05/2025. -// - import SwiftUI struct ScoreboardView: View { @@ -14,13 +7,19 @@ struct ScoreboardView: View { private let playerRelatedColumnKey: LocalizedStringKey private let localizedKeyProvider: (GameEntryVM) -> LocalizedStringKey + @State + private var selection: GameEntryVM.ID? = nil + @Binding private var results: [GameEntryVM] + @Binding + private var currentGame: IngameVM? + var body: some View { if horizontalSizeClass == .compact { List(self.results) { result in - VStack(alignment: .center) { + let entry = VStack(alignment: .center) { Text(localizedKeyProvider(result)) HStack { Text("generic.datetime \(Text(result.date, style: .date)) \(Text(result.date, style: .time))") @@ -28,9 +27,19 @@ struct ScoreboardView: View { Text(result.rules.baseTranslationKey) }.foregroundStyle(.secondary) } + + if let game = result.game { + Button { + currentGame = IngameVM(game: game) + } label: { + entry + }.buttonRepeatBehavior(.disabled) + } else { + entry + } } - } else{ - Table(self.results) { + } else { + Table(self.results, selection: $selection) { TableColumn("scoreboard.table.column.date") { result in Text("generic.datetime \(Text(result.date, style: .date)) \(Text(result.date, style: .time))") } @@ -40,18 +49,25 @@ struct ScoreboardView: View { TableColumn("scoreboard.table.column.rules", content: { result in Text(result.rules.baseTranslationKey) }) + }.contextMenu(forSelectionType: GameEntryVM.ID.self) { _ in } primaryAction: { items in + guard !items.isEmpty, + let selection, + let game = results.first(where: { $0.date == selection && $0.game != nil })?.game + else { return } + + currentGame = IngameVM(game: game) } } } - public init(results: Binding<[GameEntryVM]>, _ playerRelatedColumnKey: LocalizedStringKey, localizedKeyProvider: @escaping (GameEntryVM) -> LocalizedStringKey) { + public init(results: Binding<[GameEntryVM]>, currentGame: Binding, _ playerRelatedColumnKey: LocalizedStringKey, localizedKeyProvider: @escaping (GameEntryVM) -> LocalizedStringKey) { self._results = results + self._currentGame = currentGame self.playerRelatedColumnKey = playerRelatedColumnKey self.localizedKeyProvider = localizedKeyProvider } } -//TODO // #Preview { // ScoreboardView("scoreboard.table.column.players") { result in // "\(result.player1) savedGames.section.unfinished.entry \(result.player2)" diff --git a/App/App/View/UIUtilities.swift b/App/App/View/UIUtilities.swift index 017bc63..ebcab5d 100644 --- a/App/App/View/UIUtilities.swift +++ b/App/App/View/UIUtilities.swift @@ -23,7 +23,7 @@ extension Binding { func map(_ get: @escaping (Value) -> T) -> Binding { Binding( get: { get(self.wrappedValue) }, - set: { _ in fatalError("Not implemented") } + set: { _ in fatalError("Not supported") } ) } @@ -36,4 +36,14 @@ extension Binding { set: { self.wrappedValue = set($0) } ) } + + func map( + get: @escaping (Value) -> T, + setWithContext set: @escaping (Value, T) -> Value + ) -> Binding { + Binding( + get: { get(self.wrappedValue) }, + set: { self.wrappedValue = set(self.wrappedValue, $0) } + ) + } } diff --git a/App/App/ViewModel/InGameVM.swift b/App/App/ViewModel/InGameVM.swift index 120080e..652dd4e 100644 --- a/App/App/ViewModel/InGameVM.swift +++ b/App/App/ViewModel/InGameVM.swift @@ -40,30 +40,16 @@ class IngameVM: ObservableObject { var result: Connect4Core.Result = .notFinished private var running: Bool = false - - init?(settings: NewGameVM, player1: PlayerSettingsVM, player2: PlayerSettingsVM) { + + init(game: Game) { let fmt = DateFormatter() fmt.locale = Locale(identifier: "EN") // No "root" locale? self.gameName = fmt.string(from: Date.now) - - let rulesInit: (Int, Int, Int) -> (any Rules)? = switch (settings.rulesType) { - case .Classic: Connect4Rules.init - case .TicTacToe: TicTacToeRules.init - case .PopOut: PopOutRules.init - } - guard let rules = rulesInit( - Int(settings.height), - Int(settings.width), - Int(settings.alignedTokens) - ) else { return nil } - - guard let player1 = Self.playerOf(settings: player1, id: .player1) else { return nil } - guard let player2 = Self.playerOf(settings: player2, id: .player2) else { return nil } - self.game = try! Game(withRules: rules, andPlayer1: player1, andPlayer2: player2) + self.game = game - self.player1 = PlayerVM(inner: player1) - self.player2 = PlayerVM(inner: player2) + self.player1 = PlayerVM(inner: game.players[.player1]!) + self.player2 = PlayerVM(inner: game.players[.player2]!) self.currentBoard = self.game.board @@ -117,6 +103,26 @@ class IngameVM: ObservableObject { } } } + + convenience init(settings: NewGameVM, player1: PlayerSettingsVM, player2: PlayerSettingsVM) { + let rulesInit: (Int, Int, Int) -> (any Rules)? = switch (settings.rulesType) { + case .Classic: Connect4Rules.init + case .TicTacToe: TicTacToeRules.init + case .PopOut: PopOutRules.init + } + let rules = rulesInit( + Int(settings.height), + Int(settings.width), + Int(settings.alignedTokens) + )! + + let player1 = Self.playerOf(settings: player1, id: .player1)! + let player2 = Self.playerOf(settings: player2, id: .player2)! + + let game = try! Game(withRules: rules, andPlayer1: player1, andPlayer2: player2) + + self.init(game: game) + } public func start() { if running { return }