From c8ded4e84c5ea490e591ae8cda7b169e7f82b27c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 15:18:19 -0800 Subject: [PATCH 01/61] Bump --- App/isowords.xcodeproj/project.pbxproj | 6 ----- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- Package.swift | 23 ++++++++++--------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/App/isowords.xcodeproj/project.pbxproj b/App/isowords.xcodeproj/project.pbxproj index 20b2baec..9579f478 100644 --- a/App/isowords.xcodeproj/project.pbxproj +++ b/App/isowords.xcodeproj/project.pbxproj @@ -1339,8 +1339,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = TESTFLIGHT; @@ -1826,8 +1824,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1884,8 +1880,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 58b2bdd9..edbe5c49 100644 --- a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "branch" : "main", - "revision" : "af5ae21f65553d5bb39d55d9b4f80182a0c4fcb5" + "branch" : "observation-beta", + "revision" : "06ca7d94b4d2d60c437e9c5b577dc4ae56e6c57f" } }, { diff --git a/Package.swift b/Package.swift index dab77533..610a8357 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.9 import Foundation import PackageDescription @@ -7,10 +7,10 @@ import PackageDescription var package = Package( name: "isowords", platforms: [ - .iOS(.v16), - .macOS(.v13), - .tvOS(.v16), - .watchOS(.v9), + .iOS(.v17), + .macOS(.v14), + .tvOS(.v17), + .watchOS(.v10), ], products: [ .library(name: "Build", targets: ["Build"]), @@ -29,7 +29,8 @@ var package = Package( .package(url: "https://github.com/apple/swift-crypto", from: "1.1.6"), .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.1.0"), .package( - url: "https://github.com/pointfreeco/swift-composable-architecture", branch: "main" + url: "https://github.com/pointfreeco/swift-composable-architecture", + branch: "observation-beta" ), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), @@ -45,7 +46,7 @@ var package = Package( .target( name: "Build", dependencies: [ - .product(name: "Dependencies", package: "swift-composable-architecture"), + .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Tagged", package: "swift-tagged"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ] @@ -61,7 +62,7 @@ var package = Package( name: "DictionaryClient", dependencies: [ "SharedModels", - .product(name: "Dependencies", package: "swift-composable-architecture"), + .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ] ), @@ -254,7 +255,7 @@ if ProcessInfo.processInfo.environment["TEST_SERVER"] == nil { "SharedModels", "XCTestDebugSupport", .product(name: "CasePaths", package: "swift-case-paths"), - .product(name: "Dependencies", package: "swift-composable-architecture"), + .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ] ), @@ -265,7 +266,7 @@ if ProcessInfo.processInfo.environment["TEST_SERVER"] == nil { "ServerRouter", "SharedModels", "TcaHelpers", - .product(name: "Dependencies", package: "swift-composable-architecture"), + .product(name: "Dependencies", package: "swift-dependencies"), ], exclude: ["Secrets.swift.example"] ), @@ -525,7 +526,7 @@ if ProcessInfo.processInfo.environment["TEST_SERVER"] == nil { .target( name: "DeviceId", dependencies: [ - .product(name: "Dependencies", package: "swift-composable-architecture"), + .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ] ), From bf0c97a2cbb1be330ad16548a5ec8f8a4b1dac9e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 15:32:42 -0800 Subject: [PATCH 02/61] wip --- .../xcshareddata/swiftpm/Package.resolved | 24 ++++---- Sources/AppFeature/AppView.swift | 60 ++++++++----------- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index edbe5c49..fa9e605e 100644 --- a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "ed7facdd4a361514b46e3bbc6238cd41c84be4ec", - "version" : "1.1.1" + "revision" : "a5521dde99570789d8cb7c43e51418d7cd1a87ca", + "version" : "1.1.2" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", - "version" : "1.0.0" + "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version" : "1.0.2" } }, { @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "65fc9e2b62727cacfab9fc60d580c284a4b9308c", - "version" : "1.1.1" + "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", + "version" : "1.1.2" } }, { @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "63301f4a181ed9aefb46dccef2dfb66466798341", - "version" : "1.1.1" + "revision" : "9783b58167f7618cb86011156e741cbc6f4cc864", + "version" : "1.1.2" } }, { @@ -176,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types", "state" : { - "revision" : "99d066e29effa8845e4761dd3f2f831edfdf8925", - "version" : "1.0.0" + "revision" : "1827dc94bdab2eb5f2fc804e9b0cb43574282566", + "version" : "1.0.2" } }, { @@ -293,8 +293,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "4862d48562483d274a2ac7522d905c9237a31a48", - "version" : "1.15.0" + "revision" : "59b663f68e69f27a87b45de48cb63264b8194605", + "version" : "1.15.1" } }, { diff --git a/Sources/AppFeature/AppView.swift b/Sources/AppFeature/AppView.swift index e6430ae8..e39382f7 100644 --- a/Sources/AppFeature/AppView.swift +++ b/Sources/AppFeature/AppView.swift @@ -14,6 +14,7 @@ import SwiftUI public struct AppReducer { @Reducer public struct Destination { + @ObservableState public enum State: Equatable { case game(Game.State) case onboarding(Onboarding.State) @@ -32,15 +33,16 @@ public struct AppReducer { } } + @ObservableState public struct State: Equatable { public var appDelegate: AppDelegateReducer.State - @PresentationState public var destination: Destination.State? + @Presents public var destination: Destination.State? public var home: Home.State public init( appDelegate: AppDelegateReducer.State = AppDelegateReducer.State(), destination: Destination.State? = nil, - home: Home.State = .init() + home: Home.State = Home.State() ) { self.appDelegate = appDelegate self.destination = destination @@ -328,60 +330,48 @@ public struct AppReducer { public struct AppView: View { let store: StoreOf - @ObservedObject var viewStore: ViewStore @Environment(\.deviceState) var deviceState - struct ViewState: Equatable { - let isHomeActive: Bool - - init(state: AppReducer.State) { - self.isHomeActive = state.destination == nil - } - } - public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) } public var body: some View { Group { - if self.viewStore.isHomeActive { + switch store.destination { + case .none: NavigationStack { - HomeView(store: self.store.scope(state: \.home, action: \.home)) + HomeView(store: store.scope(state: \.home, action: \.home)) } .zIndex(0) - } else { - IfLetStore( - self.store.scope(state: \.destination?.game, action: \.destination.game) - ) { store in + + case .some(.game): + if let store = store.scope(state: \.destination?.game, action: \.destination.game) { GameView( content: CubeView(store: store.scope(state: \.cubeScene, action: \.cubeScene)), store: store ) + .transition(.game) + .zIndex(1) } - .transition(.game) - .zIndex(1) - IfLetStore( - self.store.scope(state: \.destination?.onboarding, action: \.destination.onboarding), - then: OnboardingView.init(store:) - ) - .zIndex(2) + case .some(.onboarding): + if let store = store.scope( + state: \.destination?.onboarding, action: \.destination.onboarding + ) { + OnboardingView(store: store) + .zIndex(2) + } } } .modifier(DeviceStateModifier()) } } -#if DEBUG - struct AppView_Previews: PreviewProvider { - static var previews: some View { - AppView( - store: Store(initialState: AppReducer.State()) { - AppReducer() - } - ) +#Preview { + AppView( + store: Store(initialState: AppReducer.State()) { + AppReducer() } - } -#endif + ) +} From 84fb8952f41bd846bcd15bb2f08efcd60d1a378c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 15:59:32 -0800 Subject: [PATCH 03/61] wip --- .../DailyChallengeHeaderView.swift | 31 +++------ Sources/HomeFeature/Home.swift | 66 +++++++------------ Sources/HomeFeature/LeaderboardLinkView.swift | 31 +++------ Sources/HomeFeature/NagBanner.swift | 38 +++++------ Sources/HomeFeature/StartNewGameView.swift | 24 ++++--- 5 files changed, 70 insertions(+), 120 deletions(-) diff --git a/Sources/HomeFeature/DailyChallengeHeaderView.swift b/Sources/HomeFeature/DailyChallengeHeaderView.swift index f258434a..ab7b7675 100644 --- a/Sources/HomeFeature/DailyChallengeHeaderView.swift +++ b/Sources/HomeFeature/DailyChallengeHeaderView.swift @@ -8,24 +8,10 @@ import SwiftUI struct DailyChallengeHeaderView: View { @Environment(\.colorScheme) var colorScheme @Environment(\.date) var date - let store: StoreOf - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - let dailyChallenges: [FetchTodaysDailyChallengeResponse]? - - init(homeState: Home.State) { - self.dailyChallenges = homeState.dailyChallenges - } - } - - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) - } + @Bindable var store: StoreOf var body: some View { - let numberOfPlayers = self.viewStore.dailyChallenges?.reduce(into: 0) { + let numberOfPlayers = store.dailyChallenges?.reduce(into: 0) { $0 += $1.yourResult.outOf } @@ -60,7 +46,7 @@ struct DailyChallengeHeaderView: View { VStack { Button { - self.viewStore.send(.dailyChallengeButtonTapped) + store.send(.dailyChallengeButtonTapped) } label: { HStack { Group { @@ -115,15 +101,14 @@ struct DailyChallengeHeaderView: View { } } .navigationDestination( - store: self.store.scope( - state: \.$destination.dailyChallenge, action: \.destination.dailyChallenge - ), - destination: DailyChallengeView.init(store:) - ) + item: $store.scope(state: \.destination?.dailyChallenge, action: \.destination.dailyChallenge) + ) { store in + DailyChallengeView(store: store) + } } var hasPlayedAllDailyChallenges: Bool { - self.viewStore.dailyChallenges + store.dailyChallenges .map { $0.allSatisfy { $0.yourResult.rank != nil } } ?? false } diff --git a/Sources/HomeFeature/Home.swift b/Sources/HomeFeature/Home.swift index 4b0b678f..dddf3f31 100644 --- a/Sources/HomeFeature/Home.swift +++ b/Sources/HomeFeature/Home.swift @@ -24,6 +24,7 @@ public struct ActiveMatchResponse: Equatable { public struct Home { @Reducer public struct Destination { + @ObservableState public enum State: Equatable { case changelog(ChangelogReducer.State = .init()) case dailyChallenge(DailyChallengeReducer.State = .init()) @@ -64,12 +65,13 @@ public struct Home { } } + @ObservableState public struct State: Equatable { public var dailyChallenges: [FetchTodaysDailyChallengeResponse]? - @PresentationState public var destination: Destination.State? + @Presents public var destination: Destination.State? public var hasChangelog: Bool public var hasPastTurnBasedGames: Bool - @PresentationState public var nagBanner: NagBanner.State? + @Presents public var nagBanner: NagBanner.State? public var savedGames: SavedGamesState { didSet { guard var dailyChallengeState = self.destination?.dailyChallenge @@ -410,28 +412,11 @@ extension GameCenterClient { } public struct HomeView: View { - struct ViewState: Equatable { - let hasActiveGames: Bool - let hasChangelog: Bool - let isNagBannerVisible: Bool - - init(state: Home.State) { - self.hasActiveGames = - state.savedGames.dailyChallengeUnlimited != nil - || state.savedGames.unlimited != nil - || !state.turnBasedMatches.isEmpty - self.hasChangelog = state.hasChangelog - self.isNagBannerVisible = state.nagBanner != nil - } - } - @Environment(\.colorScheme) var colorScheme - let store: StoreOf - @ObservedObject var viewStore: ViewStore + @Bindable var store: StoreOf public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(store, observe: ViewState.init) } public var body: some View { @@ -440,20 +425,20 @@ public struct HomeView: View { VStack(spacing: .grid(12)) { VStack(spacing: .grid(6)) { HStack { - CubeIconView(shake: self.viewStore.hasChangelog) { - self.viewStore.send(.cubeButtonTapped) + CubeIconView(shake: store.hasChangelog) { + store.send(.cubeButtonTapped) } Spacer() Button { - self.viewStore.send(.howToPlayButtonTapped, animation: .default) + store.send(.howToPlayButtonTapped, animation: .default) } label: { Image(systemName: "questionmark.circle") } Button { - self.viewStore.send(.settingsButtonTapped) + store.send(.settingsButtonTapped) } label: { Image(systemName: "gear") } @@ -462,11 +447,11 @@ public struct HomeView: View { .foregroundColor(self.colorScheme == .dark ? .hex(0xF2E29F) : .isowordsBlack) .adaptivePadding(.horizontal) - DailyChallengeHeaderView(store: self.store) + DailyChallengeHeaderView(store: store) .screenEdgePadding(.horizontal) } - if self.viewStore.hasActiveGames { + if store.hasActiveGames { VStack(alignment: .leading) { Text("Active games") .adaptiveFont(.matterMedium, size: 16) @@ -474,19 +459,16 @@ public struct HomeView: View { .screenEdgePadding(.horizontal) ActiveGamesView( - store: self.store.scope( - state: \.activeGames, - action: Home.Action.activeGames - ), + store: store.scope(state: \.activeGames, action: \.activeGames), showMenuItems: true ) .foregroundColor(self.colorScheme == .dark ? .hex(0xE9A27C) : .isowordsBlack) } } - StartNewGameView(store: self.store) + StartNewGameView(store: store) .screenEdgePadding(.horizontal) - LeaderboardLinkView(store: self.store) + LeaderboardLinkView(store: store) .screenEdgePadding(.horizontal) } .adaptivePadding(.vertical, .grid(4)) @@ -502,7 +484,7 @@ public struct HomeView: View { ) ) - if self.viewStore.isNagBannerVisible { + if store.nagBanner != nil { Spacer().frame(height: 80) } } @@ -526,22 +508,22 @@ public struct HomeView: View { .ignoresSafeArea() ) - IfLetStore( - self.store.scope(state: \.nagBanner, action: \.nagBanner.presented), - then: NagBannerView.init(store:) - ) + if let store = store.scope(state: \.nagBanner, action: \.nagBanner.presented) { + NagBannerView(store: store) + } } .navigationBarHidden(true) .navigationDestination( - store: self.store.scope(state: \.$destination.settings, action: \.destination.settings) + item: $store.scope(state: \.destination?.settings, action: \.destination.settings) ) { store in SettingsView(store: store, navPresentationStyle: .navigation) } .sheet( - store: self.store.scope(state: \.$destination.changelog, action: \.destination.changelog), - content: ChangelogView.init(store:) - ) - .task { await self.viewStore.send(.task).finish() } + item: $store.scope(state: \.destination?.changelog, action: \.destination.changelog) + ) { store in + ChangelogView(store: store) + } + .task { await store.send(.task).finish() } } } diff --git a/Sources/HomeFeature/LeaderboardLinkView.swift b/Sources/HomeFeature/LeaderboardLinkView.swift index 94710201..5ae13b33 100644 --- a/Sources/HomeFeature/LeaderboardLinkView.swift +++ b/Sources/HomeFeature/LeaderboardLinkView.swift @@ -6,21 +6,7 @@ import SwiftUI struct LeaderboardLinkView: View { @Environment(\.colorScheme) var colorScheme - let store: StoreOf - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - var weekInReview: FetchWeekInReviewResponse? - - init(state: Home.State) { - self.weekInReview = state.weekInReview - } - } - - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) - } + @Bindable var store: StoreOf var body: some View { VStack(alignment: .leading) { @@ -32,14 +18,14 @@ struct LeaderboardLinkView: View { Spacer() Button("View all") { - self.viewStore.send(.leaderboardButtonTapped) + store.send(.leaderboardButtonTapped) } .adaptiveFont(.matterMedium, size: 12) } .foregroundColor(self.colorScheme == .dark ? .hex(0xE79072) : .isowordsBlack) Button { - self.viewStore.send(.leaderboardButtonTapped) + store.send(.leaderboardButtonTapped) } label: { VStack(alignment: .leading, spacing: .grid(4)) { Text("Week in review") @@ -48,7 +34,7 @@ struct LeaderboardLinkView: View { .frame(height: 2) .background(self.colorScheme == .dark ? Color.isowordsBlack : .hex(0xE26C5E)) - self.weekInReview(self.viewStore.weekInReview) + self.weekInReview(store.weekInReview) .adaptiveFont(.matterMedium, size: 14) } } @@ -59,11 +45,10 @@ struct LeaderboardLinkView: View { ) ) .navigationDestination( - store: self.store.scope( - state: \.$destination.leaderboard, action: \.destination.leaderboard - ), - destination: LeaderboardView.init(store:) - ) + item: $store.scope(state: \.destination?.leaderboard, action: \.destination.leaderboard) + ) { store in + LeaderboardView(store: store) + } } } diff --git a/Sources/HomeFeature/NagBanner.swift b/Sources/HomeFeature/NagBanner.swift index 8b4fa66a..deb0b93f 100644 --- a/Sources/HomeFeature/NagBanner.swift +++ b/Sources/HomeFeature/NagBanner.swift @@ -4,8 +4,9 @@ import UpgradeInterstitialFeature @Reducer public struct NagBanner { + @ObservableState public struct State: Equatable { - @PresentationState var upgradeInterstitial: UpgradeInterstitial.State? = nil + @Presents var upgradeInterstitial: UpgradeInterstitial.State? = nil public init(upgradeInterstitial: UpgradeInterstitial.State? = nil) { self.upgradeInterstitial = upgradeInterstitial @@ -44,34 +45,33 @@ public struct NagBanner { } public struct NagBannerView: View { - let store: StoreOf + @Bindable var store: StoreOf public init(store: StoreOf) { self.store = store } public var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - Button { - viewStore.send(.tapped) - } label: { - Marquee(duration: TimeInterval(messages.count) * 9) { - ForEach(messages, id: \.self) { message in - Text(message) - .adaptiveFont(.matterMedium, size: 14) - .foregroundColor(.isowordsRed) - } + Button { + store.send(.tapped) + } label: { + Marquee(duration: TimeInterval(messages.count) * 9) { + ForEach(messages, id: \.self) { message in + Text(message) + .adaptiveFont(.matterMedium, size: 14) + .foregroundColor(.isowordsRed) } } - .buttonStyle(PlainButtonStyle()) - .frame(maxWidth: .infinity, alignment: .center) - .frame(height: 56) - .background(Color.white.edgesIgnoringSafeArea(.bottom)) } + .buttonStyle(PlainButtonStyle()) + .frame(maxWidth: .infinity, alignment: .center) + .frame(height: 56) + .background(Color.white.edgesIgnoringSafeArea(.bottom)) .sheet( - store: self.store.scope(state: \.$upgradeInterstitial, action: \.upgradeInterstitial), - content: UpgradeInterstitialView.init(store:) - ) + item: $store.scope(state: \.upgradeInterstitial, action: \.upgradeInterstitial) + ) { store in + UpgradeInterstitialView(store: store) + } } } diff --git a/Sources/HomeFeature/StartNewGameView.swift b/Sources/HomeFeature/StartNewGameView.swift index 9289e3d4..0bbe67e2 100644 --- a/Sources/HomeFeature/StartNewGameView.swift +++ b/Sources/HomeFeature/StartNewGameView.swift @@ -6,11 +6,7 @@ import SwiftUI struct StartNewGameView: View { @Environment(\.colorScheme) var colorScheme - let store: StoreOf - - init(store: StoreOf) { - self.store = store - } + @Bindable var store: StoreOf var body: some View { VStack(alignment: .leading) { @@ -20,7 +16,7 @@ struct StartNewGameView: View { .padding(.vertical) Button { - self.store.send(.soloButtonTapped) + store.send(.soloButtonTapped) } label: { HStack { Text("Solo") @@ -36,7 +32,7 @@ struct StartNewGameView: View { ) Button { - self.store.send(.multiplayerButtonTapped) + store.send(.multiplayerButtonTapped) } label: { HStack { Text("Multiplayer") @@ -52,13 +48,15 @@ struct StartNewGameView: View { ) } .navigationDestination( - store: self.store.scope(state: \.$destination.solo, action: \.destination.solo), - destination: SoloView.init(store:) - ) + item: $store.scope(state: \.destination?.solo, action: \.destination.solo) + ) { store in + SoloView(store: store) + } .navigationDestination( - store: self.store.scope(state: \.$destination.multiplayer, action: \.destination.multiplayer), - destination: MultiplayerView.init(store:) - ) + item: $store.scope(state: \.destination?.multiplayer, action: \.destination.multiplayer) + ) { store in + MultiplayerView(store: store) + } } } From 7a12f8bcd5d6cb3a36173427c3996ac6b0dac93a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:00:56 -0800 Subject: [PATCH 04/61] wip --- Sources/ChangelogFeature/ChangelogView.swift | 57 ++++++++------------ 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/Sources/ChangelogFeature/ChangelogView.swift b/Sources/ChangelogFeature/ChangelogView.swift index 06713550..1e7da081 100644 --- a/Sources/ChangelogFeature/ChangelogView.swift +++ b/Sources/ChangelogFeature/ChangelogView.swift @@ -10,6 +10,7 @@ import UIApplicationClient @Reducer public struct ChangelogReducer { + @ObservableState public struct State: Equatable { public var changelog: IdentifiedArrayOf public var currentBuild: Build.Number @@ -116,54 +117,40 @@ public struct ChangelogReducer { public struct ChangelogView: View { let store: StoreOf - struct ViewState: Equatable { - let currentBuild: Build.Number - let isUpdateButtonVisible: Bool - - init(state: ChangelogReducer.State) { - self.currentBuild = state.currentBuild - self.isUpdateButtonVisible = state.isUpdateButtonVisible - } - } - - public init( - store: StoreOf - ) { + public init(store: StoreOf) { self.store = store } public var body: some View { - WithViewStore(self.store, observe: ViewState.init) { viewStore in - ScrollView { - VStack(alignment: .leading) { - if viewStore.isUpdateButtonVisible { - HStack { - Spacer() - Button("Update") { - viewStore.send(.updateButtonTapped) - } - .buttonStyle(ActionButtonStyle()) + ScrollView { + VStack(alignment: .leading) { + if store.isUpdateButtonVisible { + HStack { + Spacer() + Button("Update") { + store.send(.updateButtonTapped) } + .buttonStyle(ActionButtonStyle()) } + } - Text("What's new?") - .font(.largeTitle) + Text("What's new?") + .font(.largeTitle) - ForEachStore(self.store.scope(state: \.whatsNew, action: \.changelog)) { store in - ChangeView(currentBuild: viewStore.currentBuild, store: store) - } + ForEach(store.scope(state: \.whatsNew, action: \.changelog)) { store in + ChangeView(currentBuild: self.store.currentBuild, store: store) + } - Text("Past updates") - .font(.largeTitle) + Text("Past updates") + .font(.largeTitle) - ForEachStore(self.store.scope(state: \.pastUpdates, action: \.changelog)) { store in - ChangeView(currentBuild: viewStore.currentBuild, store: store) - } + ForEach(store.scope(state: \.pastUpdates, action: \.changelog)) { store in + ChangeView(currentBuild: self.store.currentBuild, store: store) } - .padding() } - .task { await viewStore.send(.task).finish() } + .padding() } + .task { await store.send(.task).finish() } } } From e67c0c9d641e475f600e6e5a3856a605436ecb04 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:04:20 -0800 Subject: [PATCH 05/61] wip --- Sources/SoloFeature/SoloView.swift | 99 ++++++++++++++---------------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/Sources/SoloFeature/SoloView.swift b/Sources/SoloFeature/SoloView.swift index 0fc5f7e5..dfae4583 100644 --- a/Sources/SoloFeature/SoloView.swift +++ b/Sources/SoloFeature/SoloView.swift @@ -8,6 +8,7 @@ import SwiftUI @Reducer public struct Solo { + @ObservableState public struct State: Equatable { var inProgressGame: InProgressGame? @@ -53,67 +54,57 @@ public struct SoloView: View { @Environment(\.colorScheme) var colorScheme let store: StoreOf - struct ViewState: Equatable { - let currentScore: Int? - - init(state: Solo.State) { - self.currentScore = state.inProgressGame?.currentScore - } - } - public init(store: StoreOf) { self.store = store } public var body: some View { - WithViewStore(self.store, observe: ViewState.init) { viewStore in - VStack { - Spacer() - .frame(maxHeight: .grid(16)) - - VStack(spacing: -8) { - Text("Kill time") - Text("and refine") - Text("your skills") - } - .font(.custom(.matter, size: self.adaptiveSize.pad(48, by: 2))) - .multilineTextAlignment(.center) - - Spacer() - - LazyVGrid( - columns: [ - GridItem(.flexible(), spacing: .grid(4)), - GridItem(.flexible()), - ] - ) { - GameButton( - title: Text("Timed"), - icon: Image(systemName: "clock.fill"), - color: .solo, - inactiveText: nil, - isLoading: false, - resumeText: nil, - action: { viewStore.send(.gameButtonTapped(.timed), animation: .default) } - ) - - GameButton( - title: Text("Unlimited"), - icon: Image(systemName: "infinity"), - color: .solo, - inactiveText: nil, - isLoading: false, - resumeText: viewStore.currentScore.flatMap { - $0 > 0 ? Text("\($0) points") : nil - }, - action: { viewStore.send(.gameButtonTapped(.unlimited), animation: .default) } - ) - } + VStack { + Spacer() + .frame(maxHeight: .grid(16)) + + VStack(spacing: -8) { + Text("Kill time") + Text("and refine") + Text("your skills") + } + .font(.custom(.matter, size: self.adaptiveSize.pad(48, by: 2))) + .multilineTextAlignment(.center) + + Spacer() + + LazyVGrid( + columns: [ + GridItem(.flexible(), spacing: .grid(4)), + GridItem(.flexible()), + ] + ) { + GameButton( + title: Text("Timed"), + icon: Image(systemName: "clock.fill"), + color: .solo, + inactiveText: nil, + isLoading: false, + resumeText: nil, + action: { store.send(.gameButtonTapped(.timed), animation: .default) } + ) + + GameButton( + title: Text("Unlimited"), + icon: Image(systemName: "infinity"), + color: .solo, + inactiveText: nil, + isLoading: false, + resumeText: (store.inProgressGame?.currentScore).flatMap { + $0 > 0 ? Text("\($0) points") : nil + }, + action: { store.send(.gameButtonTapped(.unlimited), animation: .default) } + ) } - .adaptivePadding(.vertical) - .screenEdgePadding(.horizontal) - .task { await viewStore.send(.task).finish() } } + .adaptivePadding(.vertical) + .screenEdgePadding(.horizontal) + .task { await store.send(.task).finish() } .navigationStyle( backgroundColor: self.colorScheme == .dark ? .isowordsBlack : .solo, foregroundColor: self.colorScheme == .dark ? .solo : .isowordsBlack, From 5b24ecf335d463805ceb641e27b52effb5ff68f0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:07:51 -0800 Subject: [PATCH 06/61] wip --- Sources/VocabFeature/Vocab.swift | 62 ++++++++++++++++---------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/Sources/VocabFeature/Vocab.swift b/Sources/VocabFeature/Vocab.swift index c34742da..77a312b4 100644 --- a/Sources/VocabFeature/Vocab.swift +++ b/Sources/VocabFeature/Vocab.swift @@ -7,6 +7,7 @@ import SwiftUI public struct Vocab: Reducer { @Reducer public struct Destination: Reducer { + @ObservableState public enum State: Equatable { case cubePreview(CubePreview.State) } @@ -22,8 +23,9 @@ public struct Vocab: Reducer { } } + @ObservableState public struct State: Equatable { - @PresentationState var destination: Destination.State? + @Presents var destination: Destination.State? var isAnimationReduced: Bool var vocab: LocalDatabaseClient.Vocab? @@ -122,7 +124,7 @@ public struct Vocab: Reducer { } public struct VocabView: View { - public let store: StoreOf + @Bindable var store: StoreOf public init(store: StoreOf) { self.store = store @@ -130,42 +132,38 @@ public struct VocabView: View { public var body: some View { VStack { - IfLetStore(self.store.scope(state: \.vocab, action: \.self)) { vocabStore in - WithViewStore(vocabStore, observe: { $0 }) { vocabViewStore in - List { - ForEach(vocabViewStore.words, id: \.letters) { word in - Button { - vocabViewStore.send(.wordTapped(word)) - } label: { - HStack { - HStack(alignment: .top, spacing: 0) { - Text(word.letters.capitalized) - .adaptiveFont(.matterMedium, size: 20) - - Text("\(word.score)") - .padding(.top, -4) - .adaptiveFont(.matterMedium, size: 14) - } - - Spacer() - - if word.playCount > 1 { - Text("(\(word.playCount)x)") - } + if let words = store.vocab?.words { + List { + ForEach(words, id: \.letters) { word in + Button { + store.send(.wordTapped(word)) + } label: { + HStack { + HStack(alignment: .top, spacing: 0) { + Text(word.letters.capitalized) + .adaptiveFont(.matterMedium, size: 20) + + Text("\(word.score)") + .padding(.top, -4) + .adaptiveFont(.matterMedium, size: 14) + } + + Spacer() + + if word.playCount > 1 { + Text("(\(word.playCount)x)") } } } } } } - .task { await self.store.send(.task).finish() } - .sheet( - store: self.store.scope( - state: \.$destination.cubePreview, - action: \.destination.cubePreview - ), - content: CubePreviewView.init(store:) - ) + } + .task { await store.send(.task).finish() } + .sheet( + item: $store.scope(state: \.destination?.cubePreview, action: \.destination.cubePreview) + ) { store in + CubePreviewView(store: store) } .adaptiveFont(.matterMedium, size: 16) .navigationStyle(title: Text("Words Found")) From 015932b3fb71ed650627572bcf65ec798b1e1037 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:09:14 -0800 Subject: [PATCH 07/61] wip --- .../UpgradeInterstitialView.swift | 181 +++++++++--------- 1 file changed, 90 insertions(+), 91 deletions(-) diff --git a/Sources/UpgradeInterstitialFeature/UpgradeInterstitialView.swift b/Sources/UpgradeInterstitialFeature/UpgradeInterstitialView.swift index 11973256..7683c040 100644 --- a/Sources/UpgradeInterstitialFeature/UpgradeInterstitialView.swift +++ b/Sources/UpgradeInterstitialFeature/UpgradeInterstitialView.swift @@ -15,6 +15,7 @@ public enum GameContext: String, Codable { @Reducer public struct UpgradeInterstitial { + @ObservableState public struct State: Equatable { public var fullGameProduct: StoreKitClient.Product? public var isDismissable: Bool @@ -164,118 +165,116 @@ public struct UpgradeInterstitialView: View { } public var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack { VStack { - VStack { - if !viewStore.isDismissable - && viewStore.secondsPassedCount < viewStore.upgradeInterstitialDuration - { - Text("\(viewStore.upgradeInterstitialDuration - viewStore.secondsPassedCount)s") - .animation(nil) - .multilineTextAlignment(.center) - .adaptiveFont(.matterMedium, size: 16) { $0.monospacedDigit() } - .adaptivePadding(.bottom) - .transition(.opacity) - } + if !store.isDismissable + && store.secondsPassedCount < store.upgradeInterstitialDuration + { + Text("\(store.upgradeInterstitialDuration - store.secondsPassedCount)s") + .animation(nil) + .multilineTextAlignment(.center) + .adaptiveFont(.matterMedium, size: 16) { $0.monospacedDigit() } + .adaptivePadding(.bottom) + .transition(.opacity) + } - VStack(spacing: 32) { - (Text("A personal\nappeal from\nthe creators\nof ") - + Text("isowords").fontWeight(.medium)) - .multilineTextAlignment(.center) - .adaptiveFont(.matter, size: 35) - .fixedSize() + VStack(spacing: 32) { + (Text("A personal\nappeal from\nthe creators\nof ") + + Text("isowords").fontWeight(.medium)) + .multilineTextAlignment(.center) + .adaptiveFont(.matter, size: 35) + .fixedSize() - Text( + Text( """ Hello! We could put an ad here, but we chose not to because ads suck. But also, keeping \ this game running costs money. So if you can, please purchase the full version and help \ support the development of new features and remove these annoying prompts! """ - ) - .minimumScaleFactor(0.2) - .multilineTextAlignment(.center) - .adaptiveFont(.matter, size: 16) - } - .adaptivePadding() - - Spacer() + ) + .minimumScaleFactor(0.2) + .multilineTextAlignment(.center) + .adaptiveFont(.matter, size: 16) } - .applying { - if self.colorScheme == .dark { - $0.foreground( - LinearGradient( - gradient: Gradient(colors: [.hex(0xF3EBA4), .hex(0xE1665B)]), - startPoint: .top, - endPoint: .bottom - ) + .adaptivePadding() + + Spacer() + } + .applying { + if self.colorScheme == .dark { + $0.foreground( + LinearGradient( + gradient: Gradient(colors: [.hex(0xF3EBA4), .hex(0xE1665B)]), + startPoint: .top, + endPoint: .bottom ) - } else { - $0 - } + ) + } else { + $0 } + } - VStack(spacing: 24) { - Button { - viewStore.send(.upgradeButtonTapped, animation: .default) - } label: { - HStack(spacing: .grid(2)) { - if viewStore.isPurchasing { - ProgressView() - .progressViewStyle( - CircularProgressViewStyle( - tint: self.colorScheme == .dark ? .isowordsBlack : .hex(0xE1665B) - ) + VStack(spacing: 24) { + Button { + store.send(.upgradeButtonTapped, animation: .default) + } label: { + HStack(spacing: .grid(2)) { + if store.isPurchasing { + ProgressView() + .progressViewStyle( + CircularProgressViewStyle( + tint: self.colorScheme == .dark ? .isowordsBlack : .hex(0xE1665B) ) - } - if let fullGameProduct = viewStore.fullGameProduct { - Text("Upgrade for \(cost(product: fullGameProduct))") - } else { - Text("Upgrade") - } + ) + } + if let fullGameProduct = store.fullGameProduct { + Text("Upgrade for \(cost(product: fullGameProduct))") + } else { + Text("Upgrade") } - .frame(maxWidth: .infinity) } - .buttonStyle( - ActionButtonStyle( - backgroundColor: self.colorScheme == .dark ? .hex(0xE1665B) : .isowordsBlack, - foregroundColor: self.colorScheme == .dark ? .isowordsBlack : .hex(0xE1665B) - ) + .frame(maxWidth: .infinity) + } + .buttonStyle( + ActionButtonStyle( + backgroundColor: self.colorScheme == .dark ? .hex(0xE1665B) : .isowordsBlack, + foregroundColor: self.colorScheme == .dark ? .isowordsBlack : .hex(0xE1665B) ) - .disabled(viewStore.isPurchasing) - - if viewStore.isDismissable - || viewStore.secondsPassedCount >= viewStore.upgradeInterstitialDuration - { - Button { - viewStore.send(.maybeLaterButtonTapped, animation: .default) - } label: { - Text("Maybe later") - .foregroundColor(self.colorScheme == .dark ? .hex(0xE1665B) : .isowordsBlack) - } - .foregroundColor(.isowordsBlack) - .adaptiveFont(.matterMedium, size: 14) - .transition(.opacity) + ) + .disabled(store.isPurchasing) + + if store.isDismissable + || store.secondsPassedCount >= store.upgradeInterstitialDuration + { + Button { + store.send(.maybeLaterButtonTapped, animation: .default) + } label: { + Text("Maybe later") + .foregroundColor(self.colorScheme == .dark ? .hex(0xE1665B) : .isowordsBlack) } + .foregroundColor(.isowordsBlack) + .adaptiveFont(.matterMedium, size: 14) + .transition(.opacity) } } - .adaptivePadding() - .task { await viewStore.send(.task).finish() } - .applying { - if self.colorScheme == .dark { - $0.background( - Color.isowordsBlack - .ignoresSafeArea() - ) - } else { - $0.background( - LinearGradient( - gradient: Gradient(colors: [.hex(0xF3EBA4), .hex(0xE1665B)]), - startPoint: .top, - endPoint: .bottom - ) + } + .adaptivePadding() + .task { await store.send(.task).finish() } + .applying { + if self.colorScheme == .dark { + $0.background( + Color.isowordsBlack .ignoresSafeArea() + ) + } else { + $0.background( + LinearGradient( + gradient: Gradient(colors: [.hex(0xF3EBA4), .hex(0xE1665B)]), + startPoint: .top, + endPoint: .bottom ) - } + .ignoresSafeArea() + ) } } } From 57d676dec566bb3ccf450894727113e2fef69eda Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:13:41 -0800 Subject: [PATCH 08/61] wip --- .../MultiplayerFeature/MultiplayerView.swift | 31 +++++++---------- .../OnboardingFeature/OnboardingView.swift | 34 +++++++------------ 2 files changed, 25 insertions(+), 40 deletions(-) diff --git a/Sources/MultiplayerFeature/MultiplayerView.swift b/Sources/MultiplayerFeature/MultiplayerView.swift index 7134619f..34aabb7d 100644 --- a/Sources/MultiplayerFeature/MultiplayerView.swift +++ b/Sources/MultiplayerFeature/MultiplayerView.swift @@ -6,6 +6,7 @@ import TcaHelpers public struct Multiplayer { @Reducer public struct Destination { + @ObservableState public enum State: Equatable { case pastGames(PastGames.State) } @@ -21,8 +22,9 @@ public struct Multiplayer { } } + @ObservableState public struct State: Equatable { - @PresentationState public var destination: Destination.State? + @Presents public var destination: Destination.State? public var hasPastGames: Bool public init( @@ -73,20 +75,10 @@ public struct Multiplayer { public struct MultiplayerView: View { @Environment(\.adaptiveSize) var adaptiveSize @Environment(\.colorScheme) var colorScheme - let store: StoreOf - @ObservedObject var viewStore: ViewStore + @Bindable var store: StoreOf public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) - } - - struct ViewState: Equatable { - let hasPastGames: Bool - - init(state: Multiplayer.State) { - self.hasPastGames = state.hasPastGames - } } public var body: some View { @@ -111,7 +103,7 @@ public struct MultiplayerView: View { Spacer() Button { - self.viewStore.send(.startButtonTapped) + store.send(.startButtonTapped) } label: { VStack(spacing: 20) { Image(systemName: "person.2.fill") @@ -126,11 +118,11 @@ public struct MultiplayerView: View { } .buttonStyle(PlainButtonStyle()) .adaptivePadding(.vertical) - .adaptivePadding(.bottom, .grid(self.viewStore.hasPastGames ? 0 : 8)) + .adaptivePadding(.bottom, .grid(store.hasPastGames ? 0 : 8)) - if self.viewStore.hasPastGames { + if store.hasPastGames { Button { - self.viewStore.send(.pastGamesButtonTapped) + store.send(.pastGamesButtonTapped) } label: { HStack { Text("View past games") @@ -149,9 +141,10 @@ public struct MultiplayerView: View { } } .navigationDestination( - store: self.store.scope(state: \.$destination.pastGames, action: \.destination.pastGames), - destination: PastGamesView.init(store:) - ) + item: $store.scope(state: \.destination?.pastGames, action: \.destination.pastGames) + ) { store in + PastGamesView(store: store) + } .navigationStyle( backgroundColor: self.colorScheme == .dark ? .isowordsBlack : .multiplayer, foregroundColor: self.colorScheme == .dark ? .multiplayer : .isowordsBlack, diff --git a/Sources/OnboardingFeature/OnboardingView.swift b/Sources/OnboardingFeature/OnboardingView.swift index b8625bba..a2896c34 100644 --- a/Sources/OnboardingFeature/OnboardingView.swift +++ b/Sources/OnboardingFeature/OnboardingView.swift @@ -15,8 +15,9 @@ import UserDefaultsClient @Reducer public struct Onboarding { + @ObservableState public struct State: Equatable { - @PresentationState public var alert: AlertState? + @Presents public var alert: AlertState? public var game: Game.State public var presentationStyle: PresentationStyle public var step: Step @@ -33,6 +34,10 @@ public struct Onboarding { self.step = step } + fileprivate var isSkipButtonVisible: Bool { + self.step != Onboarding.State.Step.allCases.last + } + fileprivate var cubeScene: CubeSceneView.ViewState { var viewState = CubeSceneView.ViewState(game: self.game, nub: nil) @@ -390,44 +395,32 @@ public struct Onboarding { public struct OnboardingView: View { @Environment(\.colorScheme) var colorScheme let store: StoreOf - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - let isSkipButtonVisible: Bool - let step: Onboarding.State.Step - - init(state: Onboarding.State) { - self.isSkipButtonVisible = state.step != Onboarding.State.Step.allCases.last - self.step = state.step - } - } public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) } public var body: some View { ZStack(alignment: .topTrailing) { - CubeView(store: self.store.scope(state: \.cubeScene, action: \.game.cubeScene)) - .opacity(viewStore.step.isFullscreen ? 0 : 1) + CubeView(store: store.scope(state: \.cubeScene, action: \.game.cubeScene)) + .opacity(store.step.isFullscreen ? 0 : 1) - OnboardingStepView(store: self.store) + OnboardingStepView(store: store) - if viewStore.isSkipButtonVisible { - Button("Skip") { viewStore.send(.skipButtonTapped, animation: .default) } + if store.isSkipButtonVisible { + Button("Skip") { store.send(.skipButtonTapped, animation: .default) } .adaptiveFont(.matterMedium, size: 18) .buttonStyle(PlainButtonStyle()) .padding(.horizontal) .foregroundColor( self.colorScheme == .dark - ? viewStore.step.color + ? store.step.color : Color.isowordsBlack ) } } .background( - (self.colorScheme == .dark ? Color.isowordsBlack : viewStore.step.color) + (self.colorScheme == .dark ? Color.isowordsBlack : store.step.color) .ignoresSafeArea() ) } @@ -474,7 +467,6 @@ private enum CancelID { static var previews: some View { OnboardingView( store: Store(initialState: .init(presentationStyle: .firstLaunch)) { - } ) } From 816d53fcc2764a05f0f40664b2fe122744d1300e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:19:45 -0800 Subject: [PATCH 09/61] wip --- .../AccessibilitySettingsView.swift | 19 ++---- .../AppearanceSettingsView.swift | 12 +--- .../DeveloperSettingsView.swift | 12 +--- .../NotificationsSettingsView.swift | 22 ++----- Sources/SettingsFeature/Settings.swift | 7 ++- Sources/SettingsFeature/SettingsView.swift | 58 +++++++------------ .../SettingsFeature/SoundsSettingsView.swift | 25 +++----- 7 files changed, 49 insertions(+), 106 deletions(-) diff --git a/Sources/SettingsFeature/AccessibilitySettingsView.swift b/Sources/SettingsFeature/AccessibilitySettingsView.swift index 40909e15..679b57b2 100644 --- a/Sources/SettingsFeature/AccessibilitySettingsView.swift +++ b/Sources/SettingsFeature/AccessibilitySettingsView.swift @@ -3,19 +3,13 @@ import Styleguide import SwiftUI struct AccessibilitySettingsView: View { - let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf - - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) - } + @Bindable var store: StoreOf var body: some View { SettingsForm { SettingsRow { VStack(alignment: .leading) { - Toggle("Cube motion", isOn: self.viewStore.$userSettings.enableGyroMotion) + Toggle("Cube motion", isOn: $store.userSettings.enableGyroMotion) .adaptiveFont(.matterMedium, size: 16) Text("Use your device’s gyroscope to apply a small amount of motion to the cube.") @@ -26,17 +20,14 @@ struct AccessibilitySettingsView: View { } SettingsRow { VStack(alignment: .leading) { - Toggle("Haptics", isOn: self.viewStore.$userSettings.enableHaptics) + Toggle("Haptics", isOn: $store.userSettings.enableHaptics) .adaptiveFont(.matterMedium, size: 16) } } SettingsRow { VStack(alignment: .leading) { - Toggle( - "Reduce animation", - isOn: self.viewStore.$userSettings.enableReducedAnimation - ) - .adaptiveFont(.matterMedium, size: 16) + Toggle("Reduce animation", isOn: $store.userSettings.enableReducedAnimation) + .adaptiveFont(.matterMedium, size: 16) } } } diff --git a/Sources/SettingsFeature/AppearanceSettingsView.swift b/Sources/SettingsFeature/AppearanceSettingsView.swift index 3bff448e..caff2c9d 100644 --- a/Sources/SettingsFeature/AppearanceSettingsView.swift +++ b/Sources/SettingsFeature/AppearanceSettingsView.swift @@ -4,22 +4,16 @@ import SwiftUI import UserSettingsClient struct AppearanceSettingsView: View { - let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf - - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) - } + @Bindable var store: StoreOf var body: some View { SettingsForm { SettingsSection(title: "Theme") { - ColorSchemePicker(colorScheme: self.viewStore.$userSettings.colorScheme) + ColorSchemePicker(colorScheme: $store.userSettings.colorScheme) } SettingsSection(title: "App Icon", padContents: false) { - AppIconPicker(appIcon: self.viewStore.$userSettings.appIcon.animation()) + AppIconPicker(appIcon: $store.userSettings.appIcon.animation()) } } .navigationStyle(title: Text("Appearance")) diff --git a/Sources/SettingsFeature/DeveloperSettingsView.swift b/Sources/SettingsFeature/DeveloperSettingsView.swift index 09ef7cc4..eefbc274 100644 --- a/Sources/SettingsFeature/DeveloperSettingsView.swift +++ b/Sources/SettingsFeature/DeveloperSettingsView.swift @@ -3,25 +3,19 @@ import Styleguide import SwiftUI struct DeveloperSettingsView: View { - let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf + @Bindable var store: StoreOf @AppStorage(.enableCubeShadow) var enableCubeShadow @AppStorage(.showSceneStatistics) var showSceneStatistics - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(store, observe: { $0 }) - } - var body: some View { SettingsForm { SettingsRow { VStack(alignment: .leading) { Text("API") - Text(self.viewStore.developer.currentBaseUrl.rawValue) + Text(store.developer.currentBaseUrl.rawValue) .adaptiveFont(.matter, size: 14) - Picker("Base URL", selection: self.viewStore.$developer.currentBaseUrl) { + Picker("Base URL", selection: $store.developer.currentBaseUrl) { ForEach(DeveloperSettings.BaseUrl.allCases, id: \.self) { Text($0.description) } diff --git a/Sources/SettingsFeature/NotificationsSettingsView.swift b/Sources/SettingsFeature/NotificationsSettingsView.swift index a080e844..fa0b257d 100644 --- a/Sources/SettingsFeature/NotificationsSettingsView.swift +++ b/Sources/SettingsFeature/NotificationsSettingsView.swift @@ -3,29 +3,22 @@ import Styleguide import SwiftUI struct NotificationsSettingsView: View { - let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf - - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) - } + @Bindable var store: StoreOf var body: some View { SettingsForm { SettingsRow { Toggle( - "Enable notifications", isOn: self.viewStore.$userSettings.enableNotifications.animation() + "Enable notifications", isOn: $store.userSettings.enableNotifications.animation() ) .adaptiveFont(.matterMedium, size: 16) } - if self.viewStore.userSettings.enableNotifications { + if store.userSettings.enableNotifications { SettingsRow { VStack(alignment: .leading, spacing: 16) { Toggle( - "Daily challenge reminders", - isOn: self.viewStore.$userSettings.sendDailyChallengeReminder + "Daily challenge reminders", isOn: $store.userSettings.sendDailyChallengeReminder ) .adaptiveFont(.matterMedium, size: 16) @@ -37,11 +30,8 @@ struct NotificationsSettingsView: View { SettingsRow { VStack(alignment: .leading, spacing: 16) { - Toggle( - "Daily challenge summary", - isOn: self.viewStore.$userSettings.sendDailyChallengeSummary - ) - .adaptiveFont(.matterMedium, size: 16) + Toggle("Daily challenge summary", isOn: $store.userSettings.sendDailyChallengeSummary) + .adaptiveFont(.matterMedium, size: 16) Text("Receive your rank for yesterday’s challenge if you played.") .foregroundColor(.gray) diff --git a/Sources/SettingsFeature/Settings.swift b/Sources/SettingsFeature/Settings.swift index b57aa3a6..35f727a0 100644 --- a/Sources/SettingsFeature/Settings.swift +++ b/Sources/SettingsFeature/Settings.swift @@ -42,17 +42,18 @@ public struct DeveloperSettings: Equatable { @Reducer public struct Settings { + @ObservableState public struct State: Equatable { - @PresentationState public var alert: AlertState? + @Presents public var alert: AlertState? public var buildNumber: Build.Number? - @BindingState public var developer: DeveloperSettings + public var developer: DeveloperSettings public var fullGameProduct: Result? public var fullGamePurchasedAt: Date? public var isPurchasing: Bool public var isRestoring: Bool public var stats: Stats.State public var userNotificationSettings: UserNotificationClient.Notification.Settings? - @BindingState public var userSettings: UserSettings + public var userSettings: UserSettings public struct ProductError: Error, Equatable {} diff --git a/Sources/SettingsFeature/SettingsView.swift b/Sources/SettingsFeature/SettingsView.swift index dbd2f3b8..6f0962ac 100644 --- a/Sources/SettingsFeature/SettingsView.swift +++ b/Sources/SettingsFeature/SettingsView.swift @@ -11,22 +11,7 @@ public struct SettingsView: View { @Environment(\.colorScheme) var colorScheme let navPresentationStyle: NavPresentationStyle @State var isSharePresented = false - let store: StoreOf - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - let buildNumber: Build.Number? - let fullGameProduct: Result? - let isFullGamePurchased: Bool - let isPurchasing: Bool - - init(state: Settings.State) { - self.buildNumber = state.buildNumber - self.fullGameProduct = state.fullGameProduct - self.isFullGamePurchased = state.isFullGamePurchased - self.isPurchasing = state.isPurchasing - } - } + @Bindable var store: StoreOf public init( store: StoreOf, @@ -34,7 +19,6 @@ public struct SettingsView: View { ) { self.navPresentationStyle = navPresentationStyle self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) } public var body: some View { @@ -42,15 +26,15 @@ public struct SettingsView: View { SettingsSection(title: "Support the game", padContents: false) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { - if !self.viewStore.isFullGamePurchased { + if !store.isFullGamePurchased { Group { - if !self.viewStore.isPurchasing, - let fullGameProduct = self.viewStore.fullGameProduct + if !store.isPurchasing, + let fullGameProduct = store.fullGameProduct { switch fullGameProduct { case let .success(product): Button { - self.viewStore.send(.tappedProduct(product), animation: .default) + store.send(.tappedProduct(product), animation: .default) } label: { HStack(alignment: .top, spacing: 0) { Text(product.priceLocale.currencySymbol ?? "$") @@ -81,7 +65,7 @@ public struct SettingsView: View { } Button { - self.viewStore.send(.leaveUsAReviewButtonTapped) + store.send(.leaveUsAReviewButtonTapped) } label: { Image(systemName: "star") .font(.system(size: 40)) @@ -115,33 +99,33 @@ public struct SettingsView: View { } SettingsNavigationLink( - destination: NotificationsSettingsView(store: self.store), + destination: NotificationsSettingsView(store: store), title: "Notifications" ) SettingsNavigationLink( - destination: SoundsSettingsView(store: self.store), + destination: SoundsSettingsView(store: store), title: "Sounds" ) SettingsNavigationLink( - destination: AppearanceSettingsView(store: self.store), + destination: AppearanceSettingsView(store: store), title: "Appearance" ) SettingsNavigationLink( - destination: AccessibilitySettingsView(store: self.store), + destination: AccessibilitySettingsView(store: store), title: "Accessibility" ) SettingsNavigationLink( - destination: StatsView(store: self.store.scope(state: \.stats, action: \.stats)), + destination: StatsView(store: store.scope(state: \.stats, action: \.stats)), title: "Stats" ) SettingsNavigationLink( - destination: PurchasesSettingsView(store: self.store), + destination: PurchasesSettingsView(store: store), title: "Purchases" ) - if self.viewStore.isFullGamePurchased { + if store.isFullGamePurchased { SettingsRow { Button { - self.viewStore.send(.leaveUsAReviewButtonTapped) + store.send(.leaveUsAReviewButtonTapped) } label: { HStack { Text("Leave us a review") @@ -154,17 +138,17 @@ public struct SettingsView: View { } #if DEBUG SettingsNavigationLink( - destination: DeveloperSettingsView(store: self.store), + destination: DeveloperSettingsView(store: store), title: "\(Image(systemName: "hammer.fill")) Developer" ) #endif VStack(spacing: 6) { - if let buildNumber = self.viewStore.buildNumber { + if let buildNumber = store.buildNumber { Text("Build \(buildNumber.rawValue)") } Button { - self.viewStore.send(.reportABugButtonTapped) + store.send(.reportABugButtonTapped) } label: { Text("Report a bug") .underline() @@ -179,11 +163,11 @@ public struct SettingsView: View { foregroundColor: .hex(self.colorScheme == .dark ? 0x7d7d7d : 0x393939), title: Text("Settings"), navPresentationStyle: self.navPresentationStyle, - onDismiss: { self.viewStore.send(.onDismiss) } + onDismiss: { store.send(.onDismiss) } ) - .task { await self.viewStore.send(.task).finish() } - .alert(store: self.store.scope(state: \.$alert, action: \.alert)) - .sheet(isPresented: self.$isSharePresented) { + .task { await store.send(.task).finish() } + .alert($store.scope(state: \.alert, action: \.alert)) + .sheet(isPresented: $isSharePresented) { ActivityView(activityItems: [URL(string: "https://www.isowords.xyz")!]) .ignoresSafeArea() } diff --git a/Sources/SettingsFeature/SoundsSettingsView.swift b/Sources/SettingsFeature/SoundsSettingsView.swift index e7069084..350aa560 100644 --- a/Sources/SettingsFeature/SoundsSettingsView.swift +++ b/Sources/SettingsFeature/SoundsSettingsView.swift @@ -3,13 +3,7 @@ import Styleguide import SwiftUI struct SoundsSettingsView: View { - let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf - - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) - } + @Bindable var store: StoreOf var body: some View { SettingsForm { @@ -18,12 +12,10 @@ struct SoundsSettingsView: View { Text("Music volume") VStack { - Slider( - value: self.viewStore.$userSettings.musicVolume.animation(), in: 0...1 - ) - .accentColor(.isowordsOrange) + Slider(value: $store.userSettings.musicVolume.animation(), in: 0...1) + .accentColor(.isowordsOrange) - if self.viewStore.userSettings.musicVolume <= 0 { + if store.userSettings.musicVolume <= 0 { Text("Music is off") .foregroundColor(.gray) .adaptiveFont(.matterMedium, size: 14) @@ -38,13 +30,10 @@ struct SoundsSettingsView: View { Text("Sound FX volume") VStack { - Slider( - value: self.viewStore.$userSettings.soundEffectsVolume.animation(), - in: 0...1 - ) - .accentColor(.isowordsOrange) + Slider(value: $store.userSettings.soundEffectsVolume.animation(), in: 0...1) + .accentColor(.isowordsOrange) - if self.viewStore.userSettings.soundEffectsVolume <= 0 { + if store.userSettings.soundEffectsVolume <= 0 { Text("Sound FX are off") .foregroundColor(.gray) .adaptiveFont(.matterMedium, size: 14) From 286618f29f540a0b786134b7653c629b7c59045b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:21:40 -0800 Subject: [PATCH 10/61] wip --- Sources/StatsFeature/StatsFeature.swift | 31 +++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Sources/StatsFeature/StatsFeature.swift b/Sources/StatsFeature/StatsFeature.swift index e9a2c08c..7a3cc5ef 100644 --- a/Sources/StatsFeature/StatsFeature.swift +++ b/Sources/StatsFeature/StatsFeature.swift @@ -8,6 +8,7 @@ import VocabFeature public struct Stats { @Reducer public struct Destination { + @ObservableState public enum State: Equatable { case vocab(Vocab.State) } @@ -23,9 +24,10 @@ public struct Stats { } } + @ObservableState public struct State: Equatable { public var averageWordLength: Double? - @PresentationState public var destination: Destination.State? + @Presents public var destination: Destination.State? public var gamesPlayed: Int public var highestScoringWord: LocalDatabaseClient.Stats.Word? public var highScoreTimed: Int? @@ -117,12 +119,10 @@ public struct Stats { } public struct StatsView: View { - let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf + @Bindable var store: StoreOf public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(store, observe: { $0 }) } public var body: some View { @@ -131,7 +131,7 @@ public struct StatsView: View { HStack { Text("Games played") Spacer() - Text("\(self.viewStore.gamesPlayed)") + Text("\(store.gamesPlayed)") .foregroundColor(.isowordsOrange) } .adaptiveFont(.matterMedium, size: 16) @@ -147,7 +147,7 @@ public struct StatsView: View { Text("Timed") Spacer() Group { - if let highScoreTimed = self.viewStore.highScoreTimed { + if let highScoreTimed = store.highScoreTimed { Text("\(highScoreTimed)") } else { Text("none") @@ -160,7 +160,7 @@ public struct StatsView: View { Text("Unlimited") Spacer() Group { - if let highScoreUnlimited = self.viewStore.highScoreUnlimited { + if let highScoreUnlimited = store.highScoreUnlimited { Text("\(highScoreUnlimited)") } else { Text("none") @@ -176,13 +176,13 @@ public struct StatsView: View { SettingsRow { Button { - self.viewStore.send(.vocabButtonTapped) + store.send(.vocabButtonTapped) } label: { HStack { Text("Words found") Spacer() Group { - Text("\(self.viewStore.wordsFound)") + Text("\(store.wordsFound)") Image(systemName: "arrow.right") } .foregroundColor(.isowordsOrange) @@ -193,7 +193,7 @@ public struct StatsView: View { .buttonStyle(PlainButtonStyle()) } - if let highestScoringWord = self.viewStore.highestScoringWord { + if let highestScoringWord = store.highestScoringWord { SettingsRow { VStack(alignment: .trailing, spacing: 12) { HStack { @@ -218,17 +218,18 @@ public struct StatsView: View { HStack { Text("Time played") Spacer() - Text(timePlayed(seconds: self.viewStore.secondsPlayed)) + Text(timePlayed(seconds: store.secondsPlayed)) .foregroundColor(.isowordsOrange) } .adaptiveFont(.matterMedium, size: 16) } } - .task { await self.viewStore.send(.task).finish() } + .task { await store.send(.task).finish() } .navigationDestination( - store: self.store.scope(state: \.$destination.vocab, action: \.destination.vocab), - destination: VocabView.init(store:) - ) + item: $store.scope(state: \.destination?.vocab, action: \.destination.vocab) + ) { store in + VocabView(store: store) + } .navigationStyle(title: Text("Stats")) } } From 49df034f108f62a5d534035a9cdaa59c70241f83 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:22:30 -0800 Subject: [PATCH 11/61] wip --- .../PurchasesSettingsView.swift | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Sources/SettingsFeature/PurchasesSettingsView.swift b/Sources/SettingsFeature/PurchasesSettingsView.swift index fb28c2e5..5c33ddd8 100644 --- a/Sources/SettingsFeature/PurchasesSettingsView.swift +++ b/Sources/SettingsFeature/PurchasesSettingsView.swift @@ -4,16 +4,10 @@ import SwiftUI struct PurchasesSettingsView: View { let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf - - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(store, observe: { $0 }) - } var body: some View { SettingsForm { - if let fullGamePurchasedAt = self.viewStore.fullGamePurchasedAt { + if let fullGamePurchasedAt = store.fullGamePurchasedAt { VStack(alignment: .leading, spacing: 16) { Text("🎉") .font(.system(size: 40)) @@ -28,14 +22,14 @@ struct PurchasesSettingsView: View { .continuousCornerRadius(12) .padding() } else { - if !self.viewStore.isPurchasing, - let fullGameProduct = self.viewStore.fullGameProduct + if !store.isPurchasing, + let fullGameProduct = store.fullGameProduct { switch fullGameProduct { case let .success(product): SettingsRow { Button { - self.viewStore.send(.tappedProduct(product), animation: .default) + store.send(.tappedProduct(product), animation: .default) } label: { Text("Upgrade") .foregroundColor(.isowordsOrange) @@ -55,10 +49,10 @@ struct PurchasesSettingsView: View { } } - if !self.viewStore.isRestoring { + if !store.isRestoring { SettingsRow { Button { - self.viewStore.send(.restoreButtonTapped, animation: .default) + store.send(.restoreButtonTapped, animation: .default) } label: { Text("Restore purchases") .foregroundColor(.isowordsOrange) From 0c5270b18eb8ebf5d2802f80822197599df2e7f2 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:24:45 -0800 Subject: [PATCH 12/61] wip --- Sources/LeaderboardFeature/Leaderboard.swift | 49 ++++++++++---------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/Sources/LeaderboardFeature/Leaderboard.swift b/Sources/LeaderboardFeature/Leaderboard.swift index 40ec3d35..248242cc 100644 --- a/Sources/LeaderboardFeature/Leaderboard.swift +++ b/Sources/LeaderboardFeature/Leaderboard.swift @@ -33,6 +33,7 @@ public enum LeaderboardScope: CaseIterable, Equatable { public struct Leaderboard { @Reducer public struct Destination { + @ObservableState public enum State: Equatable { case cubePreview(CubePreview.State) } @@ -48,8 +49,9 @@ public struct Leaderboard { } } + @ObservableState public struct State: Equatable { - @PresentationState public var destination: Destination.State? + @Presents public var destination: Destination.State? public var scope: LeaderboardScope = .games public var solo: LeaderboardResults.State = .init(timeScope: .lastWeek) public var vocab: LeaderboardResults.State = .init(timeScope: .lastWeek) @@ -155,12 +157,10 @@ public struct Leaderboard { public struct LeaderboardView: View { @Environment(\.colorScheme) var colorScheme - let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf + @Bindable var store: StoreOf public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) } public var body: some View { @@ -168,11 +168,11 @@ public struct LeaderboardView: View { HStack { ForEach(LeaderboardScope.allCases, id: \.self) { scope in Button { - self.viewStore.send(.scopeTapped(scope), animation: .default) + store.send(.scopeTapped(scope), animation: .default) } label: { Text(scope.title) - .foregroundColor(self.viewStore.state.scope == scope ? scope.color : nil) - .opacity(self.viewStore.state.scope == scope ? 1 : 0.3) + .foregroundColor(store.state.scope == scope ? scope.color : nil) + .opacity(store.state.scope == scope ? 1 : 0.3) } } } @@ -181,22 +181,22 @@ public struct LeaderboardView: View { .screenEdgePadding(.horizontal) Group { - switch self.viewStore.state.scope { + switch store.state.scope { case .games: LeaderboardResultsView( - store: self.store.scope(state: \.solo, action: \.solo), + store: store.scope(state: \.solo, action: \.solo), title: Text("Solo"), - subtitle: Text("\(self.viewStore.solo.resultEnvelope?.outOf ?? 0) players"), + subtitle: Text("\(store.solo.resultEnvelope?.outOf ?? 0) players"), isFilterable: true, color: .isowordsOrange, - timeScopeLabel: Text(self.viewStore.solo.timeScope.displayTitle), + timeScopeLabel: Text(store.solo.timeScope.displayTitle), timeScopeMenu: VStack(alignment: .trailing, spacing: .grid(2)) { ForEach([TimeScope.lastDay, .lastWeek, .allTime], id: \.self) { scope in Button(scope.displayTitle) { - self.viewStore.send(.solo(.timeScopeChanged(scope)), animation: .default) + store.send(.solo(.timeScopeChanged(scope)), animation: .default) } - .disabled(self.viewStore.solo.timeScope == scope) - .opacity(self.viewStore.solo.timeScope == scope ? 0.3 : 1) + .disabled(store.solo.timeScope == scope) + .opacity(store.solo.timeScope == scope ? 0.3 : 1) } .padding(.leading, .grid(12)) } @@ -204,21 +204,21 @@ public struct LeaderboardView: View { case .vocab: LeaderboardResultsView( - store: self.store.scope(state: \.vocab, action: \.vocab), - title: (self.viewStore.vocab.resultEnvelope?.outOf).flatMap { + store: store.scope(state: \.vocab, action: \.vocab), + title: (store.vocab.resultEnvelope?.outOf).flatMap { $0 == 0 ? nil : Text("\($0) words") }, subtitle: nil, isFilterable: false, color: .isowordsRed, - timeScopeLabel: Text(self.viewStore.vocab.timeScope.displayTitle), + timeScopeLabel: Text(store.vocab.timeScope.displayTitle), timeScopeMenu: VStack(alignment: .trailing, spacing: .grid(2)) { ForEach([TimeScope.lastDay, .lastWeek, .allTime, .interesting], id: \.self) { scope in Button(scope.displayTitle) { - self.viewStore.send(.vocab(.timeScopeChanged(scope)), animation: .default) + store.send(.vocab(.timeScopeChanged(scope)), animation: .default) } - .disabled(self.viewStore.vocab.timeScope == scope) - .opacity(self.viewStore.vocab.timeScope == scope ? 0.3 : 1) + .disabled(store.vocab.timeScope == scope) + .opacity(store.vocab.timeScope == scope ? 0.3 : 1) } .padding(.leading, .grid(12)) } @@ -233,15 +233,16 @@ public struct LeaderboardView: View { .navigationStyle( foregroundColor: self.colorScheme == .light ? .hex(0x393939) - : self.viewStore.state.scope == .games + : store.state.scope == .games ? .isowordsOrange : .isowordsRed, title: Text("Leaderboards") ) .sheet( - store: self.store.scope(state: \.$destination.cubePreview, action: \.destination.cubePreview), - content: CubePreviewView.init(store:) - ) + item: $store.scope(state: \.destination?.cubePreview, action: \.destination.cubePreview) + ) { store in + CubePreviewView(store: store) + } } } From 852d16ca2208f780c3ab37c3d612a8533408eb7e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:26:46 -0800 Subject: [PATCH 13/61] wip --- .../ActiveGamesFeature/ActiveGamesView.swift | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Sources/ActiveGamesFeature/ActiveGamesView.swift b/Sources/ActiveGamesFeature/ActiveGamesView.swift index de260b9b..f6df6e9c 100644 --- a/Sources/ActiveGamesFeature/ActiveGamesView.swift +++ b/Sources/ActiveGamesFeature/ActiveGamesView.swift @@ -6,6 +6,7 @@ import SharedModels import Styleguide import SwiftUI +@ObservableState public struct ActiveGamesState: Equatable { public var savedGames: SavedGamesState public var turnBasedMatches: [ActiveTurnBasedMatch] @@ -45,7 +46,6 @@ public struct ActiveGamesView: View { @Environment(\.date) var date let showMenuItems: Bool let store: Store - @ObservedObject var viewStore: ViewStore public init( store: Store, @@ -53,13 +53,12 @@ public struct ActiveGamesView: View { ) { self.showMenuItems = showMenuItems self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }, removeDuplicates: ==) } public var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 20) { - if self.viewStore.savedGames.dailyChallengeUnlimited != nil { + if store.savedGames.dailyChallengeUnlimited != nil { ActiveGameCard( button: .init( icon: .init(systemName: "arrow.right"), @@ -70,12 +69,12 @@ public struct ActiveGamesView: View { .fontWeight(.medium) + Text("\nleft to play!") .foregroundColor(self.color.opacity(0.4)), - tapAction: { self.viewStore.send(.dailyChallengeTapped, animation: .default) }, + tapAction: { store.send(.dailyChallengeTapped, animation: .default) }, title: Text("Daily challenge") ) } - if let inProgressGame = self.viewStore.savedGames.unlimited { + if let inProgressGame = store.savedGames.unlimited { ActiveGameCard( button: .init( icon: .init(systemName: "arrow.right"), @@ -83,12 +82,12 @@ public struct ActiveGamesView: View { title: Text("Resume") ), message: soloMessage(inProgressGame: inProgressGame), - tapAction: { self.viewStore.send(.soloTapped, animation: .default) }, + tapAction: { store.send(.soloTapped, animation: .default) }, title: Text("Solo") ) } - ForEach(self.viewStore.turnBasedMatches) { match in + ForEach(store.turnBasedMatches) { match in let sendReminderAction = !match.isYourTurn && match.isStale ? match.theirIndex.map { otherPlayerIndex in @@ -101,9 +100,9 @@ public struct ActiveGamesView: View { ActiveGameCard( button: turnBasedButton(match: match), message: turnBasedMessage(match: match), - tapAction: { self.viewStore.send(.turnBasedGameTapped(match.id), animation: .default) }, + tapAction: { store.send(.turnBasedGameTapped(match.id), animation: .default) }, buttonAction: self.showMenuItems - ? sendReminderAction.map { action in { self.viewStore.send(action) } } + ? sendReminderAction.map { action in { store.send(action) } } : nil, title: Text("vs \(match.theirName ?? "your opponent")") ) @@ -114,13 +113,13 @@ public struct ActiveGamesView: View { let sendReminderAction = sendReminderAction { Button { - self.viewStore.send(sendReminderAction) + store.send(sendReminderAction) } label: { Label("Send Reminder", systemImage: "clock") } } Button { - self.viewStore.send(.turnBasedGameMenuItemTapped(.deleteMatch(match.id))) + store.send(.turnBasedGameMenuItemTapped(.deleteMatch(match.id))) } label: { Label("Delete Match", systemImage: "trash") .foregroundColor(.red) From 39c9bb79162892ca9d677d17f9ad415dd78fe7ac Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:30:21 -0800 Subject: [PATCH 14/61] wip --- Sources/GameCore/GameCore.swift | 8 +- Sources/GameCore/Views/GameFooterView.swift | 18 +---- Sources/GameCore/Views/GameHeaderView.swift | 86 ++++++--------------- 3 files changed, 31 insertions(+), 81 deletions(-) diff --git a/Sources/GameCore/GameCore.swift b/Sources/GameCore/GameCore.swift index b8bfa686..1c81a218 100644 --- a/Sources/GameCore/GameCore.swift +++ b/Sources/GameCore/GameCore.swift @@ -22,6 +22,7 @@ import UserSettingsClient public struct Game { @Reducer public struct Destination { + @ObservableState public enum State: Equatable { case alert(AlertState) case bottomMenu(BottomMenuState) @@ -67,11 +68,12 @@ public struct Game { } } + @ObservableState public struct State: Equatable { public var activeGames: ActiveGamesState public var cubes: Puzzle public var cubeStartedShakingAt: Date? - @PresentationState public var destination: Destination.State? + @Presents public var destination: Destination.State? public var gameContext: ClientModels.GameContext public var gameCurrentTime: Date public var gameMode: GameMode @@ -433,12 +435,14 @@ public struct Game { type: .playedWord(state.selectedWord) ) + var cubes = state.cubes let result = verify( move: move, - on: &state.cubes, + on: &cubes, isValidWord: { self.dictionaryContains($0, state.language) }, previousMoves: state.moves ) + state.cubes = cubes defer { state.selectedWord = [] } diff --git a/Sources/GameCore/Views/GameFooterView.swift b/Sources/GameCore/Views/GameFooterView.swift index 05ddd345..ba3bb1b2 100644 --- a/Sources/GameCore/Views/GameFooterView.swift +++ b/Sources/GameCore/Views/GameFooterView.swift @@ -6,17 +6,6 @@ import SwiftUI public struct GameFooterView: View { let isLeftToRight: Bool let store: StoreOf - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - let isAnimationReduced: Bool - let selectedWordString: String - - init(state: Game.State) { - self.isAnimationReduced = state.isAnimationReduced - self.selectedWordString = state.selectedWordString - } - } public init( isLeftToRight: Bool = false, @@ -24,17 +13,16 @@ public struct GameFooterView: View { ) { self.isLeftToRight = isLeftToRight self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) } public var body: some View { - if self.viewStore.selectedWordString.isEmpty { + if store.selectedWordString.isEmpty { WordListView( isLeftToRight: self.isLeftToRight, - store: self.store + store: store ) .transition( - viewStore.isAnimationReduced + store.isAnimationReduced ? .opacity : AnyTransition.offset(y: 50) .combined(with: .opacity) diff --git a/Sources/GameCore/Views/GameHeaderView.swift b/Sources/GameCore/Views/GameHeaderView.swift index dbf62565..25e1b0c7 100644 --- a/Sources/GameCore/Views/GameHeaderView.swift +++ b/Sources/GameCore/Views/GameHeaderView.swift @@ -5,84 +5,42 @@ import SwiftUI struct GameHeaderView: View { let store: StoreOf - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - let isTurnBasedGame: Bool - let selectedWordString: String - - init(state: Game.State) { - self.isTurnBasedGame = state.gameContext.is(\.turnBased) - self.selectedWordString = state.selectedWordString - } - } - - public init( - store: StoreOf - ) { - self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) - } var body: some View { - if self.viewStore.isTurnBasedGame, self.viewStore.selectedWordString.isEmpty { - PlayersAndScoresView(store: self.store) + if store.gameContext.is(\.turnBased), store.selectedWordString.isEmpty { + PlayersAndScoresView(store: store) .transition(.opacity) } else { - ScoreView(store: self.store) + ScoreView(store: store) } } } +extension Game.State { + fileprivate var secondsRemaining: Int { + max(0, self.gameMode.seconds - self.secondsPlayed) + } +} + struct ScoreView: View { @Environment(\.deviceState) var deviceState let store: StoreOf - @ObservedObject var viewStore: ViewStore @State var isTimeAccented = false - struct ViewState: Equatable { - let currentScore: Int - let gameContext: GameContext - let gameMode: GameMode - let secondsRemaining: Int - let selectedWordHasAlreadyBeenPlayed: Bool - let selectedWordIsValid: Bool - let selectedWordScore: Int - let selectedWordString: String - - init(state: Game.State) { - self.currentScore = state.currentScore - self.gameContext = state.gameContext - self.gameMode = state.gameMode - self.secondsRemaining = max(0, state.gameMode.seconds - state.secondsPlayed) - self.selectedWordHasAlreadyBeenPlayed = state.selectedWordHasAlreadyBeenPlayed - self.selectedWordIsValid = state.selectedWordIsValid - self.selectedWordScore = state.selectedWordScore - self.selectedWordString = state.selectedWordString - } - } - - public init( - store: StoreOf - ) { - self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) - } - var body: some View { HStack { - if self.viewStore.selectedWordString.isEmpty { - if !self.viewStore.gameContext.is(\.turnBased) { - Text("\(self.viewStore.currentScore)") + if store.selectedWordString.isEmpty { + if !store.gameContext.is(\.turnBased) { + Text("\(store.currentScore)") } } else { - Text(self.viewStore.selectedWordString) + Text(store.selectedWordString) .overlay( Text( - self.viewStore.selectedWordIsValid - ? "\(self.viewStore.selectedWordScore)" - : self.viewStore.selectedWordHasAlreadyBeenPlayed + store.selectedWordIsValid + ? "\(store.selectedWordScore)" + : store.selectedWordHasAlreadyBeenPlayed ? "(used)" : "" ) @@ -91,27 +49,27 @@ struct ScoreView: View { .alignmentGuide(.trailing) { _ in 0 }, alignment: .topTrailing ) - .opacity(self.viewStore.selectedWordIsValid ? 1 : 0.5) + .opacity(store.selectedWordIsValid ? 1 : 0.5) .allowsTightening(true) .minimumScaleFactor(0.2) .lineLimit(1) .transition(.opacity) - .animation(nil, value: self.viewStore.selectedWordString) + .animation(nil, value: store.selectedWordString) } Spacer() - if !self.viewStore.gameContext.is(\.turnBased) { + if !store.gameContext.is(\.turnBased) { Text( displayTime( - gameMode: self.viewStore.gameMode, - secondsRemaining: self.viewStore.secondsRemaining + gameMode: store.gameMode, + secondsRemaining: store.secondsRemaining ) ) .foregroundColor(.white) .colorMultiply(self.isTimeAccented ? .red : .adaptiveBlack) .scaleEffect(self.isTimeAccented ? 1.5 : 1) - .onChange(of: self.viewStore.secondsRemaining) { secondsRemaining in + .onChange(of: store.secondsRemaining) { _, secondsRemaining in guard secondsRemaining == 10 || (secondsRemaining <= 5 && secondsRemaining > 0) else { return } From 19c35f6717061c6222643086442ab42982a99a25 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:31:12 -0800 Subject: [PATCH 15/61] wip --- Sources/GameCore/Views/GameNavView.swift | 32 +++++------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/Sources/GameCore/Views/GameNavView.swift b/Sources/GameCore/Views/GameNavView.swift index 20d7a716..fae8769f 100644 --- a/Sources/GameCore/Views/GameNavView.swift +++ b/Sources/GameCore/Views/GameNavView.swift @@ -3,41 +3,21 @@ import SwiftUI struct GameNavView: View { let store: StoreOf - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - let isTrayAvailable: Bool - let isTrayVisible: Bool - let trayTitle: String - - init(state: Game.State) { - self.isTrayAvailable = state.isTrayAvailable - self.isTrayVisible = state.isTrayVisible - self.trayTitle = state.displayTitle - } - } - - public init( - store: StoreOf - ) { - self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) - } var body: some View { HStack(alignment: .center, spacing: 8) { Button { - self.viewStore.send(.trayButtonTapped, animation: .default) + store.send(.trayButtonTapped, animation: .default) } label: { HStack { - Text(self.viewStore.trayTitle) + Text(store.displayTitle) .lineLimit(1) Spacer() Image(systemName: "chevron.down") - .rotationEffect(.degrees(self.viewStore.isTrayVisible ? 180 : 0)) - .opacity(self.viewStore.isTrayAvailable ? 1 : 0) + .rotationEffect(.degrees(store.isTrayVisible ? 180 : 0)) + .opacity(store.isTrayAvailable ? 1 : 0) } .adaptiveFont(.matterMedium, size: 14) .foregroundColor(.adaptiveBlack) @@ -48,10 +28,10 @@ struct GameNavView: View { .opacity(0.05) ) .cornerRadius(12) - .disabled(!self.viewStore.isTrayAvailable) + .disabled(!store.isTrayAvailable) Button { - self.viewStore.send(.menuButtonTapped, animation: .default) + store.send(.menuButtonTapped, animation: .default) } label: { Image(systemName: "ellipsis") .foregroundColor(.adaptiveBlack) From fda7be3355786eae034cf3a3b3fa2c789d9dc389 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:34:17 -0800 Subject: [PATCH 16/61] wip --- Sources/GameCore/Views/GameView.swift | 117 ++++++++++---------------- 1 file changed, 44 insertions(+), 73 deletions(-) diff --git a/Sources/GameCore/Views/GameView.swift b/Sources/GameCore/Views/GameView.swift index 6a6b259e..cc2d5c42 100644 --- a/Sources/GameCore/Views/GameView.swift +++ b/Sources/GameCore/Views/GameView.swift @@ -15,27 +15,8 @@ public struct GameView: View where Content: View { @Environment(\.colorScheme) var colorScheme @Environment(\.deviceState) var deviceState let content: Content - let store: StoreOf + @Bindable var store: StoreOf var trayHeight: CGFloat { ActiveGamesView.height + (16 + self.adaptiveSize.padding) * 2 } - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - let isAnimationReduced: Bool - let isDailyChallenge: Bool - let isGameLoaded: Bool - let isNavVisible: Bool - let isTrayVisible: Bool - let selectedWordString: String - - init(state: Game.State) { - self.isAnimationReduced = state.isAnimationReduced - self.isDailyChallenge = state.gameContext.is(\.dailyChallenge) - self.isGameLoaded = state.isGameLoaded - self.isNavVisible = state.isNavVisible - self.isTrayVisible = state.isTrayVisible - self.selectedWordString = state.selectedWordString - } - } public init( content: Content, @@ -43,14 +24,13 @@ public struct GameView: View where Content: View { ) { self.content = content self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) } public var body: some View { GeometryReader { proxy in ZStack { ZStack(alignment: .top) { - if self.viewStore.isGameLoaded { + if store.isGameLoaded { self.content .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .ignoresSafeArea() @@ -70,31 +50,28 @@ public struct GameView: View where Content: View { VStack { Group { - if self.viewStore.isNavVisible { - GameNavView(store: self.store) + if store.isNavVisible { + GameNavView(store: store) } else { - GameNavView(store: self.store) + GameNavView(store: store) .hidden() } - GameHeaderView(store: self.store) + GameHeaderView(store: store) } .screenEdgePadding(self.deviceState.isPad ? .horizontal : []) Spacer() - GameFooterView(store: self.store) + GameFooterView(store: store) .padding(.bottom) } .ignoresSafeArea(.keyboard) - if !self.viewStore.selectedWordString.isEmpty { + if !store.selectedWordString.isEmpty { WordSubmitButton( - store: self.store.scope( - state: \.wordSubmitButtonFeature, - action: \.wordSubmitButton - ) + store: store.scope(state: \.wordSubmitButtonFeature, action: \.wordSubmitButton) ) .ignoresSafeArea() .transition( - viewStore.isAnimationReduced + store.isAnimationReduced ? .opacity : AnyTransition .asymmetric(insertion: .offset(y: 50), removal: .offset(y: 50)) @@ -103,7 +80,7 @@ public struct GameView: View where Content: View { } ActiveGamesView( - store: self.store.scope(state: \.activeGames, action: \.activeGames), + store: store.scope(state: \.activeGames, action: \.activeGames), showMenuItems: false ) .adaptivePadding(.vertical, 8) @@ -121,54 +98,48 @@ public struct GameView: View where Content: View { ) ) .fixedSize(horizontal: false, vertical: true) - .opacity(self.viewStore.isTrayVisible ? 1 : 0) + .opacity(store.isTrayVisible ? 1 : 0) .offset(y: -self.trayHeight) } - .offset(y: self.viewStore.isTrayVisible ? self.trayHeight : 0) + .offset(y: store.isTrayVisible ? self.trayHeight : 0) .zIndex(0) - IfLetStore( - self.store.scope( - state: \.destination?.gameOver, action: \.destination.gameOver.presented - ), - then: GameOverView.init(store:) - ) - .background(Color.adaptiveWhite.ignoresSafeArea()) - .transition( - .asymmetric( - insertion: AnyTransition.opacity.animation(.linear(duration: 1)), - removal: .game - ) - ) - .zIndex(1) - - IfLetStore( - self.store.scope( - state: \.destination?.upgradeInterstitial, - action: \.destination.upgradeInterstitial.presented - ) - ) { store in + if let store = store.scope( + state: \.destination?.gameOver, action: \.destination.gameOver.presented + ) { + GameOverView(store: store) + .background(Color.adaptiveWhite.ignoresSafeArea()) + .transition( + .asymmetric( + insertion: AnyTransition.opacity.animation(.linear(duration: 1)), + removal: .game + ) + ) + .zIndex(1) + } else if let store = store.scope( + state: \.destination?.upgradeInterstitial, + action: \.destination.upgradeInterstitial.presented + ) { UpgradeInterstitialView(store: store) .transition(.opacity) + .zIndex(2) } - .zIndex(2) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background( - viewStore.isAnimationReduced + store.isAnimationReduced ? nil : BloomBackground( size: proxy.size, - store: self.store - .scope( - state: { - BloomBackground.ViewState( - bloomCount: $0.selectedWord.count, - word: $0.selectedWordString - ) - }, - action: absurd - ) + store: store.scope( + state: { + BloomBackground.ViewState( + bloomCount: $0.selectedWord.count, + word: $0.selectedWordString + ) + }, + action: absurd + ) ) ) .background( @@ -176,20 +147,20 @@ public struct GameView: View where Content: View { .ignoresSafeArea() ) .bottomMenu( - store: self.store.scope(state: \.$destination, action: \.destination), + store: store.scope(state: \.$destination, action: \.destination), state: \.bottomMenu, action: { .bottomMenu($0) } ) - .alert(store: self.store.scope(state: \.$destination.alert, action: \.destination.alert)) + .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .sheet( - store: self.store.scope(state: \.$destination.settings, action: \.destination.settings) + item: $store.scope(state: \.destination?.settings, action: \.destination.settings) ) { store in NavigationStack { SettingsView(store: store, navPresentationStyle: .modal) } } } - .task { await self.viewStore.send(.task).finish() } + .task { await store.send(.task).finish() } } } From 3e5a8aa73615f0af1f6caf2133a9299d1d886a87 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:36:02 -0800 Subject: [PATCH 17/61] wip --- Sources/GameCore/Views/WordSubmitButton.swift | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/Sources/GameCore/Views/WordSubmitButton.swift b/Sources/GameCore/Views/WordSubmitButton.swift index 4a107947..4d076fa5 100644 --- a/Sources/GameCore/Views/WordSubmitButton.swift +++ b/Sources/GameCore/Views/WordSubmitButton.swift @@ -4,6 +4,7 @@ import SwiftUI @Reducer public struct WordSubmitButtonFeature { + @ObservableState public struct State: Equatable { public var isSelectedWordValid: Bool public let isTurnBasedMatch: Bool @@ -138,19 +139,15 @@ public struct WordSubmitButtonFeature { public struct WordSubmitButton: View { @Environment(\.deviceState) var deviceState let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf @State var isTouchDown = false - public init( - store: StoreOf - ) { + public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) } public var body: some View { ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) { - if self.viewStore.wordSubmitButton.areReactionsOpen { + if store.wordSubmitButton.areReactionsOpen { RadialGradient( gradient: Gradient(colors: [.white, Color.white.opacity(0)]), center: .bottom, @@ -164,13 +161,13 @@ public struct WordSubmitButton: View { Spacer() ZStack { - ReactionsView(store: self.store.scope(state: \.wordSubmitButton, action: \.self)) + ReactionsView(store: store.scope(state: \.wordSubmitButton, action: \.self)) Button { - self.viewStore.send(.submitButtonTapped, animation: .default) + store.send(.submitButtonTapped, animation: .default) } label: { Group { - if !self.viewStore.wordSubmitButton.areReactionsOpen { + if !store.wordSubmitButton.areReactionsOpen { Image(systemName: "hand.thumbsup") } else { Image(systemName: "xmark") @@ -182,7 +179,7 @@ public struct WordSubmitButton: View { ) .background(Circle().fill(Color.adaptiveBlack)) .foregroundColor(.adaptiveWhite) - .opacity(self.viewStore.isSelectedWordValid ? 1 : 0.5) + .opacity(store.isSelectedWordValid ? 1 : 0.5) .font(.system(size: self.deviceState.isPad ? 40 : 30)) .adaptivePadding([.all], .grid(4)) // NB: Expand the tappable radius of the button. @@ -192,12 +189,12 @@ public struct WordSubmitButton: View { DragGesture(minimumDistance: 0) .onChanged { touch in if !self.isTouchDown { - self.viewStore.send(.submitButtonPressed, animation: .default) + store.send(.submitButtonPressed, animation: .default) } self.isTouchDown = true } .onEnded { _ in - self.viewStore.send(.submitButtonReleased, animation: .default) + store.send(.submitButtonReleased, animation: .default) self.isTouchDown = false } ) @@ -207,13 +204,13 @@ public struct WordSubmitButton: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background( - self.viewStore.wordSubmitButton.areReactionsOpen - ? Color.isowordsBlack.opacity(0.4) - : nil - ) - .animation(.default, value: self.viewStore.wordSubmitButton.areReactionsOpen) - .onTapGesture { self.viewStore.send(.backgroundTapped, animation: .default) } + .background { + if store.wordSubmitButton.areReactionsOpen { + Color.isowordsBlack.opacity(0.4) + } + } + .animation(.default, value: store.wordSubmitButton.areReactionsOpen) + .onTapGesture { store.send(.backgroundTapped, animation: .default) } } } From 810fcf31d6eb297e4f531ae050c7a2ffbdaafc1c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:38:14 -0800 Subject: [PATCH 18/61] wip --- Sources/GameCore/Views/GameFooterView.swift | 24 ++++----------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/Sources/GameCore/Views/GameFooterView.swift b/Sources/GameCore/Views/GameFooterView.swift index ba3bb1b2..49de9ea8 100644 --- a/Sources/GameCore/Views/GameFooterView.swift +++ b/Sources/GameCore/Views/GameFooterView.swift @@ -35,15 +35,8 @@ public struct WordListView: View { @Environment(\.adaptiveSize) var adaptiveSize @Environment(\.deviceState) var deviceState - struct ViewState: Equatable { - let isTurnBasedGame: Bool - let isYourTurn: Bool - let words: [PlayedWord] - } - let isLeftToRight: Bool let store: StoreOf - @ObservedObject var viewStore: ViewStore public init( isLeftToRight: Bool = false, @@ -51,14 +44,13 @@ public struct WordListView: View { ) { self.isLeftToRight = isLeftToRight self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) } struct SpacerId: Hashable {} public var body: some View { Group { - if self.viewStore.words.isEmpty { + if store.playedWords.isEmpty { Text("Tap the cube to play") .adaptiveFont(.matterMedium, size: 14) } else { @@ -66,7 +58,7 @@ public struct WordListView: View { ScrollViewReader { reader in HStack(spacing: 10) { ForEach( - self.isLeftToRight ? self.viewStore.words : self.viewStore.words.reversed(), + self.isLeftToRight ? store.playedWords : store.playedWords.reversed(), id: \.word ) { word in ZStack(alignment: .topTrailing) { @@ -104,7 +96,7 @@ public struct WordListView: View { guard self.isLeftToRight else { return } reader.scrollTo(SpacerId(), anchor: self.isLeftToRight ? .trailing : .leading) } - .onChange(of: self.viewStore.words) { _ in + .onChange(of: store.playedWords) { guard self.isLeftToRight else { return } withAnimation { reader.scrollTo(SpacerId(), anchor: self.isLeftToRight ? .trailing : .leading) @@ -128,7 +120,7 @@ public struct WordListView: View { @ViewBuilder func colors(for playedWord: PlayedWord) -> some View { - if self.viewStore.isTurnBasedGame && playedWord.isYourWord { + if store.gameContext.is(\.turnBased) && playedWord.isYourWord { LinearGradient( gradient: Gradient(colors: Styleguide.colors(for: playedWord.word)), startPoint: .bottomLeading, @@ -140,14 +132,6 @@ public struct WordListView: View { } } -extension WordListView.ViewState { - init(state: Game.State) { - self.isTurnBasedGame = state.gameContext.is(\.turnBased) - self.isYourTurn = state.isYourTurn - self.words = state.playedWords - } -} - #if DEBUG import ClientModels import ComposableGameCenter From 0932033dd2929781a3c86285902285fb180012a7 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:39:28 -0800 Subject: [PATCH 19/61] wip --- Sources/GameCore/Views/WordSubmitButton.swift | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/Sources/GameCore/Views/WordSubmitButton.swift b/Sources/GameCore/Views/WordSubmitButton.swift index 4d076fa5..457d4991 100644 --- a/Sources/GameCore/Views/WordSubmitButton.swift +++ b/Sources/GameCore/Views/WordSubmitButton.swift @@ -24,6 +24,7 @@ public struct WordSubmitButtonFeature { } } + @ObservableState public struct ButtonState: Equatable { public var areReactionsOpen: Bool public var favoriteReactions: [Move.Reaction] @@ -216,20 +217,13 @@ public struct WordSubmitButton: View { struct ReactionsView: View { let store: Store - @ObservedObject var viewStore: - ViewStore - - public init(store: Store) { - self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) - } var body: some View { - ForEach(Array(self.viewStore.favoriteReactions.enumerated()), id: \.offset) { idx, reaction in + ForEach(Array(store.favoriteReactions.enumerated()), id: \.offset) { idx, reaction in let offset = self.offset(index: idx) Button { - self.viewStore.send(.reactionButtonTapped(reaction), animation: .default) + store.send(.reactionButtonTapped(reaction), animation: .default) } label: { Text(reaction.rawValue) .font(.system(size: 32)) @@ -237,23 +231,23 @@ struct ReactionsView: View { } .background(Color.white.opacity(0.5)) .clipShape(Circle()) - .rotationEffect(.degrees(self.viewStore.areReactionsOpen ? -360 : 0)) - .opacity(self.viewStore.areReactionsOpen ? 1 : 0) + .rotationEffect(.degrees(store.areReactionsOpen ? -360 : 0)) + .opacity(store.areReactionsOpen ? 1 : 0) .offset(x: offset.x, y: offset.y) .animation( - .default.delay(Double(idx) / Double(self.viewStore.favoriteReactions.count * 10)), - value: self.viewStore.areReactionsOpen + .default.delay(Double(idx) / Double(store.favoriteReactions.count * 10)), + value: store.areReactionsOpen ) } } func offset(index: Int) -> CGPoint { let angle: CGFloat = - CGFloat.pi / CGFloat(self.viewStore.favoriteReactions.count - 1) * CGFloat(index) + .pi + CGFloat.pi / CGFloat(store.favoriteReactions.count - 1) * CGFloat(index) + .pi return .init( - x: self.viewStore.areReactionsOpen ? cos(angle) * 130 : 0, - y: self.viewStore.areReactionsOpen ? sin(angle) * 130 : 0 + x: store.areReactionsOpen ? cos(angle) * 130 : 0, + y: store.areReactionsOpen ? sin(angle) * 130 : 0 ) } } From 4db019c577134ab643b55277b35b8a30474a0cb4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:44:05 -0800 Subject: [PATCH 20/61] wip --- .../DailyChallengeView.swift | 4 +- Sources/MultiplayerFeature/PastGameRow.swift | 61 ++++++++++++------- .../MultiplayerFeature/PastGameState.swift | 16 ++--- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/Sources/DailyChallengeFeature/DailyChallengeView.swift b/Sources/DailyChallengeFeature/DailyChallengeView.swift index 957b0e05..601122ab 100644 --- a/Sources/DailyChallengeFeature/DailyChallengeView.swift +++ b/Sources/DailyChallengeFeature/DailyChallengeView.swift @@ -13,6 +13,7 @@ import SwiftUI public struct DailyChallengeReducer { @Reducer public struct Destination { + @ObservableState public enum State: Equatable { case alert(AlertState) case notificationsAuthAlert(NotificationsAuthAlert.State) @@ -38,9 +39,10 @@ public struct DailyChallengeReducer { } } + @ObservableState public struct State: Equatable { public var dailyChallenges: [FetchTodaysDailyChallengeResponse] - @PresentationState public var destination: Destination.State? + @Presents public var destination: Destination.State? public var gameModeIsLoading: GameMode? public var inProgressDailyChallengeUnlimited: InProgressGame? public var userNotificationSettings: UserNotificationClient.Notification.Settings? diff --git a/Sources/MultiplayerFeature/PastGameRow.swift b/Sources/MultiplayerFeature/PastGameRow.swift index 34081b06..1e7ddbb9 100644 --- a/Sources/MultiplayerFeature/PastGameRow.swift +++ b/Sources/MultiplayerFeature/PastGameRow.swift @@ -5,8 +5,9 @@ import Tagged @Reducer public struct PastGame { + @ObservableState public struct State: Equatable, Identifiable { - @PresentationState public var alert: AlertState? + @Presents public var alert: AlertState? public var challengeeDisplayName: String public var challengerDisplayName: String public var challengeeScore: Int @@ -33,6 +34,28 @@ public struct PastGame { ? .challengee : .tied } + + init( + alert: AlertState? = nil, + challengeeDisplayName: String, + challengerDisplayName: String, + challengeeScore: Int, + challengerScore: Int, + endDate: Date, + isRematchRequestInFlight: Bool = false, + matchId: TurnBasedMatch.Id, + opponentDisplayName: String + ) { + self.alert = alert + self.challengeeDisplayName = challengeeDisplayName + self.challengerDisplayName = challengerDisplayName + self.challengeeScore = challengeeScore + self.challengerScore = challengerScore + self.endDate = endDate + self.isRematchRequestInFlight = isRematchRequestInFlight + self.matchId = matchId + self.opponentDisplayName = opponentDisplayName + } } public enum Action { @@ -112,24 +135,18 @@ public struct PastGame { struct PastGameRow: View { @Environment(\.colorScheme) var colorScheme - let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf - - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) - } + @Bindable var store: StoreOf var body: some View { ZStack(alignment: .bottomLeading) { Button { - self.viewStore.send(.tappedRow, animation: .default) + store.send(.tappedRow, animation: .default) } label: { VStack(alignment: .leading, spacing: .grid(6)) { HStack(spacing: .grid(1)) { - Text("\(self.viewStore.endDate, formatter: dateFormatter)") + Text("\(store.endDate, formatter: dateFormatter)") - Text("vs \(self.viewStore.opponentDisplayName)") + Text("vs \(store.opponentDisplayName)") .opacity(0.5) .lineLimit(1) .truncationMode(.tail) @@ -138,46 +155,46 @@ struct PastGameRow: View { VStack(alignment: .leading, spacing: .grid(1)) { HStack { - Text(self.viewStore.challengerDisplayName) + Text(store.challengerDisplayName) .adaptiveFont(.matterMedium, size: 16) - if self.viewStore.outcome != .challengee { + if store.outcome != .challengee { Image(systemName: "checkmark.circle.fill") .font(.system(size: 18)) } Spacer() - Text("\(self.viewStore.challengerScore)") + Text("\(store.challengerScore)") .adaptiveFont(.matterMedium, size: 16) { $0.monospacedDigit() } } HStack(spacing: .grid(1)) { - Text(self.viewStore.challengeeDisplayName) + Text(store.challengeeDisplayName) .adaptiveFont(.matterMedium, size: 16) - if self.viewStore.outcome != .challenger { + if store.outcome != .challenger { Image(systemName: "checkmark.circle.fill") .font(.system(size: 18)) } Spacer() - Text("\(self.viewStore.challengeeScore)") + Text("\(store.challengeeScore)") .adaptiveFont(.matterMedium, size: 16) { $0.monospacedDigit() } } } - self.rematchButton(matchId: self.viewStore.matchId) + self.rematchButton(matchId: store.matchId) .hidden() } } .frame(maxWidth: .infinity, alignment: .leading) - self.rematchButton(matchId: self.viewStore.matchId) + self.rematchButton(matchId: store.matchId) } - .alert(store: self.store.scope(state: \.$alert, action: \.alert)) + .alert($store.scope(state: \.alert, action: \.alert)) } func rematchButton(matchId: TurnBasedMatch.Id) -> some View { Button { - self.viewStore.send(.rematchButtonTapped, animation: .default) + store.send(.rematchButtonTapped, animation: .default) } label: { HStack(spacing: .grid(1)) { - if self.viewStore.isRematchRequestInFlight { + if store.isRematchRequestInFlight { ProgressView() .progressViewStyle( CircularProgressViewStyle( diff --git a/Sources/MultiplayerFeature/PastGameState.swift b/Sources/MultiplayerFeature/PastGameState.swift index 4f9e2c35..b1c5d480 100644 --- a/Sources/MultiplayerFeature/PastGameState.swift +++ b/Sources/MultiplayerFeature/PastGameState.swift @@ -30,12 +30,14 @@ extension PastGame.State { let opponentPlayer = match.participants[opponentIndex].player else { return nil } - self.challengeeDisplayName = challengeePlayer.displayName - self.challengeeScore = matchData.score(forPlayerIndex: 1) - self.challengerDisplayName = challengerPlayer.displayName - self.challengerScore = matchData.score(forPlayerIndex: 0) - self.endDate = endDate - self.matchId = match.matchId - self.opponentDisplayName = opponentPlayer.displayName + self.init( + challengeeDisplayName: challengeePlayer.displayName, + challengerDisplayName: challengerPlayer.displayName, + challengeeScore: matchData.score(forPlayerIndex: 1), + challengerScore: matchData.score(forPlayerIndex: 0), + endDate: endDate, + matchId: match.matchId, + opponentDisplayName: opponentPlayer.displayName + ) } } From 7b9ae18a1029c080a280aa8a663c072137c585c0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:47:24 -0800 Subject: [PATCH 21/61] wip --- .../xcshareddata/swiftpm/Package.resolved | 2 +- Sources/MultiplayerFeature/PastGamesView.swift | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fa9e605e..4d4d3486 100644 --- a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -114,7 +114,7 @@ "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { "branch" : "observation-beta", - "revision" : "06ca7d94b4d2d60c437e9c5b577dc4ae56e6c57f" + "revision" : "4e572f3be218d404b9d8e1347c8bc2aa46dd4b0c" } }, { diff --git a/Sources/MultiplayerFeature/PastGamesView.swift b/Sources/MultiplayerFeature/PastGamesView.swift index 06828e70..8fad29ad 100644 --- a/Sources/MultiplayerFeature/PastGamesView.swift +++ b/Sources/MultiplayerFeature/PastGamesView.swift @@ -6,6 +6,7 @@ import SwiftUI @Reducer public struct PastGames { + @ObservableState public struct State: Equatable { public var pastGames: IdentifiedArrayOf = [] } @@ -60,18 +61,10 @@ public struct PastGames { struct PastGamesView: View { @Environment(\.colorScheme) var colorScheme let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf - - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) - } var body: some View { ScrollView { - ForEachStore( - self.store.scope(state: \.pastGames, action: \.pastGames) - ) { store in + ForEach(store.scope(state: \.pastGames, action: \.pastGames)) { store in Group { PastGameRow(store: store) @@ -83,7 +76,7 @@ struct PastGamesView: View { } .padding() } - .task { await viewStore.send(.task).finish() } + .task { await store.send(.task).finish() } .navigationStyle( backgroundColor: self.colorScheme == .dark ? .isowordsBlack : .multiplayer, foregroundColor: self.colorScheme == .dark ? .multiplayer : .isowordsBlack, From 70b7e3b6e2b913903ccb0db6544519aa29b4da1a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 16:52:11 -0800 Subject: [PATCH 22/61] wip --- Sources/TrailerFeature/Trailer.swift | 90 ++++++++++++---------------- 1 file changed, 37 insertions(+), 53 deletions(-) diff --git a/Sources/TrailerFeature/Trailer.swift b/Sources/TrailerFeature/Trailer.swift index b376900b..a9e2da96 100644 --- a/Sources/TrailerFeature/Trailer.swift +++ b/Sources/TrailerFeature/Trailer.swift @@ -9,10 +9,11 @@ import UIApplicationClient @Reducer public struct Trailer { + @ObservableState public struct State: Equatable { var game: Game.State - @BindingState var nub: CubeSceneView.ViewState.NubState - @BindingState var opacity: Double + var nub: CubeSceneView.ViewState.NubState + var opacity: Double public init( game: Game.State, @@ -25,7 +26,7 @@ public struct Trailer { } public init() { - self = .init( + self.init( game: .init( cubes: .trailer, gameContext: .solo, @@ -93,7 +94,7 @@ public struct Trailer { await self.audioPlayer.play(.onboardingBgMusic) // Fade the cube in after a second - await send(.set(\.$opacity, 1), animation: .easeInOut(duration: fadeInDuration)) + await send(.set(\.opacity, 1), animation: .easeInOut(duration: fadeInDuration)) try await self.mainQueue.sleep(for: firstWordDelay) // Play each word @@ -105,7 +106,7 @@ public struct Trailer { // Move the nub to the face being played nub.location = .face(face) await send( - .set(\.$nub, nub), + .set(\.nub, nub), animateWithDuration: moveNubToFaceDuration, options: .curveEaseInOut ) @@ -120,7 +121,7 @@ public struct Trailer { // Press the nub on the first character nub.isPressed = true if characterIndex == 0 { - await send(.set(\.$nub, nub), animateWithDuration: 0.3) + await send(.set(\.nub, nub), animateWithDuration: 0.3) } // Select the cube face await send(.game(.tap(.began, face)), animation: .default) @@ -128,14 +129,14 @@ public struct Trailer { // Release the nub when the last character is played nub.isPressed = false - await send(.set(\.$nub, nub), animateWithDuration: 0.3) + await send(.set(\.nub, nub), animateWithDuration: 0.3) // Move the nub to the submit button try await self.mainQueue.sleep(for: .seconds(0.3)) nub.location = .submitButton await send( - .set(\.$nub, nub), + .set(\.nub, nub), animateWithDuration: moveNubToSubmitButtonDuration, options: .curveEaseInOut ) @@ -157,7 +158,7 @@ public struct Trailer { group.addTask { [nub] in var nub = nub nub.isPressed = true - await send(.set(\.$nub, nub), animateWithDuration: 0.3) + await send(.set(\.nub, nub), animateWithDuration: 0.3) } group.addTask { [nub] in var nub = nub @@ -165,7 +166,7 @@ public struct Trailer { await send(.game(.submitButtonTapped(reaction: nil))) try await self.mainQueue.sleep(for: .seconds(0.3)) nub.isPressed = false - await send(.set(\.$nub, nub), animateWithDuration: 0.3) + await send(.set(\.nub, nub), animateWithDuration: 0.3) } } } @@ -174,12 +175,12 @@ public struct Trailer { try await self.mainQueue.sleep(for: .seconds(0.3)) nub.location = .offScreenBottom await send( - .set(\.$nub, nub), + .set(\.nub, nub), animateWithDuration: moveNubOffScreenDuration, options: .curveEaseInOut ) - await send(.set(\.$opacity, 0), animation: .linear(duration: moveNubOffScreenDuration)) + await send(.set(\.opacity, 0), animation: .linear(duration: moveNubOffScreenDuration)) } } } @@ -188,36 +189,18 @@ public struct Trailer { public struct TrailerView: View { let store: StoreOf - @ObservedObject var viewStore: ViewStore @Environment(\.deviceState) var deviceState - struct ViewState: Equatable { - let opacity: Double - let selectedWordHasAlreadyBeenPlayed: Bool - let selectedWordIsValid: Bool - let selectedWordScore: Int? - let selectedWordString: String - - init(state: Trailer.State) { - self.opacity = state.opacity - self.selectedWordHasAlreadyBeenPlayed = state.game.selectedWordHasAlreadyBeenPlayed - self.selectedWordIsValid = state.game.selectedWordIsValid - self.selectedWordScore = self.selectedWordIsValid ? state.game.selectedWordScore : nil - self.selectedWordString = state.game.selectedWordString - } - } - public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) } public var body: some View { GeometryReader { proxy in ZStack { VStack { - if !self.viewStore.selectedWordString.isEmpty { - (Text(self.viewStore.selectedWordString) + if !store.game.selectedWordString.isEmpty { + (Text(store.game.selectedWordString) + self.scoreText .baselineOffset( (self.deviceState.idiom == .pad ? 2 : 1) * 16 @@ -232,19 +215,19 @@ public struct TrailerView: View { .matterSemiBold, size: (self.deviceState.idiom == .pad ? 2 : 1) * 32 ) - .opacity(self.viewStore.selectedWordIsValid ? 1 : 0.5) + .opacity(store.game.selectedWordIsValid ? 1 : 0.5) .allowsTightening(true) .minimumScaleFactor(0.2) .lineLimit(1) .transition(.opacity) - .animation(nil, value: self.viewStore.selectedWordString) + .animation(nil, value: store.game.selectedWordString) } Spacer() - if !self.viewStore.selectedWordString.isEmpty { + if !store.game.selectedWordString.isEmpty { WordSubmitButton( - store: self.store.scope( + store: store.scope( state: \.game.wordSubmitButtonFeature, action: \.game.wordSubmitButton ) @@ -259,13 +242,13 @@ public struct TrailerView: View { WordListView( isLeftToRight: true, - store: self.store.scope(state: \.game, action: \.game) + store: store.scope(state: \.game, action: \.game) ) } .adaptivePadding(.top, .grid(18)) .adaptivePadding(.bottom, .grid(2)) - CubeView(store: self.store.scope(state: \.cubeScene, action: \.game.cubeScene)) + CubeView(store: store.scope(state: \.cubeScene, action: \.game.cubeScene)) .adaptivePadding( self.deviceState.idiom == .pad ? .horizontal : [], .grid(30) @@ -274,16 +257,15 @@ public struct TrailerView: View { .background( BloomBackground( size: proxy.size, - store: self.store - .scope( - state: { - BloomBackground.ViewState( - bloomCount: $0.game.selectedWord.count, - word: $0.game.selectedWordString - ) - }, - action: absurd - ) + store: store.scope( + state: { + BloomBackground.ViewState( + bloomCount: $0.game.selectedWord.count, + word: $0.game.selectedWordString + ) + }, + action: absurd + ) ) ) } @@ -291,14 +273,16 @@ public struct TrailerView: View { self.deviceState.idiom == .pad ? .vertical : [], .grid(15) ) - .opacity(self.viewStore.opacity) - .task { await self.viewStore.send(.task).finish() } + .opacity(store.opacity) + .task { await store.send(.task).finish() } } var scoreText: Text { - self.viewStore.selectedWordScore.map { - Text(" \($0)") - } ?? Text("") + if store.game.selectedWordIsValid { + return Text(" \(store.game.selectedWordScore)") + } else { + return Text(verbatim: "") + } } } From d7da8fc1cbb9b1c1c2254a87aad9b121a7ec7a96 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 23:45:51 -0800 Subject: [PATCH 23/61] wip --- Sources/DemoFeature/Demo.swift | 40 ++++++++++------------------------ 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/Sources/DemoFeature/Demo.swift b/Sources/DemoFeature/Demo.swift index a18acff8..3cf8f61b 100644 --- a/Sources/DemoFeature/Demo.swift +++ b/Sources/DemoFeature/Demo.swift @@ -10,6 +10,7 @@ import TcaHelpers @Reducer public struct Demo { + @ObservableState public struct State: Equatable { var appStoreOverlayIsPresented: Bool var step: Step @@ -126,55 +127,38 @@ public struct Demo { } public struct DemoView: View { - let store: StoreOf - @ObservedObject var viewStore: ViewStore + @Bindable var store: StoreOf - struct ViewState: Equatable { - let appStoreOverlayIsPresented: Bool - let isGameOver: Bool - - init(state: Demo.State) { - self.appStoreOverlayIsPresented = state.appStoreOverlayIsPresented - self.isGameOver = state.isGameOver - } - } - - public init( - store: StoreOf - ) { + public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) } public var body: some View { - SwitchStore(self.store.scope(state: \.step, action: \.self)) { step in - switch step { + Group { + switch store.step { case .onboarding: - CaseLet(\Demo.State.Step.onboarding, action: Demo.Action.onboarding) { - OnboardingView(store: $0) - .onAppear { self.viewStore.send(.onAppear) } + if let store = store.scope(state: \.step.onboarding, action: \.onboarding) { + OnboardingView(store: store) + .onAppear { self.store.send(.onAppear) } } case .game: - CaseLet(\Demo.State.Step.game, action: Demo.Action.game) { store in + if let store = store.scope(state: \.step.game, action: \.game) { GameWrapper( content: GameView( content: CubeView(store: store.scope(state: \.cubeScene, action: \.cubeScene)), store: store ), - isGameOver: self.viewStore.isGameOver, + isGameOver: self.store.isGameOver, bannerAction: { - self.viewStore.send(.fullVersionButtonTapped) + self.store.send(.fullVersionButtonTapped) } ) } } } .appStoreOverlay( - isPresented: self.viewStore.binding( - get: \.appStoreOverlayIsPresented, - send: Demo.Action.appStoreOverlay(isPresented:) - ) + isPresented: $store.appStoreOverlayIsPresented.sending(\.appStoreOverlay) ) { SKOverlay.AppClipConfiguration(position: .bottom) } From 27e4812d1719fe96e09951af35eb4d8cdfb96af9 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 23:47:22 -0800 Subject: [PATCH 24/61] wip --- .../DailyChallengeResults.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/DailyChallengeFeature/DailyChallengeResults.swift b/Sources/DailyChallengeFeature/DailyChallengeResults.swift index 5bce375f..fcf26996 100644 --- a/Sources/DailyChallengeFeature/DailyChallengeResults.swift +++ b/Sources/DailyChallengeFeature/DailyChallengeResults.swift @@ -6,6 +6,7 @@ import SwiftUI @Reducer public struct DailyChallengeResults { + @ObservableState public struct State: Equatable { public var history: DailyChallengeHistoryResponse? public var leaderboardResults: LeaderboardResults.State @@ -89,24 +90,22 @@ public struct DailyChallengeResults { public struct DailyChallengeResultsView: View { @Environment(\.colorScheme) var colorScheme let store: StoreOf - @ObservedObject var viewStore: ViewStoreOf public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) } public var body: some View { LeaderboardResultsView( - store: self.store.scope(state: \.leaderboardResults, action: \.leaderboardResults), + store: store.scope(state: \.leaderboardResults, action: \.leaderboardResults), title: Text("Daily Challenge"), - subtitle: (self.viewStore.leaderboardResults.resultEnvelope?.outOf) + subtitle: (store.leaderboardResults.resultEnvelope?.outOf) .flatMap { $0 == 0 ? nil : Text("\($0) players") }, isFilterable: true, color: .dailyChallenge, timeScopeLabel: Text(self.timeScopeLabelText), timeScopeMenu: VStack(alignment: .trailing, spacing: .grid(2)) { - CalendarView(store: self.store) + CalendarView(store: store) } ) .padding(.top, .grid(4)) @@ -122,8 +121,8 @@ public struct DailyChallengeResultsView: View { var timeScopeLabelText: LocalizedStringKey { guard - let history = self.viewStore.history, - let timeScope = self.viewStore.leaderboardResults.timeScope + let history = store.history, + let timeScope = store.leaderboardResults.timeScope else { return "Today (so far)" } guard From efa9f2bb4b2a35a8475fd19780009ec28d30f98b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 23:49:39 -0800 Subject: [PATCH 25/61] wip --- Sources/Bloom/BloomBackground.swift | 2 +- Sources/DailyChallengeFeature/CalendarView.swift | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/Bloom/BloomBackground.swift b/Sources/Bloom/BloomBackground.swift index 7761e228..12062a0c 100644 --- a/Sources/Bloom/BloomBackground.swift +++ b/Sources/Bloom/BloomBackground.swift @@ -110,7 +110,7 @@ public struct BloomBackground: View { public var body: some View { Blooms(blooms: self.blooms) - .onChange(of: self.viewStore.bloomCount) { count in + .onChange(of: self.viewStore.bloomCount) { _, count in withAnimation(.easeOut(duration: 1)) { self.renderBlooms(count: count) } diff --git a/Sources/DailyChallengeFeature/CalendarView.swift b/Sources/DailyChallengeFeature/CalendarView.swift index 8615f189..bd2d57d7 100644 --- a/Sources/DailyChallengeFeature/CalendarView.swift +++ b/Sources/DailyChallengeFeature/CalendarView.swift @@ -65,9 +65,7 @@ struct CalendarView: View { let store: StoreOf @ObservedObject var viewStore: ViewStore - init( - store: StoreOf - ) { + init(store: StoreOf) { self.store = store self.viewStore = ViewStore(store, observe: ViewState.init) } From 716bf9c8c92673b2da5f43a0a34cd4d142146546 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 23:54:29 -0800 Subject: [PATCH 26/61] wip --- Sources/Bloom/BloomBackground.swift | 26 ++++++----------------- Sources/CubePreview/CubePreviewView.swift | 21 ++++++------------ Sources/GameCore/Views/GameView.swift | 20 ++++++----------- Sources/TrailerFeature/Trailer.swift | 14 +++--------- 4 files changed, 21 insertions(+), 60 deletions(-) diff --git a/Sources/Bloom/BloomBackground.swift b/Sources/Bloom/BloomBackground.swift index 12062a0c..c30e105c 100644 --- a/Sources/Bloom/BloomBackground.swift +++ b/Sources/Bloom/BloomBackground.swift @@ -66,23 +66,11 @@ public struct Blooms: View { } public struct BloomBackground: View { - public struct ViewState: Equatable { - let bloomCount: Int - let word: String - - public init( - bloomCount: Int, - word: String - ) { - self.bloomCount = bloomCount - self.word = word - } - } + let word: String @State var blooms: [Bloom] = [] @Environment(\.colorScheme) var colorScheme let size: CGSize - let store: Store @State var vertexGenerator: AnyIterator = { var rng = Xoshiro(seed: 0) var vertices: [CGPoint] = [ @@ -100,29 +88,27 @@ public struct BloomBackground: View { return vertices[index % vertices.count] } }() - @ObservedObject var viewStore: ViewStore - public init(size: CGSize, store: Store) { + public init(size: CGSize, word: String) { self.size = size - self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) + self.word = word } public var body: some View { Blooms(blooms: self.blooms) - .onChange(of: self.viewStore.bloomCount) { _, count in + .onChange(of: self.word.count) { _, count in withAnimation(.easeOut(duration: 1)) { self.renderBlooms(count: count) } } - .onAppear { self.renderBlooms(count: self.viewStore.bloomCount) } + .onAppear { self.renderBlooms(count: self.word.count) } } func renderBlooms(count: Int) { if count > self.blooms.count { let colors = Styleguide.letterColors.first { key, _ in - key.contains(self.viewStore.word) + key.contains(self.word) }? .value ?? [] guard colors.count > 0 diff --git a/Sources/CubePreview/CubePreviewView.swift b/Sources/CubePreview/CubePreviewView.swift index c76e0f70..befd0aa7 100644 --- a/Sources/CubePreview/CubePreviewView.swift +++ b/Sources/CubePreview/CubePreviewView.swift @@ -274,23 +274,14 @@ public struct CubePreviewView: View { CubeView(store: self.store.scope(state: \.cubeScenePreview, action: \.cubeScene)) .task { await self.viewStore.send(.task).finish() } } - .background( - self.viewStore.isAnimationReduced - ? nil - : BloomBackground( + .background { + if !self.viewStore.isAnimationReduced { + BloomBackground( size: proxy.size, - store: self.store - .scope( - state: { _ in - BloomBackground.ViewState( - bloomCount: self.viewStore.selectedWordString.count, - word: self.viewStore.selectedWordString - ) - }, - action: absurd - ) + word: self.viewStore.selectedWordString ) - ) + } + } } .onTapGesture { UIView.setAnimationsEnabled(false) diff --git a/Sources/GameCore/Views/GameView.swift b/Sources/GameCore/Views/GameView.swift index cc2d5c42..9ee7aa4f 100644 --- a/Sources/GameCore/Views/GameView.swift +++ b/Sources/GameCore/Views/GameView.swift @@ -126,22 +126,14 @@ public struct GameView: View where Content: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background( - store.isAnimationReduced - ? nil - : BloomBackground( + .background { + if !store.isAnimationReduced { + BloomBackground( size: proxy.size, - store: store.scope( - state: { - BloomBackground.ViewState( - bloomCount: $0.selectedWord.count, - word: $0.selectedWordString - ) - }, - action: absurd - ) + word: store.selectedWordString ) - ) + } + } .background( Color(self.colorScheme == .dark ? .hex(0x111111) : .white) .ignoresSafeArea() diff --git a/Sources/TrailerFeature/Trailer.swift b/Sources/TrailerFeature/Trailer.swift index a9e2da96..a3840886 100644 --- a/Sources/TrailerFeature/Trailer.swift +++ b/Sources/TrailerFeature/Trailer.swift @@ -254,20 +254,12 @@ public struct TrailerView: View { .grid(30) ) } - .background( + .background { BloomBackground( size: proxy.size, - store: store.scope( - state: { - BloomBackground.ViewState( - bloomCount: $0.game.selectedWord.count, - word: $0.game.selectedWordString - ) - }, - action: absurd - ) + word: store.game.selectedWordString ) - ) + } } .padding( self.deviceState.idiom == .pad ? .vertical : [], From 0aaa62e59c4abad4b7fc6d528d54a5538da9d6a4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Dec 2023 23:56:01 -0800 Subject: [PATCH 27/61] wip --- Sources/ChangelogFeature/ChangeView.swift | 41 +++++++++++------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/Sources/ChangelogFeature/ChangeView.swift b/Sources/ChangelogFeature/ChangeView.swift index 122e3058..76d9bc0d 100644 --- a/Sources/ChangelogFeature/ChangeView.swift +++ b/Sources/ChangelogFeature/ChangeView.swift @@ -6,6 +6,7 @@ import Tagged @Reducer public struct Change { + @ObservableState public struct State: Equatable, Identifiable { public var change: Changelog.Change public var isExpanded = false @@ -37,35 +38,33 @@ struct ChangeView: View { let store: StoreOf var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - VStack(alignment: .leading, spacing: .grid(2)) { - HStack { - Text(viewStore.change.version) - .font(.title) + VStack(alignment: .leading, spacing: .grid(2)) { + HStack { + Text(store.change.version) + .font(.title) - if viewStore.change.build == self.currentBuild { - Text("Installed") - .font(.footnote) - .padding(.grid(1)) - .foregroundColor(.white) - .background(Color.gray) - } + if store.change.build == self.currentBuild { + Text("Installed") + .font(.footnote) + .padding(.grid(1)) + .foregroundColor(.white) + .background(Color.gray) + } - Spacer() + Spacer() - if !viewStore.isExpanded { - Button("Show") { - viewStore.send(.showButtonTapped, animation: .default) - } + if !store.isExpanded { + Button("Show") { + store.send(.showButtonTapped, animation: .default) } } + } - if viewStore.isExpanded { - Text(viewStore.change.log) - } + if store.isExpanded { + Text(store.change.log) } - .adaptivePadding(.vertical) } + .adaptivePadding(.vertical) .buttonStyle(PlainButtonStyle()) } } From 7e009ff49d5086e6012d1cc1b160d9da35c07ca2 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 8 Dec 2023 00:05:49 -0800 Subject: [PATCH 28/61] wip --- .../DailyChallengeView.swift | 13 +- .../OnboardingStepView.swift | 386 ++++++++---------- 2 files changed, 182 insertions(+), 217 deletions(-) diff --git a/Sources/DailyChallengeFeature/DailyChallengeView.swift b/Sources/DailyChallengeFeature/DailyChallengeView.swift index 601122ab..93a058e5 100644 --- a/Sources/DailyChallengeFeature/DailyChallengeView.swift +++ b/Sources/DailyChallengeFeature/DailyChallengeView.swift @@ -253,7 +253,7 @@ public struct DailyChallengeView: View { @Environment(\.adaptiveSize) var adaptiveSize @Environment(\.colorScheme) var colorScheme @Environment(\.date) var date - let store: StoreOf + @Bindable var store: StoreOf @ObservedObject var viewStore: ViewStore struct ViewState: Equatable { @@ -288,7 +288,7 @@ public struct DailyChallengeView: View { public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) + self.viewStore = ViewStore(store, observe: ViewState.init) } public var body: some View { @@ -389,11 +389,12 @@ public struct DailyChallengeView: View { ) .edgesIgnoringSafeArea(.bottom) } - .alert(store: self.store.scope(state: \.$destination.alert, action: \.destination.alert)) + .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .navigationDestination( - store: self.store.scope(state: \.$destination.results, action: \.destination.results), - destination: DailyChallengeResultsView.init(store:) - ) + item: $store.scope(state: \.destination?.results, action: \.destination.results) + ) { store in + DailyChallengeResultsView(store: store) + } .notificationsAlert( store: self.store.scope(state: \.$destination, action: \.destination), state: \.notificationsAuthAlert, diff --git a/Sources/OnboardingFeature/OnboardingStepView.swift b/Sources/OnboardingFeature/OnboardingStepView.swift index 0b994776..c15d77e1 100644 --- a/Sources/OnboardingFeature/OnboardingStepView.swift +++ b/Sources/OnboardingFeature/OnboardingStepView.swift @@ -4,44 +4,10 @@ import SwiftUI struct OnboardingStepView: View { let store: StoreOf - @ObservedObject var viewStore: ViewStore @Environment(\.colorScheme) var colorScheme init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) - } - - struct ViewState: Equatable { - let isGetStartedButtonVisible: Bool - let isNextButtonVisible: Bool - let isSubmitButtonVisible: Bool - let presentationStyle: Onboarding.State.PresentationStyle - let step: Onboarding.State.Step - - init(onboardingState state: Onboarding.State) { - self.isGetStartedButtonVisible = state.step == Onboarding.State.Step.allCases.last - self.isNextButtonVisible = - state.step != Onboarding.State.Step.allCases.first - && state.step.isFullscreen - && state.step != Onboarding.State.Step.allCases.last - - switch state.step { - case .step5_SubmitGame: - self.isSubmitButtonVisible = state.game.selectedWordString == "GAME" - case .step8_FindCubes: - self.isSubmitButtonVisible = state.game.selectedWordString == "CUBES" - case .step12_CubeIsShaking: - self.isSubmitButtonVisible = state.game.selectedWordString.isRemove - case .step16_FindAnyWord: - self.isSubmitButtonVisible = !state.game.selectedWordString.isEmpty - default: - self.isSubmitButtonVisible = false - } - - self.presentationStyle = state.presentationStyle - self.step = state.step - } } var body: some View { @@ -50,182 +16,157 @@ struct OnboardingStepView: View { ZStack(alignment: .bottom) { VStack { - if self.viewStore.step.isFullscreen { + if store.step.isFullscreen { Spacer() } Group { - Group { - if self.viewStore.step == .step1_Welcome { - FullscreenStepView( - Text("Hello!\nWelcome to ") - + Text("isowords").fontWeight(.medium) - + Text(", a word game.") - ) - } - if self.viewStore.step == .step2_FindWordsOnCube { - FullscreenStepView( - Text("The point of the game is to find words on a ") - + Text("cube").fontWeight(.medium) - + Text(".") - ) - } - if self.viewStore.step == .step3_ConnectLettersTouching { - FullscreenStepView( - Text("Words are formed by connecting letters that are ") - + Text("touching").fontWeight(.medium) - + Text(".") - ) - } - if self.viewStore.step == .step4_FindGame { - InlineStepView( - height: height, - Text("Let’s try!\nConnect letters to form ") - + Text("GAME").fontWeight(.medium) - + Text(".") - ) - } - if self.viewStore.step == .step5_SubmitGame { - InlineStepView( - height: height, - Text("Now submit the word by tapping the ") - + Text("thumbs up").fontWeight(.medium) - + Text(".") - ) - } - if self.viewStore.step == .step6_Congrats { - InlineStepView( - height: height, - Text("Well done!") - ) - } - if self.viewStore.step == .step7_BiggerCube { - FullscreenStepView( - Text("Let’s find another word, but this time with more ") - + Text("letters revealed").fontWeight(.medium) - + Text(".") - ) - } - if self.viewStore.step == .step8_FindCubes { - InlineStepView( - height: height, - Text("Find and submit the word ") - + Text("CUBES").fontWeight(.medium) - + Text(".") - ) - } - if self.viewStore.step == .step9_Congrats { - InlineStepView( - height: height, - Text("You got it!") - ) - } - if self.viewStore.step == .step10_CubeDisappear { - FullscreenStepView( - Text("You can use each letter three times before the cube ") - + Text("disappears").fontWeight(.medium) - + Text(".") - ) - } - } - Group { - if self.viewStore.step == .step11_FindRemove { - InlineStepView( - height: height, - Text("Let’s try it!\nFind the word ") - + Text("REMOVE").fontWeight(.medium) - + Text(".") - ) - } - if self.viewStore.step == .step12_CubeIsShaking { - InlineStepView( - height: height, - Text("The shaking cube means it will ") - + Text("disappear").fontWeight(.medium) - + Text(". Now submit the word.") - ) - } - if self.viewStore.step == .step13_Congrats { - InlineStepView( - height: height, - Text("Ohhhhhhh,\n").italic() - + Text("interesting!") - ) - } - if self.viewStore.step == .step14_LettersRevealed { - FullscreenStepView( - Text( - "As cubes are removed the letters inside are revealed, helping you find more " - ) - + Text("words").fontWeight(.medium) - + Text(".") + switch store.step { + case .step1_Welcome: + FullscreenStepView( + Text("Hello!\nWelcome to ") + + Text("isowords").fontWeight(.medium) + + Text(", a word game.") + ) + case .step2_FindWordsOnCube: + FullscreenStepView( + Text("The point of the game is to find words on a ") + + Text("cube").fontWeight(.medium) + + Text(".") + ) + case .step3_ConnectLettersTouching: + FullscreenStepView( + Text("Words are formed by connecting letters that are ") + + Text("touching").fontWeight(.medium) + + Text(".") + ) + case .step4_FindGame: + InlineStepView( + height: height, + Text("Let’s try!\nConnect letters to form ") + + Text("GAME").fontWeight(.medium) + + Text(".") + ) + case .step5_SubmitGame: + InlineStepView( + height: height, + Text("Now submit the word by tapping the ") + + Text("thumbs up").fontWeight(.medium) + + Text(".") + ) + case .step6_Congrats: + InlineStepView( + height: height, + Text("Well done!") + ) + case .step7_BiggerCube: + FullscreenStepView( + Text("Let’s find another word, but this time with more ") + + Text("letters revealed").fontWeight(.medium) + + Text(".") + ) + case .step8_FindCubes: + InlineStepView( + height: height, + Text("Find and submit the word ") + + Text("CUBES").fontWeight(.medium) + + Text(".") + ) + case .step9_Congrats: + InlineStepView( + height: height, + Text("You got it!") + ) + case .step10_CubeDisappear: + FullscreenStepView( + Text("You can use each letter three times before the cube ") + + Text("disappears").fontWeight(.medium) + + Text(".") + ) + case .step11_FindRemove: + InlineStepView( + height: height, + Text("Let’s try it!\nFind the word ") + + Text("REMOVE").fontWeight(.medium) + + Text(".") + ) + case .step12_CubeIsShaking: + InlineStepView( + height: height, + Text("The shaking cube means it will ") + + Text("disappear").fontWeight(.medium) + + Text(". Now submit the word.") + ) + case .step13_Congrats: + InlineStepView( + height: height, + Text("Ohhhhhhh,\n").italic() + + Text("interesting!") + ) + case .step14_LettersRevealed: + FullscreenStepView( + Text( + "As cubes are removed the letters inside are revealed, helping you find more " ) - } - if self.viewStore.step == .step15_FullCube { + + Text("words").fontWeight(.medium) + + Text(".") + ) + case .step15_FullCube: + FullscreenStepView( + Text("Good job so far, but the real game is played with all letters ") + + Text("revealed").fontWeight(.medium) + + Text(".") + ) + case .step16_FindAnyWord: + InlineStepView( + height: height, + Text("Find ") + + Text("any").fontWeight(.medium) + + Text(" word on the full cube.") + ) + case .step17_Congrats: + InlineStepView( + height: height, + Text("That’s a great one!") + ) + case .step18_OneLastThing: + FullscreenStepView( + Text("One last thing.\nYou can remove a cube by double-tapping it. ") + + Text("This can be handy for exposing ") + + Text("more letters").fontWeight(.medium) + + Text(".") + ) + case .step19_DoubleTapToRemove: + InlineStepView( + height: height, + Text("Let’s try it.\nDouble tap any cube to ") + + Text("remove").fontWeight(.medium) + + Text(" it.") + ) + case .step20_Congrats: + InlineStepView( + height: height, + Text("Perfect!") + ) + case .step21_PlayAGameYourself: + switch store.presentationStyle { + case .demo: FullscreenStepView( - Text("Good job so far, but the real game is played with all letters ") - + Text("revealed").fontWeight(.medium) - + Text(".") - ) - } - if self.viewStore.step == .step16_FindAnyWord { - InlineStepView( - height: height, - Text("Find ") - + Text("any").fontWeight(.medium) - + Text(" word on the full cube.") + Text("Ok, ready?\n Let’s try a 3 minute timed game!") ) - } - if self.viewStore.step == .step17_Congrats { - InlineStepView( - height: height, - Text("That’s a great one!") - ) - } - if self.viewStore.step == .step18_OneLastThing { + + case .firstLaunch, .help: FullscreenStepView( - Text("One last thing.\nYou can remove a cube by double-tapping it. ") - + Text("This can be handy for exposing ") - + Text("more letters").fontWeight(.medium) - + Text(".") - ) - } - if self.viewStore.step == .step19_DoubleTapToRemove { - InlineStepView( - height: height, - Text("Let’s try it.\nDouble tap any cube to ") - + Text("remove").fontWeight(.medium) - + Text(" it.") + Text("Ok, there’s more strategy to the game, but the only way to learn is to ") + + Text("play a game yourself").fontWeight(.medium) + + Text("!") ) } } - Group { - if self.viewStore.step == .step20_Congrats { - InlineStepView( - height: height, - Text("Perfect!") - ) - } - if self.viewStore.step == .step21_PlayAGameYourself { - switch self.viewStore.presentationStyle { - case .demo: - FullscreenStepView( - Text("Ok, ready?\n Let’s try a 3 minute timed game!") - ) - - case .firstLaunch, .help: - FullscreenStepView( - Text("Ok, there’s more strategy to the game, but the only way to learn is to ") - + Text("play a game yourself").fontWeight(.medium) - + Text("!") - ) - } - } - } } .foregroundColor( self.colorScheme == .dark - ? self.viewStore.step.color + ? store.step.color : Color.isowordsBlack ) .transition( @@ -247,29 +188,29 @@ struct OnboardingStepView: View { .padding(.bottom, 80) Group { - if self.viewStore.isNextButtonVisible { + if store.isNextButtonVisible { Button { - self.viewStore.send(.nextButtonTapped, animation: .default) + store.send(.nextButtonTapped, animation: .default) } label: { Image(systemName: "arrow.right") .frame(width: 80, height: 80) .background( self.colorScheme == .dark - ? self.viewStore.step.color + ? store.step.color : Color.isowordsBlack ) .foregroundColor( self.colorScheme == .dark ? Color.isowordsBlack - : self.viewStore.step.color + : store.step.color ) .font(.system(size: 30)) .clipShape(Circle()) } - } else if !self.viewStore.step.isFullscreen { - if self.viewStore.isSubmitButtonVisible { + } else if !store.step.isFullscreen { + if store.isSubmitButtonVisible { Button { - self.viewStore.send( + store.send( .game(.submitButtonTapped(reaction: nil)), animation: .default ) } label: { @@ -277,24 +218,24 @@ struct OnboardingStepView: View { .frame(width: 80, height: 80) .background( self.colorScheme == .dark - ? self.viewStore.step.color + ? store.step.color : Color.isowordsBlack ) .foregroundColor( self.colorScheme == .dark ? Color.isowordsBlack - : self.viewStore.step.color + : store.step.color ) .font(.system(size: 30)) .clipShape(Circle()) } } - } else if self.viewStore.isGetStartedButtonVisible { + } else if store.isGetStartedButtonVisible { Button { - self.viewStore.send(.getStartedButtonTapped, animation: .default) + store.send(.getStartedButtonTapped, animation: .default) } label: { HStack { - switch self.viewStore.presentationStyle { + switch store.presentationStyle { case .demo: Text("Let’s play!") case .firstLaunch, .help: @@ -307,11 +248,11 @@ struct OnboardingStepView: View { .buttonStyle( ActionButtonStyle( backgroundColor: self.colorScheme == .dark - ? self.viewStore.step.color + ? store.step.color : .isowordsBlack, foregroundColor: self.colorScheme == .dark ? .isowordsBlack - : self.viewStore.step.color + : store.step.color ) ) } @@ -326,7 +267,7 @@ struct OnboardingStepView: View { ) } } - .task { await self.viewStore.send(.task).finish() } + .task { await store.send(.task).finish() } .alert(store: self.store.scope(state: \.$alert, action: \.alert)) } } @@ -362,6 +303,29 @@ private struct InlineStepView: View { } } +fileprivate extension Onboarding.State { + var isGetStartedButtonVisible: Bool { self.step == Onboarding.State.Step.allCases.last } + var isNextButtonVisible: Bool { + self.step != Onboarding.State.Step.allCases.first + && self.step.isFullscreen + && self.step != Onboarding.State.Step.allCases.last + } + var isSubmitButtonVisible: Bool { + switch self.step { + case .step5_SubmitGame: + return self.game.selectedWordString == "GAME" + case .step8_FindCubes: + return self.game.selectedWordString == "CUBES" + case .step12_CubeIsShaking: + return self.game.selectedWordString.isRemove + case .step16_FindAnyWord: + return !self.game.selectedWordString.isEmpty + default: + return false + } + } +} + extension Onboarding.State.Step { var color: Color { let t = Double(self.rawValue) / Double(Self.allCases.count - 1) From de50bc5bf9779867f5b37fcfc36a10d8d2066de5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 8 Dec 2023 00:06:53 -0800 Subject: [PATCH 29/61] wip --- .../LeaderboardResultsView.swift | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/Sources/LeaderboardFeature/LeaderboardResultsView.swift b/Sources/LeaderboardFeature/LeaderboardResultsView.swift index e2058e3e..56bcc0c4 100644 --- a/Sources/LeaderboardFeature/LeaderboardResultsView.swift +++ b/Sources/LeaderboardFeature/LeaderboardResultsView.swift @@ -4,6 +4,7 @@ import SwiftUI // NB: `@Reducer` prevents us from synthesizing conditional conformances in this file. public struct LeaderboardResults: Reducer { + @ObservableState public struct State { public var gameMode: GameMode public var isLoading: Bool @@ -128,7 +129,6 @@ where let title: Text? let store: StoreOf> - @ObservedObject var viewStore: ViewStoreOf> public init( store: StoreOf>, @@ -143,7 +143,6 @@ where self.isFilterable = isFilterable self.subtitle = subtitle self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) self.timeScopeLabel = timeScopeLabel self.timeScopeMenu = timeScopeMenu self.title = title @@ -157,12 +156,12 @@ where .adaptiveFont(.matterMedium, size: 16) .frame(maxWidth: .infinity, alignment: .leading) .foregroundColor(self.colorScheme == .dark ? self.color : .isowordsBlack) - .redacted(reason: self.viewStore.isLoading ? .placeholder : []) + .redacted(reason: store.isLoading ? .placeholder : []) Spacer() Button { - self.viewStore.send(.tappedTimeScopeLabel, animation: .default) + store.send(.tappedTimeScopeLabel, animation: .default) } label: { HStack { self.timeScopeLabel @@ -170,7 +169,7 @@ where Image(systemName: "chevron.down") .font(.system(size: 10)) .rotationEffect( - .degrees(self.viewStore.isTimeScopeMenuVisible ? -180 : 0) + .degrees(store.isTimeScopeMenuVisible ? -180 : 0) ) } .padding(.vertical, 8) @@ -186,11 +185,11 @@ where HStack(spacing: .grid(4)) { ForEach(GameMode.allCases) { gameMode in Button { - self.viewStore.send(.gameModeButtonTapped(gameMode)) + store.send(.gameModeButtonTapped(gameMode)) } label: { Text(gameMode.title) .adaptiveFont(.matterMedium, size: 12) - .opacity(self.viewStore.gameMode == gameMode ? 1 : 0.4) + .opacity(store.gameMode == gameMode ? 1 : 0.4) } .buttonStyle(PlainButtonStyle()) } @@ -213,33 +212,33 @@ where Group { ForEach( - self.viewStore.resultEnvelope?.contiguousResults ?? [], id: \.id + store.resultEnvelope?.contiguousResults ?? [], id: \.id ) { result in Button { - self.viewStore.send(.tappedRow(id: result.id)) + store.send(.tappedRow(id: result.id)) } label: { ResultRow(color: self.color, result: result) } } - if let result = self.viewStore.resultEnvelope?.nonContiguousResult { + if let result = store.resultEnvelope?.nonContiguousResult { Image(systemName: "ellipsis") .opacity(0.4) .adaptivePadding(.vertical, .grid(5)) .adaptiveFont(.matterMedium, size: 16) Button { - self.viewStore.send(.tappedRow(id: result.id)) + store.send(.tappedRow(id: result.id)) } label: { ResultRow(color: self.color, result: result) } } - if self.viewStore.nonDisplayedResultsCount > 0 { + if store.nonDisplayedResultsCount > 0 { VStack(spacing: .grid(5)) { Image(systemName: "ellipsis") .opacity(0.4) - Text("and \(self.viewStore.nonDisplayedResultsCount) more!") + Text("and \(store.nonDisplayedResultsCount) more!") } .adaptivePadding(.top, .grid(5)) .adaptiveFont(.matterMedium, size: 16) @@ -248,13 +247,13 @@ where Spacer().frame(height: .grid(5)) } - .disabled(self.viewStore.isLoading) - .redacted(reason: self.viewStore.isLoading ? .placeholder : []) + .disabled(store.isLoading) + .redacted(reason: store.isLoading ? .placeholder : []) } .background(self.color) .foregroundColor(.isowordsBlack) .overlay( - self.viewStore.isLoading + store.isLoading ? ZStack { Color.black .opacity(0.4) @@ -264,17 +263,17 @@ where : nil ) .overlay( - self.viewStore.isTimeScopeMenuVisible + store.isTimeScopeMenuVisible ? Color.black.opacity(0.4) .onTapGesture { - self.viewStore.send(.dismissTimeScopeMenu, animation: .default) + store.send(.dismissTimeScopeMenu, animation: .default) } : nil ) .continuousCornerRadius(.grid(3)) } .overlay( - self.viewStore.isTimeScopeMenuVisible + store.isTimeScopeMenuVisible ? VStack { self.timeScopeMenu .adaptiveFont(.matterMedium, size: 12) @@ -296,7 +295,7 @@ where alignment: .topTrailing ) - .task { await self.viewStore.send(.task).finish() } + .task { await store.send(.task).finish() } } } From 319387ebb551f53b65164e822ab98b5b18452a0a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 8 Dec 2023 00:07:29 -0800 Subject: [PATCH 30/61] wip --- Sources/GameOverFeature/GameOverView.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/GameOverFeature/GameOverView.swift b/Sources/GameOverFeature/GameOverView.swift index e1395972..ce6ace35 100644 --- a/Sources/GameOverFeature/GameOverView.swift +++ b/Sources/GameOverFeature/GameOverView.swift @@ -499,12 +499,10 @@ public struct GameOverView: View { .padding(.vertical, .grid(12)) } - IfLetStore( - self.store.scope( - state: \.destination?.upgradeInterstitial, - action: \.destination.upgradeInterstitial.presented - ) - ) { store in + if let store = store.scope( + state: \.destination?.upgradeInterstitial, + action: \.destination.upgradeInterstitial.presented + ) { UpgradeInterstitialView(store: store) .transition(.opacity) } From c2f288fd734875cf4622d78c908790ac9391a465 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 8 Dec 2023 00:10:20 -0800 Subject: [PATCH 31/61] wip --- Sources/GameOverFeature/GameOverView.swift | 4 +++- Sources/OnboardingFeature/OnboardingStepView.swift | 10 +++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Sources/GameOverFeature/GameOverView.swift b/Sources/GameOverFeature/GameOverView.swift index ce6ace35..a7e493e1 100644 --- a/Sources/GameOverFeature/GameOverView.swift +++ b/Sources/GameOverFeature/GameOverView.swift @@ -19,6 +19,7 @@ import UserDefaultsClient public struct GameOver { @Reducer public struct Destination { + @ObservableState public enum State: Equatable { case notificationsAuthAlert(NotificationsAuthAlert.State = .init()) case upgradeInterstitial(UpgradeInterstitial.State = .init()) @@ -39,10 +40,11 @@ public struct GameOver { } } + @ObservableState public struct State: Equatable { public var completedGame: CompletedGame public var dailyChallenges: [FetchTodaysDailyChallengeResponse] - @PresentationState public var destination: Destination.State? + @Presents public var destination: Destination.State? public var gameModeIsLoading: GameMode? public var isDemo: Bool public var isNotificationMenuPresented: Bool diff --git a/Sources/OnboardingFeature/OnboardingStepView.swift b/Sources/OnboardingFeature/OnboardingStepView.swift index c15d77e1..8e4770a3 100644 --- a/Sources/OnboardingFeature/OnboardingStepView.swift +++ b/Sources/OnboardingFeature/OnboardingStepView.swift @@ -3,13 +3,9 @@ import Styleguide import SwiftUI struct OnboardingStepView: View { - let store: StoreOf + @Bindable var store: StoreOf @Environment(\.colorScheme) var colorScheme - init(store: StoreOf) { - self.store = store - } - var body: some View { GeometryReader { proxy in let height = proxy.size.height / 4 @@ -259,7 +255,7 @@ struct OnboardingStepView: View { } .padding() .transition( - AnyTransition.asymmetric( + .asymmetric( insertion: .offset(x: 0, y: 50), removal: .offset(x: 0, y: 50) ) @@ -268,7 +264,7 @@ struct OnboardingStepView: View { } } .task { await store.send(.task).finish() } - .alert(store: self.store.scope(state: \.$alert, action: \.alert)) + .alert($store.scope(state: \.alert, action: \.alert)) } } From be08617dc82666b87c8a40e37b68433ce5c751a5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 8 Dec 2023 00:15:07 -0800 Subject: [PATCH 32/61] wip --- Sources/CubePreview/CubePreviewView.swift | 61 +++++++++-------------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/Sources/CubePreview/CubePreviewView.swift b/Sources/CubePreview/CubePreviewView.swift index befd0aa7..407e9b95 100644 --- a/Sources/CubePreview/CubePreviewView.swift +++ b/Sources/CubePreview/CubePreviewView.swift @@ -11,6 +11,7 @@ import UserSettingsClient @Reducer public struct CubePreview { + @ObservableState public struct State: Equatable { var cubes: Puzzle var enableGyroMotion: Bool @@ -18,8 +19,8 @@ public struct CubePreview { var isOnLowPowerMode: Bool var moveIndex: Int var moves: Moves - @BindingState var nub: CubeSceneView.ViewState.NubState - @BindingState var selectedCubeFaces: [IndexedCubeFace] + var nub: CubeSceneView.ViewState.NubState + var selectedCubeFaces: [IndexedCubeFace] public init( cubes: ArchivablePuzzle, @@ -31,8 +32,9 @@ public struct CubePreview { ) { @Dependency(\.userSettings) var userSettings - self.cubes = .init(archivableCubes: cubes) - apply(moves: moves[0.. - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - let isAnimationReduced: Bool - let selectedWordIsFinalWord: Bool - let selectedWordScore: Int? - let selectedWordString: String - - init(state: CubePreview.State) { - self.isAnimationReduced = state.isAnimationReduced - self.selectedWordString = state.cubes.string(from: state.selectedCubeFaces) - self.selectedWordIsFinalWord = state.finalWordString == self.selectedWordString - self.selectedWordScore = - self.selectedWordIsFinalWord - ? state.moves[state.moveIndex].score - : nil - } - } public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) } public var body: some View { GeometryReader { proxy in ZStack(alignment: .top) { - if !self.viewStore.selectedWordString.isEmpty { - (Text(self.viewStore.selectedWordString) + if !store.selectedWordString.isEmpty { + (Text(store.selectedWordString) + self.scoreText .baselineOffset( (self.deviceState.idiom == .pad ? 2 : 1) * 16 @@ -262,36 +249,36 @@ public struct CubePreviewView: View { .matterSemiBold, size: (self.deviceState.idiom == .pad ? 2 : 1) * 32 ) - .opacity(self.viewStore.selectedWordIsFinalWord ? 1 : 0.5) + .opacity(store.selectedWordIsFinalWord ? 1 : 0.5) .allowsTightening(true) .minimumScaleFactor(0.2) .lineLimit(1) .transition(.opacity) - .animation(nil, value: self.viewStore.selectedWordString) + .animation(nil, value: store.selectedWordString) .adaptivePadding(.top, .grid(16)) } CubeView(store: self.store.scope(state: \.cubeScenePreview, action: \.cubeScene)) - .task { await self.viewStore.send(.task).finish() } + .task { await store.send(.task).finish() } } .background { - if !self.viewStore.isAnimationReduced { + if !store.isAnimationReduced { BloomBackground( size: proxy.size, - word: self.viewStore.selectedWordString + word: store.selectedWordString ) } } } .onTapGesture { UIView.setAnimationsEnabled(false) - self.viewStore.send(.tap) + store.send(.tap) UIView.setAnimationsEnabled(true) } } var scoreText: Text { - self.viewStore.selectedWordScore.map { + store.selectedWordScore.map { Text(" \($0)") } ?? Text("") } From 8abe58f037407e10083e95dbdbfb78caef36c247 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 8 Dec 2023 10:27:37 -0800 Subject: [PATCH 33/61] wip --- App/iOS/App.swift | 4 ++-- Sources/GameCore/Views/PlayersAndScoresView.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/App/iOS/App.swift b/App/iOS/App.swift index b1888f30..48c8ca8b 100644 --- a/App/iOS/App.swift +++ b/App/iOS/App.swift @@ -65,8 +65,8 @@ struct IsowordsApp: App { WindowGroup { AppView(store: self.appDelegate.store) } - .onChange(of: self.scenePhase) { - self.appDelegate.store.send(.didChangeScenePhase($0)) + .onChange(of: self.scenePhase) { _, newPhase in + self.appDelegate.store.send(.didChangeScenePhase(newPhase)) } } } diff --git a/Sources/GameCore/Views/PlayersAndScoresView.swift b/Sources/GameCore/Views/PlayersAndScoresView.swift index 51970c90..c12309fb 100644 --- a/Sources/GameCore/Views/PlayersAndScoresView.swift +++ b/Sources/GameCore/Views/PlayersAndScoresView.swift @@ -65,12 +65,12 @@ struct PlayersAndScoresView: View { self.yourImage = image } } - .onChange(of: self.viewStore.opponent) { player in + .onChange(of: self.viewStore.opponent) { _, player in player?.rawValue?.loadPhoto(for: .small) { image, _ in self.opponentImage = image } } - .onChange(of: self.viewStore.you) { player in + .onChange(of: self.viewStore.you) { _, player in player?.rawValue?.loadPhoto(for: .small) { image, _ in self.yourImage = image } From f59b3ded631570eac377e42bd43cd45a159980b5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 8 Dec 2023 13:07:42 -0800 Subject: [PATCH 34/61] wip --- .../xcshareddata/swiftpm/Package.resolved | 365 ++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..51be4de8 --- /dev/null +++ b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,365 @@ +{ + "pins" : [ + { + "identity" : "async-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/async-kit.git", + "state" : { + "revision" : "7ece208cd401687641c88367a00e3ea2b04311f1", + "version" : "1.19.0" + } + }, + { + "identity" : "bluecryptor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/IBM-Swift/BlueCryptor.git", + "state" : { + "revision" : "ee5880e031da4c609f372cf7472476ab51d5dd19", + "version" : "1.0.200" + } + }, + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" + } + }, + { + "identity" : "postgres-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/postgres-kit", + "state" : { + "revision" : "cbbe3ef8a0a8800301b8b76ab0f09dfc9e7306a2", + "version" : "2.2.0" + } + }, + { + "identity" : "postgres-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/postgres-nio.git", + "state" : { + "revision" : "abca6b390235ae337999d367c40cc40c99629385", + "version" : "1.18.1" + } + }, + { + "identity" : "sql-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/sql-kit.git", + "state" : { + "revision" : "b2f128cb62a3abfbb1e3b2893ff3ee69e70f4f0f", + "version" : "3.28.0" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-backtrace", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-backtrace", + "state" : { + "revision" : "f2fd8c4845a123419c348e0bc4b3839c414077d5", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "a5521dde99570789d8cb7c43e51418d7cd1a87ca", + "version" : "1.1.2" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", + "version" : "1.0.5" + } + }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture", + "state" : { + "branch" : "observation-beta", + "revision" : "a1f2ebbe973e35d4d476da4e0a297e0002af25a6" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto", + "state" : { + "revision" : "ddb07e896a2a8af79512543b1c7eb9797f8898a5", + "version" : "1.1.7" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", + "version" : "1.1.2" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "9783b58167f7618cb86011156e741cbc6f4cc864", + "version" : "1.1.2" + } + }, + { + "identity" : "swift-gen", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-gen", + "state" : { + "revision" : "5bd20fb662e1ead7ee47df6bb0a15398295f2e06", + "version" : "0.4.0" + } + }, + { + "identity" : "swift-html", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-html", + "state" : { + "branch" : "14d01d1", + "revision" : "14d01d19e43598167a8f8965af478285835ca010" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "1827dc94bdab2eb5f2fc804e9b0cb43574282566", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" + } + }, + { + "identity" : "swift-metrics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-metrics.git", + "state" : { + "revision" : "971ba26378ab69c43737ee7ba967a896cb74c0d1", + "version" : "2.4.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c", + "version" : "2.62.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51", + "version" : "1.20.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "3bd9004b9d685ed6b629760fc84903e48efec806", + "version" : "1.29.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", + "version" : "2.25.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e", + "version" : "1.20.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-overture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-overture", + "state" : { + "revision" : "7977acd7597f413717058acc1e080731249a1d7e", + "version" : "0.5.0" + } + }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing", + "state" : { + "revision" : "a0e7d73f462c1c38c59dc40a3969ac40cea42950", + "version" : "0.13.0" + } + }, + { + "identity" : "swift-prelude", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-prelude", + "state" : { + "branch" : "7ff9911", + "revision" : "7ff9911580b2f9b7ead5375099781f28b8aad6a8" + } + }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "59b663f68e69f27a87b45de48cb63264b8194605", + "version" : "1.15.1" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version" : "509.0.2" + } + }, + { + "identity" : "swift-tagged", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-tagged", + "state" : { + "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", + "version" : "0.10.0" + } + }, + { + "identity" : "swift-url-routing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-url-routing", + "state" : { + "revision" : "13f65cec4de950ba30f08d9bc4abcfa41f9479b9", + "version" : "0.6.0" + } + }, + { + "identity" : "swift-web", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-web", + "state" : { + "branch" : "2ad82ec", + "revision" : "2ad82ec94983029566d009f12dca8d235983616a" + } + }, + { + "identity" : "swiftawssignaturev4", + "kind" : "remoteSourceControl", + "location" : "https://github.com/crspybits/SwiftAWSSignatureV4", + "state" : { + "revision" : "c66f2db1211ad1969e3d11791b9de62361c78f45", + "version" : "1.2.1" + } + }, + { + "identity" : "swiftui-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swiftui-navigation", + "state" : { + "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc", + "version" : "1.2.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", + "version" : "1.0.2" + } + } + ], + "version" : 2 +} From e57166fea2a16114ce4c47d8e1607d69d8d2bb36 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 8 Dec 2023 23:49:25 -0800 Subject: [PATCH 35/61] wip --- Sources/DailyChallengeFeature/DailyChallengeView.swift | 9 ++++++--- Sources/GameOverFeature/GameOverView.swift | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/DailyChallengeFeature/DailyChallengeView.swift b/Sources/DailyChallengeFeature/DailyChallengeView.swift index 93a058e5..e4ee445c 100644 --- a/Sources/DailyChallengeFeature/DailyChallengeView.swift +++ b/Sources/DailyChallengeFeature/DailyChallengeView.swift @@ -396,9 +396,12 @@ public struct DailyChallengeView: View { DailyChallengeResultsView(store: store) } .notificationsAlert( - store: self.store.scope(state: \.$destination, action: \.destination), - state: \.notificationsAuthAlert, - action: { .notificationsAuthAlert($0) } + store: self.store.scope( + state: \.$destination.notificationsAuthAlert, + action: \.destination.notificationsAuthAlert + ), + state: { $0 }, + action: { $0 } ) } } diff --git a/Sources/GameOverFeature/GameOverView.swift b/Sources/GameOverFeature/GameOverView.swift index a7e493e1..2a938a9b 100644 --- a/Sources/GameOverFeature/GameOverView.swift +++ b/Sources/GameOverFeature/GameOverView.swift @@ -516,9 +516,12 @@ public struct GameOverView: View { ) .task { await self.viewStore.send(.task).finish() } .notificationsAlert( - store: self.store.scope(state: \.$destination, action: \.destination), - state: \.notificationsAuthAlert, - action: { .notificationsAuthAlert($0) } + store: self.store.scope( + state: \.$destination.notificationsAuthAlert, + action: \.destination.notificationsAuthAlert + ), + state: { $0 }, + action: { $0 } ) .sheet(isPresented: self.$isSharePresented) { ActivityView(activityItems: [URL(string: "https://www.isowords.xyz")!]) From 8a8ff1fbd789e7f009a695a152321762bf271239 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 8 Dec 2023 23:56:55 -0800 Subject: [PATCH 36/61] wip --- .../DailyChallengeView.swift | 4 +-- Sources/GameOverFeature/GameOverView.swift | 4 +-- .../NotificationsAuthAlert.swift | 28 +++++++++---------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/Sources/DailyChallengeFeature/DailyChallengeView.swift b/Sources/DailyChallengeFeature/DailyChallengeView.swift index e4ee445c..7917bd5b 100644 --- a/Sources/DailyChallengeFeature/DailyChallengeView.swift +++ b/Sources/DailyChallengeFeature/DailyChallengeView.swift @@ -399,9 +399,7 @@ public struct DailyChallengeView: View { store: self.store.scope( state: \.$destination.notificationsAuthAlert, action: \.destination.notificationsAuthAlert - ), - state: { $0 }, - action: { $0 } + ) ) } } diff --git a/Sources/GameOverFeature/GameOverView.swift b/Sources/GameOverFeature/GameOverView.swift index 2a938a9b..cc166c6f 100644 --- a/Sources/GameOverFeature/GameOverView.swift +++ b/Sources/GameOverFeature/GameOverView.swift @@ -519,9 +519,7 @@ public struct GameOverView: View { store: self.store.scope( state: \.$destination.notificationsAuthAlert, action: \.destination.notificationsAuthAlert - ), - state: { $0 }, - action: { $0 } + ) ) .sheet(isPresented: self.$isSharePresented) { ActivityView(activityItems: [URL(string: "https://www.isowords.xyz")!]) diff --git a/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift b/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift index ef017fd8..34b60a6a 100644 --- a/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift +++ b/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift @@ -56,27 +56,25 @@ public struct NotificationsAuthAlert { } extension View { - public func notificationsAlert( - store: Store, PresentationAction>, - state toAlertState: @escaping (DestinationState) -> NotificationsAuthAlert.State?, - action fromAlertAction: @escaping (NotificationsAuthAlert.Action) -> DestinationAction + public func notificationsAlert( + store: Store< + PresentationState, + PresentationAction + > ) -> some View { - self.modifier( - NotificationsAuthAlertViewModifier( - store: store, toAlertState: toAlertState, fromAlertAction: fromAlertAction - ) - ) + self.modifier(NotificationsAuthAlertViewModifier(store: store)) } } -struct NotificationsAuthAlertViewModifier: ViewModifier { - let store: Store, PresentationAction> - let toAlertState: (DestinationState) -> NotificationsAuthAlert.State? - let fromAlertAction: (NotificationsAuthAlert.Action) -> DestinationAction +struct NotificationsAuthAlertViewModifier: ViewModifier { + let store: Store< + PresentationState, + PresentationAction + > func body(content: Content) -> some View { WithViewStore( - self.store, observe: { $0.wrappedValue.flatMap(self.toAlertState) } + self.store, observe: { $0.wrappedValue } ) { viewStore in content .overlay { @@ -92,7 +90,7 @@ struct NotificationsAuthAlertViewModifier: ZStack(alignment: .topTrailing) { NotificationsAuthAlertView( store: store.scope( - state: { _ in state }, action: { .presented(fromAlertAction($0)) } + state: { _ in state }, action: { .presented($0) } ) ) From a34be6fa91b77d6336ff74d8920831343dc4ea60 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 00:01:12 -0800 Subject: [PATCH 37/61] wip --- .../GameCore/Views/PlayersAndScoresView.swift | 67 ++++++++----------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/Sources/GameCore/Views/PlayersAndScoresView.swift b/Sources/GameCore/Views/PlayersAndScoresView.swift index c12309fb..abe432a2 100644 --- a/Sources/GameCore/Views/PlayersAndScoresView.swift +++ b/Sources/GameCore/Views/PlayersAndScoresView.swift @@ -8,69 +8,40 @@ struct PlayersAndScoresView: View { @State var opponentImage: UIImage? let store: StoreOf @State var yourImage: UIImage? - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - let isYourTurn: Bool - let opponent: ComposableGameCenter.Player? - let opponentScore: Int - let you: ComposableGameCenter.Player? - let yourScore: Int - - init(state: Game.State) { - self.isYourTurn = state.isYourTurn - self.opponent = state.gameContext.turnBased?.otherParticipant?.player - self.you = state.gameContext.turnBased?.localPlayer.player - self.yourScore = - state.gameContext.turnBased?.localPlayerIndex - .flatMap { state.turnBasedScores[$0] } - ?? (state.gameContext.is(\.turnBased) ? 0 : state.currentScore) - self.opponentScore = - state.gameContext.turnBased?.otherPlayerIndex - .flatMap { state.turnBasedScores[$0] } ?? 0 - } - } - - public init( - store: StoreOf - ) { - self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) - } var body: some View { HStack(spacing: 0) { PlayerView( - displayName: (self.viewStore.you?.displayName).map { LocalizedStringKey($0) } ?? "You", + displayName: (store.you?.displayName).map { LocalizedStringKey($0) } ?? "You", image: self.defaultYourImage ?? self.yourImage, - isPlayerTurn: self.viewStore.isYourTurn, + isPlayerTurn: store.isYourTurn, isYou: true, - score: self.viewStore.yourScore + score: store.yourScore ) PlayerView( - displayName: (self.viewStore.opponent?.displayName).map { LocalizedStringKey($0) } + displayName: (store.opponent?.displayName).map { LocalizedStringKey($0) } ?? "Your opponent", image: self.defaultOpponentImage ?? self.opponentImage, - isPlayerTurn: !self.viewStore.isYourTurn, + isPlayerTurn: !store.isYourTurn, isYou: false, - score: self.viewStore.opponentScore + score: store.opponentScore ) } .onAppear { - self.viewStore.opponent?.rawValue?.loadPhoto(for: .small) { image, _ in + store.opponent?.rawValue?.loadPhoto(for: .small) { image, _ in self.opponentImage = image } - self.viewStore.you?.rawValue?.loadPhoto(for: .small) { image, _ in + store.you?.rawValue?.loadPhoto(for: .small) { image, _ in self.yourImage = image } } - .onChange(of: self.viewStore.opponent) { _, player in + .onChange(of: store.opponent) { _, player in player?.rawValue?.loadPhoto(for: .small) { image, _ in self.opponentImage = image } } - .onChange(of: self.viewStore.you) { _, player in + .onChange(of: store.you) { _, player in player?.rawValue?.loadPhoto(for: .small) { image, _ in self.yourImage = image } @@ -143,6 +114,24 @@ private struct PlayerView: View { } } +fileprivate extension Game.State { + var opponent: ComposableGameCenter.Player? { + self.gameContext.turnBased?.otherParticipant?.player + } + var opponentScore: Int { + self.gameContext.turnBased?.otherPlayerIndex + .flatMap { self.turnBasedScores[$0] } ?? 0 + } + var you: ComposableGameCenter.Player? { + self.gameContext.turnBased?.localPlayer.player + } + var yourScore: Int { + self.gameContext.turnBased?.localPlayerIndex + .flatMap { self.turnBasedScores[$0] } + ?? (self.gameContext.is(\.turnBased) ? 0 : self.currentScore) + } +} + #if DEBUG import ClientModels import Overture From 380ce12110fbfef9f65830da552fe3b66e4617cd Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 00:18:53 -0800 Subject: [PATCH 38/61] wip --- Sources/GameOverFeature/GameOverView.swift | 206 +++++++++------------ 1 file changed, 83 insertions(+), 123 deletions(-) diff --git a/Sources/GameOverFeature/GameOverView.swift b/Sources/GameOverFeature/GameOverView.swift index cc166c6f..b5bf5714 100644 --- a/Sources/GameOverFeature/GameOverView.swift +++ b/Sources/GameOverFeature/GameOverView.swift @@ -54,6 +54,38 @@ public struct GameOver { public var turnBasedContext: TurnBasedContext? public var userNotificationSettings: UserNotificationClient.Notification.Settings? + var completedMatch: CompletedMatch? { + switch self.completedGame.gameContext { + case .dailyChallenge, .shared, .solo: + return nil + case .turnBased: + return self.turnBasedContext.flatMap { + CompletedMatch(completedGame: self.completedGame, turnBasedContext: $0) + } + } + } + var theirWords: [PlayedWord] { self.words.filter { !$0.isYourWord } } + var unplayedDaily: GameMode? { + self.dailyChallenges + .first(where: { $0.yourResult.rank == nil })?.dailyChallenge.gameMode + } + var words: [PlayedWord] { + self.completedGame.moves.compactMap { move in + move.type.playedWord.map { + PlayedWord( + isYourWord: move.playerIndex == self.completedGame.localPlayerIndex, + reactions: move.reactions, + score: move.score, + word: self.completedGame.cubes.string(from: $0) + ) + } + } + } + var you: ComposableGameCenter.Player? { self.turnBasedContext?.localPlayer.player } + var yourOpponent: ComposableGameCenter.Player? { self.turnBasedContext?.otherPlayer } + var yourWords: [PlayedWord] { self.words.filter { $0.isYourWord } } + var yourScore: Int { yourWords.reduce(into: 0) { $0 += $1.score } } + public init( completedGame: CompletedGame, dailyChallenges: [FetchTodaysDailyChallengeResponse] = [], @@ -374,75 +406,12 @@ public struct GameOverView: View { @Environment(\.opponentImage) var defaultOpponentImage @Environment(\.yourImage) var defaultYourImage let store: StoreOf - @ObservedObject var viewStore: ViewStore @State var yourImage: UIImage? @State var yourOpponentImage: UIImage? @State var isSharePresented = false - struct ViewState: Equatable { - let completedMatch: CompletedMatch? - let gameContext: CompletedGame.GameContext - let gameMode: GameMode - let gameModeIsLoading: GameMode? - let isDemo: Bool - let isUpgradeInterstitialPresented: Bool - let isViewEnabled: Bool - let showConfetti: Bool - let summary: GameOver.State.RankSummary? - let unplayedDaily: GameMode? - let words: [PlayedWord] - let you: ComposableGameCenter.Player? - let yourOpponent: ComposableGameCenter.Player? - let yourScore: Int - var theirWords: [PlayedWord] { self.words.filter { !$0.isYourWord } } - var yourWords: [PlayedWord] { self.words.filter { $0.isYourWord } } - - init(state: GameOver.State) { - self.gameContext = state.completedGame.gameContext - self.gameMode = state.completedGame.gameMode - let yourWords = state.completedGame.words( - forPlayerIndex: state.completedGame.localPlayerIndex) - self.gameModeIsLoading = state.gameModeIsLoading - let yourScore = yourWords.reduce(into: 0) { $0 += $1.score } - switch state.completedGame.gameContext { - case .dailyChallenge: - self.completedMatch = nil - case .shared: - self.completedMatch = nil - case .solo: - self.completedMatch = nil - case .turnBased: - self.completedMatch = state.turnBasedContext.flatMap { - CompletedMatch(completedGame: state.completedGame, turnBasedContext: $0) - } - } - self.isDemo = state.isDemo - self.isUpgradeInterstitialPresented = state.destination.is(\.some.upgradeInterstitial) - self.isViewEnabled = state.isViewEnabled - self.showConfetti = state.showConfetti - self.summary = state.summary - self.unplayedDaily = - state.dailyChallenges - .first(where: { $0.yourResult.rank == nil })?.dailyChallenge.gameMode - self.words = state.completedGame.moves.compactMap { move in - move.type.playedWord.map { - PlayedWord( - isYourWord: move.playerIndex == state.completedGame.localPlayerIndex, - reactions: move.reactions, - score: move.score, - word: state.completedGame.cubes.string(from: $0) - ) - } - } - self.you = state.turnBasedContext?.localPlayer.player - self.yourOpponent = state.turnBasedContext?.otherPlayer - self.yourScore = yourScore - } - } - public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(self.store, observe: ViewState.init) } public var body: some View { @@ -452,10 +421,10 @@ public struct GameOverView: View { HStack { Image(systemName: "cube.fill") - if !self.viewStore.isDemo { + if !store.isDemo { Spacer() Button { - self.viewStore.send(.closeButtonTapped, animation: .default) + store.send(.closeButtonTapped, animation: .default) } label: { Image(systemName: "xmark") } @@ -464,7 +433,7 @@ public struct GameOverView: View { .font(.system(size: 24)) .adaptivePadding() - switch self.viewStore.gameContext { + switch store.completedGame.gameContext { case .dailyChallenge: self.dailyChallengeResults case .shared: @@ -476,7 +445,7 @@ public struct GameOverView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .opacity(self.viewStore.isUpgradeInterstitialPresented ? 0 : 1) + .opacity(store.destination.is(\.some.upgradeInterstitial) ? 0 : 1) VStack(spacing: .grid(8)) { Divider() @@ -496,7 +465,7 @@ public struct GameOverView: View { foregroundColor: self.colorScheme == .dark ? .isowordsBlack : self.color ) ) - .padding(.bottom, .grid(self.viewStore.isDemo ? 30 : 0)) + .padding(.bottom, .grid(store.isDemo ? 30 : 0)) } .padding(.vertical, .grid(12)) } @@ -514,9 +483,9 @@ public struct GameOverView: View { (self.colorScheme == .dark ? .isowordsBlack : self.color) .ignoresSafeArea() ) - .task { await self.viewStore.send(.task).finish() } + .task { await store.send(.task).finish() } .notificationsAlert( - store: self.store.scope( + store: store.scope( state: \.$destination.notificationsAuthAlert, action: \.destination.notificationsAuthAlert ) @@ -525,12 +494,12 @@ public struct GameOverView: View { ActivityView(activityItems: [URL(string: "https://www.isowords.xyz")!]) .ignoresSafeArea() } - .disabled(!self.viewStore.isViewEnabled) + .disabled(!store.isViewEnabled) } @ViewBuilder var dailyChallengeResults: some View { - let result = self.viewStore.summary?.dailyChallenge + let result = store.summary?.dailyChallenge VStack(spacing: -8) { result.map { @@ -548,14 +517,11 @@ public struct GameOverView: View { .lineLimit(2) .multilineTextAlignment(.center) .redacted(reason: result == nil ? .placeholder : []) - .overlay( - self.viewStore.showConfetti - ? Confetti( - foregroundColor: self.colorScheme == .dark ? self.color : .isowordsBlack - ) - : nil, - alignment: .top - ) + .overlay(alignment: .top) { + if store.showConfetti { + Confetti(foregroundColor: self.colorScheme == .dark ? self.color : .isowordsBlack) + } + } VStack(spacing: 48) { VStack(spacing: self.adaptiveSize.pad(8)) { @@ -576,12 +542,12 @@ public struct GameOverView: View { HStack { Text("Score") Spacer() - Text("\(self.viewStore.yourScore)") + Text("\(store.yourScore)") } HStack { Text("Words found") Spacer() - Text("\(self.viewStore.yourWords.count)") + Text("\(store.yourWords.count)") } } .adaptivePadding(.horizontal) @@ -589,7 +555,7 @@ public struct GameOverView: View { self.wordList - if let unplayedDaily = self.viewStore.unplayedDaily { + if let unplayedDaily = store.unplayedDaily { VStack(spacing: self.adaptiveSize.pad(8)) { LazyVGrid( columns: [ @@ -602,22 +568,22 @@ public struct GameOverView: View { icon: Image(systemName: "clock.fill"), color: self.color, inactiveText: unplayedDaily == .unlimited ? Text("Played") : nil, - isLoading: self.viewStore.gameModeIsLoading == .timed, + isLoading: store.gameModeIsLoading == .timed, resumeText: nil, - action: { self.viewStore.send(.gameButtonTapped(.timed), animation: .default) } + action: { store.send(.gameButtonTapped(.timed), animation: .default) } ) - .disabled(self.viewStore.gameModeIsLoading != nil) + .disabled(store.gameModeIsLoading != nil) GameButton( title: Text("Unlimited"), icon: Image(systemName: "infinity"), color: self.color, inactiveText: unplayedDaily == .timed ? Text("Played") : nil, - isLoading: self.viewStore.gameModeIsLoading == .unlimited, + isLoading: store.gameModeIsLoading == .unlimited, resumeText: nil, - action: { self.viewStore.send(.gameButtonTapped(.unlimited), animation: .default) } + action: { store.send(.gameButtonTapped(.unlimited), animation: .default) } ) - .disabled(self.viewStore.gameModeIsLoading != nil) + .disabled(store.gameModeIsLoading != nil) } } .adaptivePadding(.horizontal) @@ -629,9 +595,9 @@ public struct GameOverView: View { @ViewBuilder var soloResults: some View { VStack(spacing: -8) { - Text("\(self.viewStore.yourScore).").fontWeight(.medium) + Text("\(store.yourScore).").fontWeight(.medium) + Text("\n") - + Text(praise(mode: self.viewStore.gameMode, score: self.viewStore.yourScore)) + + Text(praise(mode: store.completedGame.gameMode, score: store.yourScore)) } .adaptiveFont(.matter, size: 52) .adaptivePadding(.horizontal) @@ -652,7 +618,7 @@ public struct GameOverView: View { HStack { Text(timeScope.displayTitle) Spacer() - let rank = self.viewStore.summary?.leaderboard?[timeScope] + let rank = store.summary?.leaderboard?[timeScope] Text( """ \((rank?.rank ?? 0) as NSNumber, formatter: ordinalFormatter) of \ @@ -664,22 +630,19 @@ public struct GameOverView: View { } } .frame(maxWidth: .infinity, alignment: .leading) - .animation(.default, value: self.viewStore.summary) + .animation(.default, value: store.summary) } .adaptiveFont(.matterMedium, size: 16) .adaptivePadding(.horizontal) - .overlay( - self.viewStore.showConfetti - ? Confetti( - foregroundColor: self.colorScheme == .dark ? self.color : .isowordsBlack - ) - : nil, - alignment: .top - ) + .overlay(alignment: .top) { + if store.showConfetti { + Confetti(foregroundColor: self.colorScheme == .dark ? self.color : .isowordsBlack) + } + } self.wordList - if !self.viewStore.isDemo { + if !store.isDemo { VStack(spacing: self.adaptiveSize.pad(8)) { Text("Play again") .adaptiveFont(.matterMedium, size: 16) @@ -699,7 +662,7 @@ public struct GameOverView: View { inactiveText: nil, isLoading: false, resumeText: nil, - action: { self.viewStore.send(.gameButtonTapped(.timed), animation: .default) } + action: { store.send(.gameButtonTapped(.timed), animation: .default) } ) GameButton( @@ -709,7 +672,7 @@ public struct GameOverView: View { inactiveText: nil, isLoading: false, resumeText: nil, - action: { self.viewStore.send(.gameButtonTapped(.unlimited), animation: .default) } + action: { store.send(.gameButtonTapped(.unlimited), animation: .default) } ) } .adaptivePadding(.horizontal) @@ -726,23 +689,20 @@ public struct GameOverView: View { @ViewBuilder var turnBasedResults: some View { - if let completedMatch = self.viewStore.completedMatch { + if let completedMatch = store.completedMatch { VStack(spacing: -8) { Text(completedMatch.description).fontWeight(.medium) Text(completedMatch.detailDescription) } .adaptiveFont(.matter, size: 52) - .overlay( - self.viewStore.showConfetti - ? Confetti( - foregroundColor: self.colorScheme == .dark ? self.color : .isowordsBlack - ) - : nil, - alignment: .bottom - ) + .overlay(alignment: .bottom) { + if store.showConfetti { + Confetti(foregroundColor: self.colorScheme == .dark ? self.color : .isowordsBlack) + } + } if completedMatch.isTurnBased { - Button("Rematch?") { self.viewStore.send(.rematchButtonTapped, animation: .default) } + Button("Rematch?") { store.send(.rematchButtonTapped, animation: .default) } .adaptiveFont(.matter, size: 14) .buttonStyle( ActionButtonStyle( @@ -761,7 +721,7 @@ public struct GameOverView: View { .adaptiveFont(.matterMedium, size: 14) .frame(maxWidth: .infinity, alignment: .trailing) .lineLimit(1) - Text("\(self.viewStore.yourScore)") + Text("\(store.yourScore)") .adaptiveFont(.matterMedium, size: 20) .frame(maxWidth: .infinity, alignment: .trailing) } @@ -786,7 +746,7 @@ public struct GameOverView: View { .background((self.colorScheme == .dark ? self.color : .isowordsBlack).opacity(0.2)) VStack(alignment: .trailing) { - ForEach(self.viewStore.yourWords, id: \.word) { word in + ForEach(store.yourWords, id: \.word) { word in WordView( backgroundColor: self.colorScheme == .dark ? self.color : .isowordsBlack, foregroundColor: self.colorScheme == .dark ? .isowordsBlack : self.color, @@ -794,7 +754,7 @@ public struct GameOverView: View { ) } } - .padding(.top, self.viewStore.words.first?.isYourWord == .some(true) ? 0 : .grid(6)) + .padding(.top, store.words.first?.isYourWord == .some(true) ? 0 : .grid(6)) .padding(.grid(2)) } .padding(.vertical) @@ -843,7 +803,7 @@ public struct GameOverView: View { ) VStack(alignment: .leading) { - ForEach(self.viewStore.theirWords, id: \.word) { word in + ForEach(store.theirWords, id: \.word) { word in WordView( backgroundColor: self.colorScheme == .dark ? self.color : .isowordsBlack, foregroundColor: self.colorScheme == .dark ? .isowordsBlack : self.color, @@ -851,7 +811,7 @@ public struct GameOverView: View { ) } } - .padding(.top, self.viewStore.words.first?.isYourWord == .some(true) ? .grid(6) : 0) + .padding(.top, store.words.first?.isYourWord == .some(true) ? .grid(6) : 0) .padding(.grid(2)) } .padding(.vertical) @@ -875,10 +835,10 @@ public struct GameOverView: View { } ) .onAppear { - self.viewStore.you?.rawValue?.loadPhoto(for: .small) { image, _ in + store.you?.rawValue?.loadPhoto(for: .small) { image, _ in self.yourImage = image } - self.viewStore.yourOpponent?.rawValue?.loadPhoto(for: .small) { image, _ in + store.yourOpponent?.rawValue?.loadPhoto(for: .small) { image, _ in self.yourOpponentImage = image } } @@ -886,7 +846,7 @@ public struct GameOverView: View { } var color: Color { - switch self.viewStore.gameContext { + switch store.completedGame.gameContext { case .dailyChallenge: return .dailyChallenge case .shared, .solo: @@ -905,7 +865,7 @@ public struct GameOverView: View { ScrollView(.horizontal, showsIndicators: false) { HStack { - ForEach(self.viewStore.yourWords, id: \.word) { word in + ForEach(store.yourWords, id: \.word) { word in WordView( backgroundColor: self.colorScheme == .dark ? self.color : .isowordsBlack, foregroundColor: self.colorScheme == .dark ? .isowordsBlack : self.color, From eb750205219f031925ddde0ee81696ed6861c105 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 00:25:25 -0800 Subject: [PATCH 39/61] wip --- .../DailyChallengeView.swift | 90 +++++++++---------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/Sources/DailyChallengeFeature/DailyChallengeView.swift b/Sources/DailyChallengeFeature/DailyChallengeView.swift index 7917bd5b..84e2f156 100644 --- a/Sources/DailyChallengeFeature/DailyChallengeView.swift +++ b/Sources/DailyChallengeFeature/DailyChallengeView.swift @@ -60,6 +60,11 @@ public struct DailyChallengeReducer { self.inProgressDailyChallengeUnlimited = inProgressDailyChallengeUnlimited self.userNotificationSettings = userNotificationSettings } + + var isNotificationStatusDetermined: Bool { + ![.notDetermined, .provisional] + .contains(self.userNotificationSettings?.authorizationStatus) + } } public enum Action { @@ -254,41 +259,30 @@ public struct DailyChallengeView: View { @Environment(\.colorScheme) var colorScheme @Environment(\.date) var date @Bindable var store: StoreOf - @ObservedObject var viewStore: ViewStore - - struct ViewState: Equatable { - let gameModeIsLoading: GameMode? - let isNotificationStatusDetermined: Bool - let numberOfPlayers: Int - let timedState: ButtonState - let unlimitedState: ButtonState - - enum ButtonState: Equatable { - case played(rank: Int, outOf: Int) - case playable - case resume(currentScore: Int) - case unplayable - } - init(state: DailyChallengeReducer.State) { - self.gameModeIsLoading = state.gameModeIsLoading - self.isNotificationStatusDetermined = ![.notDetermined, .provisional] - .contains(state.userNotificationSettings?.authorizationStatus) - self.numberOfPlayers = state.dailyChallenges.numberOfPlayers - self.timedState = .init( - fetchedResponse: state.dailyChallenges.timed, - inProgressGame: nil - ) - self.unlimitedState = .init( - fetchedResponse: state.dailyChallenges.unlimited, - inProgressGame: state.inProgressDailyChallengeUnlimited - ) - } + enum ButtonState: Equatable { + case played(rank: Int, outOf: Int) + case playable + case resume(currentScore: Int) + case unplayable + } + + var timedState: ButtonState { + .init( + fetchedResponse: store.dailyChallenges.timed, + inProgressGame: nil + ) + } + + var unlimitedState: ButtonState { + .init( + fetchedResponse: store.dailyChallenges.unlimited, + inProgressGame: store.inProgressDailyChallengeUnlimited + ) } public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(store, observe: ViewState.init) } public var body: some View { @@ -299,12 +293,12 @@ public struct DailyChallengeView: View { VStack(spacing: .grid(8)) { Group { - if self.viewStore.numberOfPlayers <= 1 { + if store.dailyChallenges.numberOfPlayers <= 1 { (Text("Play") + Text("\nagainst the") + Text("\ncommunity")) } else { - (Text("\(self.viewStore.numberOfPlayers)") + (Text("\(store.dailyChallenges.numberOfPlayers)") + Text("\npeople have") + Text("\nplayed!")) } @@ -331,29 +325,29 @@ public struct DailyChallengeView: View { title: Text("Timed"), icon: Image(systemName: "clock.fill"), color: .dailyChallenge, - inactiveText: self.viewStore.timedState.inactiveText, - isLoading: self.viewStore.gameModeIsLoading == .timed, - resumeText: self.viewStore.timedState.resumeText, - action: { self.viewStore.send(.gameButtonTapped(.timed), animation: .default) } + inactiveText: timedState.inactiveText, + isLoading: store.gameModeIsLoading == .timed, + resumeText: timedState.resumeText, + action: { store.send(.gameButtonTapped(.timed), animation: .default) } ) - .disabled(self.viewStore.gameModeIsLoading != nil) + .disabled(store.gameModeIsLoading != nil) GameButton( title: Text("Unlimited"), icon: Image(systemName: "infinity"), color: .dailyChallenge, - inactiveText: self.viewStore.unlimitedState.inactiveText, - isLoading: self.viewStore.gameModeIsLoading == .unlimited, - resumeText: self.viewStore.unlimitedState.resumeText, - action: { self.viewStore.send(.gameButtonTapped(.unlimited), animation: .default) } + inactiveText: unlimitedState.inactiveText, + isLoading: store.gameModeIsLoading == .unlimited, + resumeText: unlimitedState.resumeText, + action: { store.send(.gameButtonTapped(.unlimited), animation: .default) } ) - .disabled(self.viewStore.gameModeIsLoading != nil) + .disabled(store.gameModeIsLoading != nil) } .adaptivePadding(.vertical) .screenEdgePadding(.horizontal) Button { - self.viewStore.send(.resultsButtonTapped) + store.send(.resultsButtonTapped) } label: { HStack { Text("View all results") @@ -370,15 +364,15 @@ public struct DailyChallengeView: View { .foregroundColor(self.colorScheme == .dark ? .isowordsBlack : .dailyChallenge) .background(self.colorScheme == .dark ? Color.dailyChallenge : .isowordsBlack) } - .task { await self.viewStore.send(.task).finish() } + .task { await store.send(.task).finish() } .navigationStyle( backgroundColor: self.colorScheme == .dark ? .isowordsBlack : .dailyChallenge, foregroundColor: self.colorScheme == .dark ? .dailyChallenge : .isowordsBlack, title: Text("Daily Challenge"), trailing: Group { - if !self.viewStore.isNotificationStatusDetermined { + if !store.isNotificationStatusDetermined { ReminderBell { - self.viewStore.send(.notificationButtonTapped, animation: .default) + store.send(.notificationButtonTapped, animation: .default) } .transition( .scale(scale: 0) @@ -396,7 +390,7 @@ public struct DailyChallengeView: View { DailyChallengeResultsView(store: store) } .notificationsAlert( - store: self.store.scope( + store: store.scope( state: \.$destination.notificationsAuthAlert, action: \.destination.notificationsAuthAlert ) @@ -404,7 +398,7 @@ public struct DailyChallengeView: View { } } -extension DailyChallengeView.ViewState.ButtonState { +extension DailyChallengeView.ButtonState { init( fetchedResponse: FetchTodaysDailyChallengeResponse?, inProgressGame: InProgressGame? From 2fc8dc63d37fa2c1c5865ff953b56df16046cb13 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 00:27:03 -0800 Subject: [PATCH 40/61] wip --- Sources/GameCore/Views/GameView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/GameCore/Views/GameView.swift b/Sources/GameCore/Views/GameView.swift index 9ee7aa4f..0bacdb34 100644 --- a/Sources/GameCore/Views/GameView.swift +++ b/Sources/GameCore/Views/GameView.swift @@ -139,9 +139,9 @@ public struct GameView: View where Content: View { .ignoresSafeArea() ) .bottomMenu( - store: store.scope(state: \.$destination, action: \.destination), - state: \.bottomMenu, - action: { .bottomMenu($0) } + store: store.scope(state: \.$destination.bottomMenu, action: \.destination.bottomMenu), + state: { $0 }, + action: { $0 } ) .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .sheet( From 5233f32c6fef88677aa814b206584883ef343075 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 00:29:16 -0800 Subject: [PATCH 41/61] wip --- Sources/BottomMenu/ComposableBottomMenu.swift | 23 ++++--------------- Sources/GameCore/Views/GameView.swift | 4 +--- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/Sources/BottomMenu/ComposableBottomMenu.swift b/Sources/BottomMenu/ComposableBottomMenu.swift index 6e8715ae..6dc8000c 100644 --- a/Sources/BottomMenu/ComposableBottomMenu.swift +++ b/Sources/BottomMenu/ComposableBottomMenu.swift @@ -71,33 +71,18 @@ extension BottomMenuState: _EphemeralState { extension View { public func bottomMenu( store: Store>, PresentationAction> - ) -> some View { - self.bottomMenu(store: store, state: { $0 }, action: { $0 }) - } - - public func bottomMenu( - store: Store, PresentationAction>, - state toMenuState: @escaping (DestinationState) -> BottomMenuState?, - action fromMenuAction: @escaping (MenuAction) -> DestinationAction ) -> some View { WithViewStore( store, observe: { $0 }, - removeDuplicates: { - ($0.wrappedValue.flatMap(toMenuState) != nil) - == ($1.wrappedValue.flatMap(toMenuState) != nil) - } + removeDuplicates: { ($0.wrappedValue != nil) == ($1.wrappedValue != nil) } ) { viewStore in self.bottomMenu( item: Binding( get: { - viewStore.wrappedValue.flatMap(toMenuState)?.converted( - send: { - viewStore.send(.presented(fromMenuAction($0))) - }, - sendWithAnimation: { - viewStore.send(.presented(fromMenuAction($0)), animation: $1) - } + viewStore.wrappedValue?.converted( + send: { viewStore.send(.presented($0)) }, + sendWithAnimation: { viewStore.send(.presented($0), animation: $1) } ) }, set: { state in diff --git a/Sources/GameCore/Views/GameView.swift b/Sources/GameCore/Views/GameView.swift index 0bacdb34..544d3abc 100644 --- a/Sources/GameCore/Views/GameView.swift +++ b/Sources/GameCore/Views/GameView.swift @@ -139,9 +139,7 @@ public struct GameView: View where Content: View { .ignoresSafeArea() ) .bottomMenu( - store: store.scope(state: \.$destination.bottomMenu, action: \.destination.bottomMenu), - state: { $0 }, - action: { $0 } + store: store.scope(state: \.$destination.bottomMenu, action: \.destination.bottomMenu) ) .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .sheet( From cf5f10eda852de813b672660f1009daf7a4d8dac Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 09:23:12 -0800 Subject: [PATCH 42/61] wip --- Sources/BottomMenu/BottomMenu.swift | 10 ++++- Sources/BottomMenu/ComposableBottomMenu.swift | 43 +++++++++---------- Sources/GameCore/Views/GameView.swift | 7 +-- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Sources/BottomMenu/BottomMenu.swift b/Sources/BottomMenu/BottomMenu.swift index 2a9edbfe..233900ee 100644 --- a/Sources/BottomMenu/BottomMenu.swift +++ b/Sources/BottomMenu/BottomMenu.swift @@ -77,7 +77,11 @@ private struct BottomMenuModifier: ViewModifier { Rectangle() .fill(Color.isowordsBlack.opacity(0.4)) .frame(maxWidth: .infinity, maxHeight: .infinity) - .onTapGesture { self.item = nil } + .onTapGesture { + withAnimation { + self.item = nil + } + } .transition(.opacity.animation(.default)) .ignoresSafeArea() } @@ -91,7 +95,9 @@ private struct BottomMenuModifier: ViewModifier { .adaptiveFont(.matterMedium, size: 18) Spacer() Button { - self.item = nil + withAnimation { + self.item = nil + } } label: { Image(systemName: "xmark.circle.fill") .font(.system(size: 24)) diff --git a/Sources/BottomMenu/ComposableBottomMenu.swift b/Sources/BottomMenu/ComposableBottomMenu.swift index 6dc8000c..88176955 100644 --- a/Sources/BottomMenu/ComposableBottomMenu.swift +++ b/Sources/BottomMenu/ComposableBottomMenu.swift @@ -70,29 +70,25 @@ extension BottomMenuState: _EphemeralState { extension View { public func bottomMenu( - store: Store>, PresentationAction> + _ item: Binding, MenuAction>?> ) -> some View { - WithViewStore( - store, - observe: { $0 }, - removeDuplicates: { ($0.wrappedValue != nil) == ($1.wrappedValue != nil) } - ) { viewStore in - self.bottomMenu( - item: Binding( - get: { - viewStore.wrappedValue?.converted( - send: { viewStore.send(.presented($0)) }, - sendWithAnimation: { viewStore.send(.presented($0), animation: $1) } - ) - }, - set: { state in - if state == nil { - viewStore.send(.dismiss, animation: .default) - } + let store = item.wrappedValue + let state = store?.withState { $0 } + return self.bottomMenu( + item: Binding( + get: { + state?.converted( + send: { store?.send($0) }, + sendWithAnimation: { store?.send($0, animation: $1) } + ) + }, + set: { + if $0 == nil { + item.transaction($1).wrappedValue = nil } - ) + } ) - } + ) } } @@ -140,8 +136,9 @@ extension BottomMenuState.Button { @Reducer private struct BottomMenuReducer { + @ObservableState struct State: Equatable { - @PresentationState var bottomMenu: BottomMenuState? + @Presents var bottomMenu: BottomMenuState? } enum Action: Equatable { @@ -183,14 +180,14 @@ extension BottomMenuState.Button { struct BottomMenu_TCA_Previews: PreviewProvider { struct TestView: View { - private let store = Store(initialState: BottomMenuReducer.State()) { + @Bindable fileprivate var store = Store(initialState: BottomMenuReducer.State()) { BottomMenuReducer() } var body: some View { Button("Present") { store.send(.showMenuButtonTapped, animation: .default) } .frame(maxWidth: .infinity, maxHeight: .infinity) - .bottomMenu(store: self.store.scope(state: \.$bottomMenu, action: \.bottomMenu)) + .bottomMenu($store.scope(state: \.bottomMenu, action: \.bottomMenu)) } } diff --git a/Sources/GameCore/Views/GameView.swift b/Sources/GameCore/Views/GameView.swift index 544d3abc..cc1da01b 100644 --- a/Sources/GameCore/Views/GameView.swift +++ b/Sources/GameCore/Views/GameView.swift @@ -73,8 +73,7 @@ public struct GameView: View where Content: View { .transition( store.isAnimationReduced ? .opacity - : AnyTransition - .asymmetric(insertion: .offset(y: 50), removal: .offset(y: 50)) + : .asymmetric(insertion: .offset(y: 50), removal: .offset(y: 50)) .combined(with: .opacity) ) } @@ -138,9 +137,7 @@ public struct GameView: View where Content: View { Color(self.colorScheme == .dark ? .hex(0x111111) : .white) .ignoresSafeArea() ) - .bottomMenu( - store: store.scope(state: \.$destination.bottomMenu, action: \.destination.bottomMenu) - ) + .bottomMenu($store.scope(state: \.destination?.bottomMenu, action: \.destination.bottomMenu)) .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .sheet( item: $store.scope(state: \.destination?.settings, action: \.destination.settings) From eab34753899954fe6aa95304ba07d81671ac6246 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 09:32:29 -0800 Subject: [PATCH 43/61] wip --- .../DailyChallengeView.swift | 4 +- Sources/GameOverFeature/GameOverView.swift | 6 +- .../NotificationsAuthAlert.swift | 109 ++++++++---------- 3 files changed, 52 insertions(+), 67 deletions(-) diff --git a/Sources/DailyChallengeFeature/DailyChallengeView.swift b/Sources/DailyChallengeFeature/DailyChallengeView.swift index 84e2f156..0a938181 100644 --- a/Sources/DailyChallengeFeature/DailyChallengeView.swift +++ b/Sources/DailyChallengeFeature/DailyChallengeView.swift @@ -390,8 +390,8 @@ public struct DailyChallengeView: View { DailyChallengeResultsView(store: store) } .notificationsAlert( - store: store.scope( - state: \.$destination.notificationsAuthAlert, + $store.scope( + state: \.destination?.notificationsAuthAlert, action: \.destination.notificationsAuthAlert ) ) diff --git a/Sources/GameOverFeature/GameOverView.swift b/Sources/GameOverFeature/GameOverView.swift index b5bf5714..628ea06f 100644 --- a/Sources/GameOverFeature/GameOverView.swift +++ b/Sources/GameOverFeature/GameOverView.swift @@ -405,7 +405,7 @@ public struct GameOverView: View { @Environment(\.colorScheme) var colorScheme @Environment(\.opponentImage) var defaultOpponentImage @Environment(\.yourImage) var defaultYourImage - let store: StoreOf + @Bindable var store: StoreOf @State var yourImage: UIImage? @State var yourOpponentImage: UIImage? @State var isSharePresented = false @@ -485,8 +485,8 @@ public struct GameOverView: View { ) .task { await store.send(.task).finish() } .notificationsAlert( - store: store.scope( - state: \.$destination.notificationsAuthAlert, + $store.scope( + state: \.destination?.notificationsAuthAlert, action: \.destination.notificationsAuthAlert ) ) diff --git a/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift b/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift index 34b60a6a..acd8dfc8 100644 --- a/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift +++ b/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift @@ -57,95 +57,80 @@ public struct NotificationsAuthAlert { extension View { public func notificationsAlert( - store: Store< - PresentationState, - PresentationAction - > + _ store: Binding?> ) -> some View { self.modifier(NotificationsAuthAlertViewModifier(store: store)) } } struct NotificationsAuthAlertViewModifier: ViewModifier { - let store: Store< - PresentationState, - PresentationAction - > + @Binding var store: Store? func body(content: Content) -> some View { - WithViewStore( - self.store, observe: { $0.wrappedValue } - ) { viewStore in - content - .overlay { - if viewStore.state != nil { - Rectangle() - .fill(Color.dailyChallenge.opacity(0.8)) - .ignoresSafeArea() - .transition(.opacity.animation(.default)) - } + let state = store?.withState { $0 } + content + .overlay { + if state != nil { + Rectangle() + .fill(Color.dailyChallenge.opacity(0.8)) + .ignoresSafeArea() + .transition(.opacity.animation(.default)) } - .overlay { - if let state = viewStore.state { - ZStack(alignment: .topTrailing) { - NotificationsAuthAlertView( - store: store.scope( - state: { _ in state }, action: { .presented($0) } - ) - ) + } + .overlay { + if state != nil { + ZStack(alignment: .topTrailing) { + NotificationsAuthAlertView { + store?.send(.turnOnNotificationsButtonTapped) + } - Button { - viewStore.send(.dismiss) - } label: { - Image(systemName: "xmark") - .font(.system(size: 20)) - .foregroundColor(.dailyChallenge) - .padding(.grid(5)) - } + Button { + store = nil + } label: { + Image(systemName: "xmark") + .font(.system(size: 20)) + .foregroundColor(.dailyChallenge) + .padding(.grid(5)) } - .transition( - .scale(scale: 0.8, anchor: .center) - .animation(.spring()) - .combined(with: .opacity.animation(.default)) - ) } + .transition( + .scale(scale: 0.8, anchor: .center) + .animation(.spring()) + .combined(with: .opacity.animation(.default)) + ) } - } + } } } struct NotificationsAuthAlertView: View { - let store: StoreOf + let action: () -> Void var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - VStack(spacing: .grid(8)) { - (Text("Want to get notified about ") - + Text("your ranks?").fontWeight(.medium)) - .adaptiveFont(.matter, size: 28) - .foregroundColor(.dailyChallenge) - .lineLimit(.max) - .minimumScaleFactor(0.2) - .multilineTextAlignment(.center) + VStack(spacing: .grid(8)) { + (Text("Want to get notified about ") + + Text("your ranks?").fontWeight(.medium)) + .adaptiveFont(.matter, size: 28) + .foregroundColor(.dailyChallenge) + .lineLimit(.max) + .minimumScaleFactor(0.2) + .multilineTextAlignment(.center) - Button("Turn on notifications") { - viewStore.send(.turnOnNotificationsButtonTapped, animation: .default) + Button("Turn on notifications") { + withAnimation { + action() } - .buttonStyle(ActionButtonStyle(backgroundColor: .dailyChallenge, foregroundColor: .black)) } - .padding(.top, .grid(4)) - .padding(.grid(8)) - .background(Color.black) + .buttonStyle(ActionButtonStyle(backgroundColor: .dailyChallenge, foregroundColor: .black)) } + .padding(.top, .grid(4)) + .padding(.grid(8)) + .background(Color.black) } } struct NotificationMenu_Previews: PreviewProvider { static var previews: some View { - NotificationsAuthAlertView( - store: Store(initialState: NotificationsAuthAlert.State()) { - NotificationsAuthAlert() - } - ) + NotificationsAuthAlertView(action: {}) } } From 2a30738d8084d9cdddfc3de1862f5dd11cf7346a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 10:15:22 -0800 Subject: [PATCH 44/61] wip --- Sources/CubeCore/CubeFaceNode.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/CubeCore/CubeFaceNode.swift b/Sources/CubeCore/CubeFaceNode.swift index 650c25bb..205b020e 100644 --- a/Sources/CubeCore/CubeFaceNode.swift +++ b/Sources/CubeCore/CubeFaceNode.swift @@ -31,14 +31,14 @@ public class CubeFaceNode: SCNNode { private var cancellables: Set = [] private let uuid = UUID() - private let viewStore: ViewStore + private let store: Store public init( letterGeometry: SCNGeometry, store: Store ) { - self.viewStore = ViewStore(store, observe: { $0 }) - self.side = self.viewStore.cubeFace.side + self.store = store + self.side = store.withState(\.cubeFace.side) super.init() @@ -49,9 +49,9 @@ public class CubeFaceNode: SCNNode { self.addChildNode(letterNode) self.category = [.cubeFace, .shadowSurface] - self.name = "Face: \(self.viewStore.cubeFace.side)" + self.name = "Face: \(self.side)" - switch self.viewStore.cubeFace.side { + switch self.side { case .top: self.eulerAngles = SCNVector3(-CGFloat.pi / 2, 0, 0) self.position = SCNVector3(0, 0.5, 0) @@ -62,7 +62,7 @@ public class CubeFaceNode: SCNNode { self.position = SCNVector3(0.5, 0, 0) } - self.viewStore.publisher + self.store.publisher .sink { [weak self] state in guard let self = self else { return } guard state.cubeFace.useCount <= 2 else { return } From 0740836b953f5976c401ad1ee8857f5dcace1305 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 10:16:40 -0800 Subject: [PATCH 45/61] wip --- Sources/CubeCore/CubeNode.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/CubeCore/CubeNode.swift b/Sources/CubeCore/CubeNode.swift index 3dd94a75..8c1c25b3 100644 --- a/Sources/CubeCore/CubeNode.swift +++ b/Sources/CubeCore/CubeNode.swift @@ -65,15 +65,15 @@ public class CubeNode: SCNNode { private lazy var shakeAnimationActionKey = "shake animation: \(ObjectIdentifier(self))" private lazy var removeAnimationActionKey = "remove animation: \(ObjectIdentifier(self))" private var cancellables: Set = [] - private let viewStore: ViewStore + private let store: Store public init( letterGeometry: SCNGeometry, store: Store ) { - self.viewStore = ViewStore(store, observe: { $0 }) + self.store = store - self.index = self.viewStore.index + self.index = self.store.withState(\.index) self.leftPlaneNode = CubeFaceNode( letterGeometry: letterGeometry, store: store.scope(state: \.left, action: \.never) @@ -89,9 +89,9 @@ public class CubeNode: SCNNode { super.init() - self.isHidden = !self.viewStore.isInPlay + self.isHidden = !self.store.withState(\.isInPlay) self.name = - "xIndex: \(self.viewStore.index.x), yIndex: \(self.viewStore.index.y), zIndex: \(self.viewStore.index.z)" + "xIndex: \(self.index.x), yIndex: \(self.index.y), zIndex: \(self.index.z)" for side in CubeFace.Side.allCases { switch side { @@ -104,7 +104,7 @@ public class CubeNode: SCNNode { } } - self.viewStore.publisher + self.store.publisher .prefix(while: \.isInPlay) .map { ($0.isCriticallySelected, $0.index, $0.cubeShakeStartedAt) } .removeDuplicates(by: ==) @@ -117,7 +117,7 @@ public class CubeNode: SCNNode { } .store(in: &self.cancellables) - self.viewStore.publisher.isInPlay + self.store.publisher.isInPlay .dropFirst() .sink { [weak self] isInPlay in guard let self = self else { return } From db145d33a51c6fa35b852d39e3eddcb8f79034e9 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 10:17:53 -0800 Subject: [PATCH 46/61] wip --- Sources/CubeCore/CubeSceneView.swift | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Sources/CubeCore/CubeSceneView.swift b/Sources/CubeCore/CubeSceneView.swift index ee7c73be..b0b558d3 100644 --- a/Sources/CubeCore/CubeSceneView.swift +++ b/Sources/CubeCore/CubeSceneView.swift @@ -79,7 +79,6 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { private var motionManager: CMMotionManager? private var startingAttitude: Attitude? private let store: Store - private let viewStore: ViewStore private var worldScale: Float = 1.0 var enableCubeShadow = true { @@ -94,7 +93,6 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { store: Store ) { self.store = store - self.viewStore = ViewStore(self.store, observe: { $0 }) super.init(frame: .zero, options: nil) @@ -117,14 +115,14 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { gameCubeNode.scale = .init(worldScale, worldScale, worldScale) self.scene?.rootNode.addChildNode(self.gameCubeNode) - self.viewStore.publisher.cubes + self.store.publisher.cubes .sink { cubes in SCNTransaction.begin() SCNTransaction.commit() } .store(in: &self.cancellables) - self.viewStore.publisher.cubes + self.store.publisher.cubes .removeDuplicates(by: { $0.letters == $1.letters }) .sink { [weak self] cubes in guard let self = self else { return } @@ -191,7 +189,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { ambientLightNode.light = ambientLight self.scene?.rootNode.addChildNode(ambientLightNode) - self.viewStore.publisher + self.store.publisher .map { ($0.enableGyroMotion, $0.isOnLowPowerMode) } .removeDuplicates(by: ==) .sink { [weak self] enableGyroMotion, isOnLowPowerMode in @@ -208,7 +206,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { } .store(in: &self.cancellables) - self.viewStore.publisher.playedWords + self.store.publisher.playedWords .sink { [weak self] _ in self?.startingAttitude = nil } .store(in: &self.cancellables) @@ -235,13 +233,13 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { nub.isHidden = true self.addSubview(nub) - self.viewStore.publisher.nub + self.store.publisher.nub .compactMap { $0?.isPressed } .removeDuplicates() .assign(to: \.isPressed, on: nub) .store(in: &self.cancellables) - self.viewStore.publisher.nub + self.store.publisher.nub .compactMap { $0?.location } .removeDuplicates() .sink { [weak self] location in @@ -291,7 +289,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { // TODO: rename private func update() { self.showsStatistics = self.showSceneStatistics - self.light.castsShadow = self.enableCubeShadow && !self.viewStore.isOnLowPowerMode + self.light.castsShadow = self.enableCubeShadow && !self.store.withState(\.isOnLowPowerMode) } deinit { @@ -319,7 +317,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { guard let (_, _, cubeNode) = self.nodes(location: location) else { return } - self.viewStore.send(.doubleTap(index: cubeNode.index), animation: .default) + self.store.send(.doubleTap(index: cubeNode.index), animation: .default) } @objc private func tap(recognizer: UIGestureRecognizer) { @@ -328,7 +326,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { let location = recognizer.location(in: self) - self.viewStore.send( + self.store.send( .tap( recognizer.state, self.nodes(location: location) @@ -357,7 +355,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { ) } - self.viewStore.send(.pan(recognizer.state, panData), animation: .default) + self.store.send(.pan(recognizer.state, panData), animation: .default) } required init?(coder: NSCoder) { From fa6251dd148ee9c15a23d41561eb98dc6d348eb5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 10:19:42 -0800 Subject: [PATCH 47/61] wip --- .../DailyChallengeFeature/CalendarView.swift | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/Sources/DailyChallengeFeature/CalendarView.swift b/Sources/DailyChallengeFeature/CalendarView.swift index bd2d57d7..6624ec3e 100644 --- a/Sources/DailyChallengeFeature/CalendarView.swift +++ b/Sources/DailyChallengeFeature/CalendarView.swift @@ -63,16 +63,11 @@ struct CalendarView: View { } let store: StoreOf - @ObservedObject var viewStore: ViewStore - - init(store: StoreOf) { - self.store = store - self.viewStore = ViewStore(store, observe: ViewState.init) - } var body: some View { + let viewState = ViewState(state: store.state) VStack(alignment: .leading, spacing: .grid(4)) { - ForEach(self.viewStore.months, id: \.date) { month in + ForEach(viewState.months, id: \.date) { month in VStack(alignment: .leading) { Text(month.name) .adaptiveFont(.matterMedium, size: 14) @@ -83,7 +78,7 @@ struct CalendarView: View { ) { ForEach(month.results, id: \.gameNumber) { result in Button { - self.viewStore.send(.leaderboardResults(.timeScopeChanged(result.gameNumber))) + store.send(.leaderboardResults(.timeScopeChanged(result.gameNumber))) } label: { VStack { Text("\(dayFormatter.string(from: result.createdAt))") @@ -96,12 +91,12 @@ struct CalendarView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.vertical, .grid(1) / 2) - .background( - self.viewStore.currentChallenge == result.gameNumber - ? RoundedRectangle(cornerRadius: .grid(2), style: .continuous) + .background { + if viewState.currentChallenge == result.gameNumber { + RoundedRectangle(cornerRadius: .grid(2), style: .continuous) .fill(Color.adaptiveWhite) - : nil - ) + } + } } } } @@ -110,10 +105,10 @@ struct CalendarView: View { } } - if self.viewStore.months.isEmpty { + if viewState.months.isEmpty { HStack { Button { - self.viewStore.send(.loadHistory) + store.send(.loadHistory) } label: { Image(systemName: "arrow.clockwise") } @@ -123,14 +118,14 @@ struct CalendarView: View { } } } - .redacted(reason: self.viewStore.isLoading ? .placeholder : []) - .disabled(self.viewStore.isLoading) - .overlay( - self.viewStore.isLoading - ? ProgressView() + .redacted(reason: viewState.isLoading ? .placeholder : []) + .disabled(viewState.isLoading) + .overlay { + if viewState.isLoading { + ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .black)) - : nil - ) + } + } } } From fd47050f7fe75e8813a5ea815658b2a5b1ec76aa Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 9 Dec 2023 10:48:10 -0800 Subject: [PATCH 48/61] wip --- Sources/CubePreview/CubePreviewView.swift | 2 -- Sources/GameCore/Views/GameView.swift | 2 -- Sources/TrailerFeature/Trailer.swift | 2 -- 3 files changed, 6 deletions(-) diff --git a/Sources/CubePreview/CubePreviewView.swift b/Sources/CubePreview/CubePreviewView.swift index 407e9b95..07669e69 100644 --- a/Sources/CubePreview/CubePreviewView.swift +++ b/Sources/CubePreview/CubePreviewView.swift @@ -283,5 +283,3 @@ public struct CubePreviewView: View { } ?? Text("") } } - -private func absurd(_: Never) -> A {} diff --git a/Sources/GameCore/Views/GameView.swift b/Sources/GameCore/Views/GameView.swift index cc1da01b..7c33d2cc 100644 --- a/Sources/GameCore/Views/GameView.swift +++ b/Sources/GameCore/Views/GameView.swift @@ -150,5 +150,3 @@ public struct GameView: View where Content: View { .task { await store.send(.task).finish() } } } - -private func absurd(_: Never) -> A {} diff --git a/Sources/TrailerFeature/Trailer.swift b/Sources/TrailerFeature/Trailer.swift index a3840886..f09a2a0c 100644 --- a/Sources/TrailerFeature/Trailer.swift +++ b/Sources/TrailerFeature/Trailer.swift @@ -295,5 +295,3 @@ private let fadeInDuration = 0.3 private let fadeOutDuration = 0.3 private let submitPressDuration = 0.05 private let submitHestitationDuration = 0.15 - -private func absurd(_: Never) -> A {} From 7199286523002b332a46438ebc58ef9732d8feed Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 10 Jan 2024 14:42:56 -0800 Subject: [PATCH 49/61] wip --- Sources/CubeCore/CubeSceneView.swift | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/CubeCore/CubeSceneView.swift b/Sources/CubeCore/CubeSceneView.swift index 00077907..bfbefa0c 100644 --- a/Sources/CubeCore/CubeSceneView.swift +++ b/Sources/CubeCore/CubeSceneView.swift @@ -115,14 +115,14 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { gameCubeNode.scale = .init(worldScale, worldScale, worldScale) self.scene?.rootNode.addChildNode(self.gameCubeNode) - self.store.publisher.cubes + self.viewStore.publisher.cubes .sink { cubes in SCNTransaction.begin() SCNTransaction.commit() } .store(in: &self.cancellables) - self.store.publisher.cubes + self.viewStore.publisher.cubes .removeDuplicates(by: { $0.letters == $1.letters }) .sink { [weak self] cubes in guard let self = self else { return } @@ -190,7 +190,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { ambientLightNode.light = ambientLight self.scene?.rootNode.addChildNode(ambientLightNode) - self.store.publisher + self.viewStore.publisher .map { ($0.enableGyroMotion, $0.isOnLowPowerMode) } .removeDuplicates(by: ==) .sink { [weak self] enableGyroMotion, isOnLowPowerMode in @@ -207,7 +207,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { } .store(in: &self.cancellables) - self.store.publisher.playedWords + self.viewStore.publisher.playedWords .sink { [weak self] _ in self?.startingAttitude = nil } .store(in: &self.cancellables) @@ -234,13 +234,13 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { nub.isHidden = true self.addSubview(nub) - self.store.publisher.nub + self.viewStore.publisher.nub .compactMap { $0?.isPressed } .removeDuplicates() .assign(to: \.isPressed, on: nub) .store(in: &self.cancellables) - self.store.publisher.nub + self.viewStore.publisher.nub .compactMap { $0?.location } .removeDuplicates() .sink { [weak self] location in @@ -290,7 +290,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { // TODO: rename private func update() { self.showsStatistics = self.showSceneStatistics - self.light.castsShadow = self.enableCubeShadow && !self.store.withState(\.isOnLowPowerMode) + self.light.castsShadow = self.enableCubeShadow && !self.viewStore.isOnLowPowerMode } deinit { @@ -318,7 +318,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { guard let (_, _, cubeNode) = self.nodes(location: location) else { return } - self.store.send(.doubleTap(index: cubeNode.index), animation: .default) + self.viewStore.send(.doubleTap(index: cubeNode.index), animation: .default) } @objc private func tap(recognizer: UIGestureRecognizer) { @@ -327,7 +327,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { let location = recognizer.location(in: self) - self.store.send( + self.viewStore.send( .tap( recognizer.state, self.nodes(location: location) @@ -356,7 +356,7 @@ public class CubeSceneView: SCNView, UIGestureRecognizerDelegate { ) } - self.store.send(.pan(recognizer.state, panData), animation: .default) + self.viewStore.send(.pan(recognizer.state, panData), animation: .default) } required init?(coder: NSCoder) { From bc41412ea7629522145ddbfcfc5d4ed7c3b05239 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 25 Jan 2024 10:10:21 -0800 Subject: [PATCH 50/61] wip --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6b1d7ee..689039b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: name: macOS runs-on: macOS-13 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # - name: Setup tmate session # uses: mxschmitt/action-tmate@v2 - name: LFS pull @@ -29,8 +29,8 @@ jobs: run: brew link postgresql@15 - name: Start Postgres run: brew services start postgresql@15 - - name: Select Xcode 15.1 - run: sudo xcode-select -s /Applications/Xcode_15.1.app + - name: Select Xcode 15.2 + run: sudo xcode-select -s /Applications/Xcode_15.2.app - name: Bootstrap run: make bootstrap - name: Run tests @@ -40,7 +40,7 @@ jobs: name: Ubuntu runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: 'sudo apt-get --fix-missing update && sudo apt-get install -y wamerican' - name: Bootstrap From 098df9f1d8713a47b18dc9beaf908480c76b2d6b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 25 Jan 2024 11:11:57 -0800 Subject: [PATCH 51/61] wip --- .../SettingsFeatureTests.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Tests/SettingsFeatureTests/SettingsFeatureTests.swift b/Tests/SettingsFeatureTests/SettingsFeatureTests.swift index d4411232..dfe841b7 100644 --- a/Tests/SettingsFeatureTests/SettingsFeatureTests.swift +++ b/Tests/SettingsFeatureTests/SettingsFeatureTests.swift @@ -97,7 +97,7 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.enableNotifications = true - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.enableNotifications = true } @@ -138,7 +138,7 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.enableNotifications = true - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.enableNotifications = true } @@ -179,7 +179,7 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.enableNotifications = false - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.enableNotifications = false } @@ -223,7 +223,7 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.enableNotifications = true - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.alert = .userNotificationAuthorizationDenied } @@ -288,7 +288,7 @@ class SettingsFeatureTests: XCTestCase { } userSettings.sendDailyChallengeReminder = false - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.enableNotifications = false $0.userSettings.sendDailyChallengeReminder = false } @@ -313,7 +313,7 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.musicVolume = 0.5 - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.musicVolume = 0.5 } @@ -335,7 +335,7 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.soundEffectsVolume = 0.5 - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.soundEffectsVolume = 0.5 } @@ -359,13 +359,13 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.colorScheme = .light - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.colorScheme = .light } await overriddenUserInterfaceStyle.withValue { XCTAssertNoDifference($0, .light) } userSettings.colorScheme = .system - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.colorScheme = .system } await overriddenUserInterfaceStyle.withValue { XCTAssertNoDifference($0, .unspecified) } @@ -386,7 +386,7 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.appIcon = .icon2 - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.appIcon = .icon2 } await overriddenIconName.withValue { XCTAssertNoDifference($0, "icon-2") } @@ -421,7 +421,7 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.appIcon = nil - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.appIcon = nil } await overriddenIconName.withValue { XCTAssertNil($0) } @@ -446,7 +446,7 @@ class SettingsFeatureTests: XCTestCase { var developer = store.state.developer developer.currentBaseUrl = .localhost - await store.send(.set(\.$developer, developer)) { + await store.send(.set(\.developer, developer)) { $0.developer.currentBaseUrl = .localhost } await setBaseUrl.withValue { XCTAssertNoDifference($0, URL(string: "http://localhost:9876")!) } @@ -465,11 +465,11 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.enableGyroMotion = false - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.enableGyroMotion = false } userSettings.enableGyroMotion = true - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.enableGyroMotion = true } } @@ -487,11 +487,11 @@ class SettingsFeatureTests: XCTestCase { var userSettings = store.state.userSettings userSettings.enableHaptics = false - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.enableHaptics = false } userSettings.enableHaptics = true - await store.send(.set(\.$userSettings, userSettings)) { + await store.send(.set(\.userSettings, userSettings)) { $0.userSettings.enableHaptics = true } } From 03716cf40cfa8d7e4cadff2d755d6f852b19ff37 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 25 Jan 2024 13:43:23 -0800 Subject: [PATCH 52/61] wip --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 689039b8..7418add7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,8 @@ jobs: - uses: actions/checkout@v4 # - name: Setup tmate session # uses: mxschmitt/action-tmate@v2 + - name: Select Xcode 15.2 + run: sudo xcode-select -s /Applications/Xcode_15.2.app - name: LFS pull run: git lfs pull - name: Install Postgres @@ -29,8 +31,6 @@ jobs: run: brew link postgresql@15 - name: Start Postgres run: brew services start postgresql@15 - - name: Select Xcode 15.2 - run: sudo xcode-select -s /Applications/Xcode_15.2.app - name: Bootstrap run: make bootstrap - name: Run tests From d39632d641bd6f905e08747cc14c6a5de676ba87 Mon Sep 17 00:00:00 2001 From: Imajin Kawabe Date: Thu, 25 Jan 2024 21:44:33 +0000 Subject: [PATCH 53/61] Remove firstLaunchOnboarding (#198) Co-authored-by: Stephen Celis --- Sources/AppFeature/AppView.swift | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Sources/AppFeature/AppView.swift b/Sources/AppFeature/AppView.swift index e39382f7..8d958e71 100644 --- a/Sources/AppFeature/AppView.swift +++ b/Sources/AppFeature/AppView.swift @@ -48,22 +48,6 @@ public struct AppReducer { self.destination = destination self.home = home } - - var firstLaunchOnboarding: Onboarding.State? { - switch self.destination { - case .game, .none: - return nil - - case let .onboarding(onboarding): - switch onboarding.presentationStyle { - case .demo, .help: - return nil - - case .firstLaunch: - return onboarding - } - } - } } public enum Action { From beeda8f379fc7a3b56af8a2727143236086aa3a0 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 6 Apr 2024 20:17:44 -0700 Subject: [PATCH 54/61] use enum reducers --- .../xcshareddata/swiftpm/Package.resolved | 8 +-- Package.swift | 2 +- Sources/ApiClient/Client.swift | 2 +- Sources/AppFeature/AppView.swift | 25 ++------- .../DailyChallengeView.swift | 35 +++--------- Sources/GameOverFeature/GameOverView.swift | 29 ++-------- Sources/HomeFeature/Home.swift | 55 ++++--------------- Sources/LeaderboardFeature/Leaderboard.swift | 21 ++----- .../MultiplayerFeature/MultiplayerView.swift | 21 ++----- Sources/StatsFeature/StatsFeature.swift | 21 ++----- Sources/VocabFeature/Vocab.swift | 21 ++----- 11 files changed, 52 insertions(+), 188 deletions(-) diff --git a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 56b2a296..8bf1bef9 100644 --- a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "76d7791b5bda47df7e3d4690c4c3aaf089730707", - "version" : "1.2.1" + "revision" : "79623dbe2c7672f5e450d8325613d231454390b3", + "version" : "1.3.2" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "branch" : "observation-beta", - "revision" : "77f6c5cd22bade2976e7c6760c15017b66b92f91" + "revision" : "115fe5af41d333b6156d4924d7c7058bc77fd580", + "version" : "1.9.2" } }, { diff --git a/Package.swift b/Package.swift index c0e33d48..bfa5c304 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,7 @@ var package = Package( .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.1.0"), .package( url: "https://github.com/pointfreeco/swift-composable-architecture", - branch: "observation-beta" + from: "1.9.2" ), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.1.0"), diff --git a/Sources/ApiClient/Client.swift b/Sources/ApiClient/Client.swift index 80d3d50c..e1770499 100644 --- a/Sources/ApiClient/Client.swift +++ b/Sources/ApiClient/Client.swift @@ -3,7 +3,7 @@ import Foundation import SharedModels @DependencyClient -public struct ApiClient { +public struct ApiClient: Sendable { public var apiRequest: @Sendable (ServerRoute.Api.Route) async throws -> (Data, URLResponse) public var authenticate: @Sendable (ServerRoute.AuthenticateRequest) async throws -> CurrentPlayerEnvelope diff --git a/Sources/AppFeature/AppView.swift b/Sources/AppFeature/AppView.swift index 8d958e71..77a56939 100644 --- a/Sources/AppFeature/AppView.swift +++ b/Sources/AppFeature/AppView.swift @@ -12,25 +12,10 @@ import SwiftUI @Reducer public struct AppReducer { - @Reducer - public struct Destination { - @ObservableState - public enum State: Equatable { - case game(Game.State) - case onboarding(Onboarding.State) - } - public enum Action { - case game(Game.Action) - case onboarding(Onboarding.Action) - } - public var body: some ReducerOf { - Scope(state: \.game, action: \.game) { - Game() - } - Scope(state: \.onboarding, action: \.onboarding) { - Onboarding() - } - } + @Reducer(state: .equatable) + public enum Destination { + case game(Game) + case onboarding(Onboarding) } @ObservableState @@ -307,7 +292,7 @@ public struct AppReducer { } } .ifLet(\.$destination, action: \.destination) { - Destination() + Destination.body } } } diff --git a/Sources/DailyChallengeFeature/DailyChallengeView.swift b/Sources/DailyChallengeFeature/DailyChallengeView.swift index 0a938181..08602325 100644 --- a/Sources/DailyChallengeFeature/DailyChallengeView.swift +++ b/Sources/DailyChallengeFeature/DailyChallengeView.swift @@ -11,32 +11,13 @@ import SwiftUI @Reducer public struct DailyChallengeReducer { - @Reducer - public struct Destination { - @ObservableState - public enum State: Equatable { - case alert(AlertState) - case notificationsAuthAlert(NotificationsAuthAlert.State) - case results(DailyChallengeResults.State) - } - - public enum Action { - case alert(Alert) - case notificationsAuthAlert(NotificationsAuthAlert.Action) - case results(DailyChallengeResults.Action) - - public enum Alert: Equatable { - } - } + @Reducer(state: .equatable) + public enum Destination { + case alert(AlertState) + case notificationsAuthAlert(NotificationsAuthAlert) + case results(DailyChallengeResults) - public var body: some ReducerOf { - Scope(state: \.notificationsAuthAlert, action: \.notificationsAuthAlert) { - NotificationsAuthAlert() - } - Scope(state: \.results, action: \.results) { - DailyChallengeResults() - } - } + public enum Alert: Equatable {} } @ObservableState @@ -213,12 +194,12 @@ public struct DailyChallengeReducer { } } .ifLet(\.$destination, action: \.destination) { - Destination() + Destination.body } } } -extension AlertState where Action == DailyChallengeReducer.Destination.Action.Alert { +extension AlertState where Action == DailyChallengeReducer.Destination.Alert { static func alreadyPlayed(nextStartsAt: Date) -> Self { Self { TextState("Already played") diff --git a/Sources/GameOverFeature/GameOverView.swift b/Sources/GameOverFeature/GameOverView.swift index 628ea06f..f8b5897d 100644 --- a/Sources/GameOverFeature/GameOverView.swift +++ b/Sources/GameOverFeature/GameOverView.swift @@ -17,27 +17,10 @@ import UserDefaultsClient @Reducer public struct GameOver { - @Reducer - public struct Destination { - @ObservableState - public enum State: Equatable { - case notificationsAuthAlert(NotificationsAuthAlert.State = .init()) - case upgradeInterstitial(UpgradeInterstitial.State = .init()) - } - - public enum Action { - case notificationsAuthAlert(NotificationsAuthAlert.Action) - case upgradeInterstitial(UpgradeInterstitial.Action) - } - - public var body: some ReducerOf { - Scope(state: \.notificationsAuthAlert, action: \.notificationsAuthAlert) { - NotificationsAuthAlert() - } - Scope(state: \.upgradeInterstitial, action: \.upgradeInterstitial) { - UpgradeInterstitial() - } - } + @Reducer(state: .equatable) + public enum Destination { + case notificationsAuthAlert(NotificationsAuthAlert) + case upgradeInterstitial(UpgradeInterstitial) } @ObservableState @@ -184,7 +167,7 @@ public struct GameOver { return .none case .delayedShowUpgradeInterstitial: - state.destination = .upgradeInterstitial() + state.destination = .upgradeInterstitial(UpgradeInterstitial.State()) return .none case .delegate: @@ -375,7 +358,7 @@ public struct GameOver { } } .ifLet(\.$destination, action: \.destination) { - Destination() + Destination.body } } diff --git a/Sources/HomeFeature/Home.swift b/Sources/HomeFeature/Home.swift index dddf3f31..ad59735b 100644 --- a/Sources/HomeFeature/Home.swift +++ b/Sources/HomeFeature/Home.swift @@ -22,47 +22,14 @@ public struct ActiveMatchResponse: Equatable { @Reducer public struct Home { - @Reducer - public struct Destination { - @ObservableState - public enum State: Equatable { - case changelog(ChangelogReducer.State = .init()) - case dailyChallenge(DailyChallengeReducer.State = .init()) - case leaderboard(Leaderboard.State = .init()) - case multiplayer(Multiplayer.State) - case settings(Settings.State = Settings.State()) - case solo(Solo.State = .init()) - } - - public enum Action { - case changelog(ChangelogReducer.Action) - case dailyChallenge(DailyChallengeReducer.Action) - case leaderboard(Leaderboard.Action) - case multiplayer(Multiplayer.Action) - case settings(Settings.Action) - case solo(Solo.Action) - } - - public var body: some ReducerOf { - Scope(state: \.changelog, action: \.changelog) { - ChangelogReducer() - } - Scope(state: \.dailyChallenge, action: \.dailyChallenge) { - DailyChallengeReducer() - } - Scope(state: \.leaderboard, action: \.leaderboard) { - Leaderboard() - } - Scope(state: \.multiplayer, action: \.multiplayer) { - Multiplayer() - } - Scope(state: \.settings, action: \.settings) { - Settings() - } - Scope(state: \.solo, action: \.solo) { - Solo() - } - } + @Reducer(state: .equatable) + public enum Destination { + case changelog(ChangelogReducer) + case dailyChallenge(DailyChallengeReducer) + case leaderboard(Leaderboard) + case multiplayer(Multiplayer) + case settings(Settings) + case solo(Solo) } @ObservableState @@ -165,7 +132,7 @@ public struct Home { public var body: some ReducerOf { Reduce(self.core) .ifLet(\.$destination, action: \.destination) { - Destination() + Destination.body } .ifLet(\.$nagBanner, action: \.nagBanner) { NagBanner() @@ -288,7 +255,7 @@ public struct Home { return .none case .leaderboardButtonTapped: - state.destination = .leaderboard() + state.destination = .leaderboard(Leaderboard.State()) return .none case .multiplayerButtonTapped: @@ -299,7 +266,7 @@ public struct Home { return .none case .settingsButtonTapped: - state.destination = .settings() + state.destination = .settings(Settings.State()) return .none case .soloButtonTapped: diff --git a/Sources/LeaderboardFeature/Leaderboard.swift b/Sources/LeaderboardFeature/Leaderboard.swift index 248242cc..3eabf569 100644 --- a/Sources/LeaderboardFeature/Leaderboard.swift +++ b/Sources/LeaderboardFeature/Leaderboard.swift @@ -31,22 +31,9 @@ public enum LeaderboardScope: CaseIterable, Equatable { @Reducer public struct Leaderboard { - @Reducer - public struct Destination { - @ObservableState - public enum State: Equatable { - case cubePreview(CubePreview.State) - } - - public enum Action { - case cubePreview(CubePreview.Action) - } - - public var body: some ReducerOf { - Scope(state: \.cubePreview, action: \.cubePreview) { - CubePreview() - } - } + @Reducer(state: .equatable) + public enum Destination { + case cubePreview(CubePreview) } @ObservableState @@ -143,7 +130,7 @@ public struct Leaderboard { } } .ifLet(\.$destination, action: \.destination) { - Destination() + Destination.body } Scope(state: \.solo, action: \.solo) { diff --git a/Sources/MultiplayerFeature/MultiplayerView.swift b/Sources/MultiplayerFeature/MultiplayerView.swift index 34aabb7d..a1b9c1d7 100644 --- a/Sources/MultiplayerFeature/MultiplayerView.swift +++ b/Sources/MultiplayerFeature/MultiplayerView.swift @@ -4,22 +4,9 @@ import TcaHelpers @Reducer public struct Multiplayer { - @Reducer - public struct Destination { - @ObservableState - public enum State: Equatable { - case pastGames(PastGames.State) - } - - public enum Action { - case pastGames(PastGames.Action) - } - - public var body: some ReducerOf { - Scope(state: \.pastGames, action: \.pastGames) { - PastGames() - } - } + @Reducer(state: .equatable) + public enum Destination { + case pastGames(PastGames) } @ObservableState @@ -67,7 +54,7 @@ public struct Multiplayer { } } .ifLet(\.$destination, action: \.destination) { - Destination() + Destination.body } } } diff --git a/Sources/StatsFeature/StatsFeature.swift b/Sources/StatsFeature/StatsFeature.swift index 7a3cc5ef..1c9e1fa1 100644 --- a/Sources/StatsFeature/StatsFeature.swift +++ b/Sources/StatsFeature/StatsFeature.swift @@ -6,22 +6,9 @@ import VocabFeature @Reducer public struct Stats { - @Reducer - public struct Destination { - @ObservableState - public enum State: Equatable { - case vocab(Vocab.State) - } - - public enum Action { - case vocab(Vocab.Action) - } - - public var body: some ReducerOf { - Scope(state: \.vocab, action: \.vocab) { - Vocab() - } - } + @Reducer(state: .equatable) + public enum Destination { + case vocab(Vocab) } @ObservableState @@ -113,7 +100,7 @@ public struct Stats { } } .ifLet(\.$destination, action: \.destination) { - Destination() + Destination.body } } } diff --git a/Sources/VocabFeature/Vocab.swift b/Sources/VocabFeature/Vocab.swift index 77a312b4..fa9a9cc7 100644 --- a/Sources/VocabFeature/Vocab.swift +++ b/Sources/VocabFeature/Vocab.swift @@ -5,22 +5,9 @@ import SwiftUI @Reducer public struct Vocab: Reducer { - @Reducer - public struct Destination: Reducer { - @ObservableState - public enum State: Equatable { - case cubePreview(CubePreview.State) - } - - public enum Action { - case cubePreview(CubePreview.Action) - } - - public var body: some ReducerOf { - Scope(state: \.cubePreview, action: \.cubePreview) { - CubePreview() - } - } + @Reducer(state: .equatable) + public enum Destination { + case cubePreview(CubePreview) } @ObservableState @@ -118,7 +105,7 @@ public struct Vocab: Reducer { } } .ifLet(\.$destination, action: \.destination) { - Destination() + Destination.body } } } From 29d18841ca4dc48383b55bdb914a9110ca1388a3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 6 Apr 2024 20:23:07 -0700 Subject: [PATCH 55/61] fix some warnings --- Sources/ComposableGameCenter/Interface.swift | 2 +- Sources/UIApplicationClient/Client.swift | 1 - Sources/UIApplicationClient/LiveKey.swift | 1 - Sources/UIApplicationClient/TestKey.swift | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ComposableGameCenter/Interface.swift b/Sources/ComposableGameCenter/Interface.swift index 7e2b40e7..b2d596f9 100644 --- a/Sources/ComposableGameCenter/Interface.swift +++ b/Sources/ComposableGameCenter/Interface.swift @@ -148,7 +148,7 @@ public struct TurnBasedMatchClient { } @DependencyClient -public struct TurnBasedMatchmakerViewControllerClient { +public struct TurnBasedMatchmakerViewControllerClient: Sendable { public var present: @Sendable (_ showExistingMatches: Bool) async throws -> Void public var dismiss: @Sendable () async -> Void } diff --git a/Sources/UIApplicationClient/Client.swift b/Sources/UIApplicationClient/Client.swift index 5e4925ea..43efb3fd 100644 --- a/Sources/UIApplicationClient/Client.swift +++ b/Sources/UIApplicationClient/Client.swift @@ -13,6 +13,5 @@ public struct UIApplicationClient { public var setAlternateIconName: @Sendable (String?) async throws -> Void // TODO: Should these endpoints be merged and `@MainActor`? Should `Reducer` be `@MainActor`? public var setUserInterfaceStyle: @Sendable (UIUserInterfaceStyle) async -> Void - @available(*, deprecated) public var supportsAlternateIcons: () -> Bool = { false } public var supportsAlternateIconsAsync: @Sendable () async -> Bool = { false } } diff --git a/Sources/UIApplicationClient/LiveKey.swift b/Sources/UIApplicationClient/LiveKey.swift index 0c9928c7..7bbf5daf 100644 --- a/Sources/UIApplicationClient/LiveKey.swift +++ b/Sources/UIApplicationClient/LiveKey.swift @@ -18,7 +18,6 @@ extension UIApplicationClient: DependencyKey { scene.keyWindow?.overrideUserInterfaceStyle = userInterfaceStyle } }, - supportsAlternateIcons: { UIApplication.shared.supportsAlternateIcons }, supportsAlternateIconsAsync: { await UIApplication.shared.supportsAlternateIcons } ) } diff --git a/Sources/UIApplicationClient/TestKey.swift b/Sources/UIApplicationClient/TestKey.swift index dbbb617e..40246617 100644 --- a/Sources/UIApplicationClient/TestKey.swift +++ b/Sources/UIApplicationClient/TestKey.swift @@ -20,7 +20,6 @@ extension UIApplicationClient { openSettingsURLString: { "settings://isowords/settings" }, setAlternateIconName: { _ in }, setUserInterfaceStyle: { _ in }, - supportsAlternateIcons: { true }, supportsAlternateIconsAsync: { true } ) } From b93b94c1a3b104faaa79f99d50ea1547f2725316 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 8 Apr 2024 14:07:05 -0700 Subject: [PATCH 56/61] udpate last destination reducer --- Sources/GameCore/GameCore.swift | 73 ++++++++-------------- Sources/GameOverFeature/DismissGame.swift | 15 +++++ Sources/GameOverFeature/GameOverView.swift | 10 +-- 3 files changed, 45 insertions(+), 53 deletions(-) create mode 100644 Sources/GameOverFeature/DismissGame.swift diff --git a/Sources/GameCore/GameCore.swift b/Sources/GameCore/GameCore.swift index 1c81a218..40ae2aa3 100644 --- a/Sources/GameCore/GameCore.swift +++ b/Sources/GameCore/GameCore.swift @@ -5,6 +5,7 @@ import ClientModels import ComposableArchitecture import ComposableGameCenter import CubeCore +import Dependencies import DictionaryClient import GameOverFeature import HapticsCore @@ -20,51 +21,26 @@ import UserSettingsClient @Reducer public struct Game { - @Reducer - public struct Destination { - @ObservableState - public enum State: Equatable { - case alert(AlertState) - case bottomMenu(BottomMenuState) - case gameOver(GameOver.State) - case settings(Settings.State = Settings.State()) - case upgradeInterstitial(UpgradeInterstitial.State = .init()) + @Reducer(state: .equatable) + public enum Destination { + case alert(AlertState) + @ReducerCaseEphemeral + case bottomMenu(BottomMenuState) + case gameOver(GameOver) + case settings(Settings) + case upgradeInterstitial(UpgradeInterstitial) + + @CasePathable + public enum Alert { + case forfeitButtonTapped } - - public enum Action { - case alert(Alert) - case bottomMenu(BottomMenu) - case gameOver(GameOver.Action) - case settings(Settings.Action) - case upgradeInterstitial(UpgradeInterstitial.Action) - - @CasePathable - public enum Alert { - case forfeitButtonTapped - } - @CasePathable - public enum BottomMenu: Equatable { - case confirmRemoveCube(LatticePoint) - case endGameButtonTapped - case exitButtonTapped - case forfeitGameButtonTapped - case settingsButtonTapped - } - } - - let dismissGame: DismissEffect - - public var body: some ReducerOf { - Scope(state: \.gameOver, action: \.gameOver) { - GameOver() - .dependency(\.dismiss, self.dismissGame) - } - Scope(state: \.settings, action: \.settings) { - Settings() - } - Scope(state: \.upgradeInterstitial, action: \.upgradeInterstitial) { - UpgradeInterstitial() - } + @CasePathable + public enum BottomMenu: Equatable { + case confirmRemoveCube(LatticePoint) + case endGameButtonTapped + case exitButtonTapped + case forfeitGameButtonTapped + case settingsButtonTapped } } @@ -220,7 +196,8 @@ public struct Game { } .filterActionsForYourTurn() .ifLet(\.$destination, action: \.destination) { - Destination(dismissGame: self.dismiss) + Destination.body + .dependency(\.dismissGame, self.dismiss) } .sounds() } @@ -241,7 +218,7 @@ public struct Game { return .none case .delayedShowUpgradeInterstitial: - state.destination = .upgradeInterstitial() + state.destination = .upgradeInterstitial(UpgradeInterstitial.State()) return .none case .destination(.presented(.alert(.forfeitButtonTapped))): @@ -301,7 +278,7 @@ public struct Game { return .none case .destination(.presented(.bottomMenu(.settingsButtonTapped))): - state.destination = .settings() + state.destination = .settings(Settings.State()) return .none case let .destination(.presented(.gameOver(.delegate(.startGame(inProgressGame))))): @@ -572,7 +549,7 @@ extension TurnBasedMatchData { } } -extension BottomMenuState where Action == Game.Destination.Action.BottomMenu { +extension BottomMenuState where Action == Game.Destination.BottomMenu { public static func removeCube( index: LatticePoint, state: Game.State, diff --git a/Sources/GameOverFeature/DismissGame.swift b/Sources/GameOverFeature/DismissGame.swift new file mode 100644 index 00000000..1bb13b99 --- /dev/null +++ b/Sources/GameOverFeature/DismissGame.swift @@ -0,0 +1,15 @@ +import ComposableArchitecture +import Dependencies + +private enum DismissGameKey: DependencyKey { + static var liveValue: DismissEffect { + @Dependency(\.dismiss) var dismiss + return dismiss + } +} +extension DependencyValues { + public var dismissGame: DismissEffect { + get { self[DismissGameKey.self] } + set { self[DismissGameKey.self] = newValue } + } +} diff --git a/Sources/GameOverFeature/GameOverView.swift b/Sources/GameOverFeature/GameOverView.swift index f8b5897d..a93559c5 100644 --- a/Sources/GameOverFeature/GameOverView.swift +++ b/Sources/GameOverFeature/GameOverView.swift @@ -127,7 +127,7 @@ public struct GameOver { @Dependency(\.apiClient) var apiClient @Dependency(\.audioPlayer) var audioPlayer @Dependency(\.database) var database - @Dependency(\.dismiss) var dismiss + @Dependency(\.dismissGame) var dismissGame @Dependency(\.fileClient) var fileClient @Dependency(\.mainRunLoop) var mainRunLoop @Dependency(\.storeKit.requestReview) var requestReview @@ -148,7 +148,7 @@ public struct GameOver { else { return .run { send in try? await self.requestReviewAsync() - await self.dismiss(animation: .default) + await self.dismissGame(animation: .default) } } @@ -208,14 +208,14 @@ public struct GameOver { where state.destination.is(\.some.notificationsAuthAlert): return .run { _ in try? await self.requestReviewAsync() - await self.dismiss(animation: .default) + await self.dismissGame(animation: .default) } case .destination( .presented(.notificationsAuthAlert(.delegate(.didChooseNotificationSettings))) ): return .run { _ in - await self.dismiss(animation: .default) + await self.dismissGame(animation: .default) } case .destination: @@ -276,7 +276,7 @@ public struct GameOver { return .run { [completedGame = state.completedGame, isDemo = state.isDemo] send in guard isDemo || completedGame.currentScore > 0 else { - await self.dismiss(animation: .default) + await self.dismissGame(animation: .default) return } From b9d6bed813e16633ca69bbd65c6b004a9b6f5019 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 8 Apr 2024 14:10:28 -0700 Subject: [PATCH 57/61] ignore some errors --- Sources/AppFeature/AppDelegate.swift | 1 + Sources/AppFeature/AppView.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/AppFeature/AppDelegate.swift b/Sources/AppFeature/AppDelegate.swift index b4e6077b..31061143 100644 --- a/Sources/AppFeature/AppDelegate.swift +++ b/Sources/AppFeature/AppDelegate.swift @@ -94,6 +94,7 @@ public struct AppDelegateReducer { ) ) ) + } catch: { _, _ in } case let .userNotifications(.willPresentNotification(_, completionHandler)): diff --git a/Sources/AppFeature/AppView.swift b/Sources/AppFeature/AppView.swift index 77a56939..dcf417e4 100644 --- a/Sources/AppFeature/AppView.swift +++ b/Sources/AppFeature/AppView.swift @@ -266,6 +266,7 @@ public struct AppReducer { ) async let refresh = self.refreshServerConfig() _ = try await (register, refresh) + } catch: { _, _ in } case .didChangeScenePhase: From dae32ea5db5ce0c3bfca7a64f794de9f8b78bfa6 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 8 Apr 2024 21:08:42 -0700 Subject: [PATCH 58/61] wip --- .../xcschemes/UserSettingsClient.xcscheme | 67 +++++++++++++++++++ Sources/SettingsFeature/Mocks.swift | 17 ----- 2 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/UserSettingsClient.xcscheme delete mode 100644 Sources/SettingsFeature/Mocks.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/UserSettingsClient.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/UserSettingsClient.xcscheme new file mode 100644 index 00000000..71cfd4b6 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/UserSettingsClient.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/SettingsFeature/Mocks.swift b/Sources/SettingsFeature/Mocks.swift deleted file mode 100644 index 6323f5af..00000000 --- a/Sources/SettingsFeature/Mocks.swift +++ /dev/null @@ -1,17 +0,0 @@ -#if DEBUG - import Dependencies - import UserSettingsClient - - extension Settings.State { - public static let everythingOff = withDependencies { - $0.userSettings = .mock( - initialUserSettings: UserSettings( - enableGyroMotion: false, - enableHaptics: false - ) - ) - } operation: { - Self() - } - } -#endif From ba30ab2b43802aa644acde914e5519ec1c598e22 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 9 Apr 2024 10:54:07 -0700 Subject: [PATCH 59/61] wip --- .../xcshareddata/swiftpm/Package.resolved | 42 +++++++++---------- Package.swift | 5 +-- Tests/AppFeatureTests/PersistenceTests.swift | 6 ++- .../RemoteNotificationsTests.swift | 4 +- Tests/AppFeatureTests/TurnBasedTests.swift | 16 ++++--- .../UserNotificationsTests.swift | 3 +- ...ailyChallengeFeatureIntegrationTests.swift | 2 +- .../DailyChallengeFeatureTests.swift | 8 +++- Tests/GameCoreTests/GameCoreTests.swift | 2 +- .../GameOverFeatureTests.swift | 31 ++++++++------ .../OnboardingFeatureTests.swift | 4 +- .../SettingsFeatureTests.swift | 15 ++++++- .../SettingsPurchaseTests.swift | 8 +++- 13 files changed, 93 insertions(+), 53 deletions(-) diff --git a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8bf1bef9..d2cb4c1a 100644 --- a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", - "version" : "1.0.6" + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" } }, { @@ -129,7 +129,7 @@ { "identity" : "swift-crypto", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-crypto", + "location" : "https://github.com/apple/swift-crypto.git", "state" : { "revision" : "ddb07e896a2a8af79512543b1c7eb9797f8898a5", "version" : "1.1.7" @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", - "version" : "1.1.2" + "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", + "version" : "1.3.0" } }, { @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "adb04a8e35f07edc001877af9f9f97fcc21d409e", - "version" : "1.2.0" + "revision" : "d3a5af3038a09add4d7682f66555d6212058a3c0", + "version" : "1.2.2" } }, { @@ -212,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "635b2589494c97e48c62514bc8b37ced762e0a62", - "version" : "2.63.0" + "revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982", + "version" : "2.64.0" } }, { @@ -221,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "363da63c1966405764f380c627409b2f9d9e710b", - "version" : "1.21.0" + "revision" : "a3b640d7dc567225db7c94386a6e71aded1bfa63", + "version" : "1.22.0" } }, { @@ -284,8 +284,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "42240120b2a8797595433288ab4118f8042214c3", - "version" : "1.1.1" + "revision" : "520c458a832d1287e6b698c5f657ae848fd696ff", + "version" : "1.1.4" } }, { @@ -302,8 +302,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "8e68404f641300bfd0e37d478683bb275926760c", - "version" : "1.15.2" + "revision" : "625ccca8570773dd84a34ee51a81aa2bc5a4f97a", + "version" : "1.16.0" } }, { @@ -311,8 +311,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax", "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" + "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", + "version" : "510.0.1" } }, { @@ -365,8 +365,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation", "state" : { - "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc", - "version" : "1.2.0" + "revision" : "2ec6c3a15293efff6083966b38439a4004f25565", + "version" : "1.3.0" } }, { @@ -374,8 +374,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "b58e6627149808b40634c4552fcf2f44d0b3ca87", - "version" : "1.1.0" + "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", + "version" : "1.1.2" } } ], diff --git a/Package.swift b/Package.swift index bfa5c304..b9538df5 100644 --- a/Package.swift +++ b/Package.swift @@ -28,10 +28,7 @@ var package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-crypto", from: "1.1.6"), .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.1.0"), - .package( - url: "https://github.com/pointfreeco/swift-composable-architecture", - from: "1.9.2" - ), + .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.9.2"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.1.0"), .package(url: "https://github.com/pointfreeco/swift-gen", from: "0.3.0"), diff --git a/Tests/AppFeatureTests/PersistenceTests.swift b/Tests/AppFeatureTests/PersistenceTests.swift index 877117f2..d798225c 100644 --- a/Tests/AppFeatureTests/PersistenceTests.swift +++ b/Tests/AppFeatureTests/PersistenceTests.swift @@ -20,8 +20,8 @@ import XCTest @testable import SoloFeature @testable import UserDefaultsClient -@MainActor class PersistenceTests: XCTestCase { + @MainActor func testUnlimitedSaveAndQuit() async throws { let saves = ActorIsolated<[Data]>([]) @@ -143,6 +143,7 @@ class PersistenceTests: XCTestCase { } } + @MainActor func testUnlimitedAbandon() async throws { let didArchiveGame = ActorIsolated(false) let saves = ActorIsolated<[Data]>([]) @@ -206,6 +207,7 @@ class PersistenceTests: XCTestCase { } } + @MainActor func testTimedAbandon() async { let didArchiveGame = ActorIsolated(false) @@ -254,6 +256,7 @@ class PersistenceTests: XCTestCase { await didArchiveGame.withValue { XCTAssert($0) } } + @MainActor func testUnlimitedResume() async { let savedGames = SavedGamesState(dailyChallengeUnlimited: nil, unlimited: .mock) let store = TestStore( @@ -282,6 +285,7 @@ class PersistenceTests: XCTestCase { await task.cancel() } + @MainActor func testTurnBasedAbandon() async { let store = TestStore( initialState: AppReducer.State( diff --git a/Tests/AppFeatureTests/RemoteNotificationsTests.swift b/Tests/AppFeatureTests/RemoteNotificationsTests.swift index f7999830..28e89267 100644 --- a/Tests/AppFeatureTests/RemoteNotificationsTests.swift +++ b/Tests/AppFeatureTests/RemoteNotificationsTests.swift @@ -9,8 +9,8 @@ import XCTest @testable import AppFeature -@MainActor class RemoteNotificationsTests: XCTestCase { + @MainActor func testRegisterForRemoteNotifications_OnActivate_Authorized() async { let didRegisterForRemoteNotifications = ActorIsolated(false) let requestedAuthorizationOptions = ActorIsolated(nil) @@ -68,6 +68,7 @@ class RemoteNotificationsTests: XCTestCase { await task.cancel() } + @MainActor func testRegisterForRemoteNotifications_NotAuthorized() async { let didRegisterForRemoteNotifications = ActorIsolated(false) let requestedAuthorizationOptions = ActorIsolated(nil) @@ -94,6 +95,7 @@ class RemoteNotificationsTests: XCTestCase { await task.cancel() } + @MainActor func testReceiveNotification_dailyChallengeEndsSoon() async { let inProgressGame = InProgressGame.mock diff --git a/Tests/AppFeatureTests/TurnBasedTests.swift b/Tests/AppFeatureTests/TurnBasedTests.swift index 19bbb842..a70fe3dc 100644 --- a/Tests/AppFeatureTests/TurnBasedTests.swift +++ b/Tests/AppFeatureTests/TurnBasedTests.swift @@ -21,11 +21,11 @@ import XCTest @testable import ComposableGameCenter @testable import HomeFeature -@MainActor class TurnBasedTests: XCTestCase { let mainQueue = DispatchQueue.test let mainRunLoop = RunLoop.test + @MainActor func testNewGame() async throws { try await withMainSerialExecutor { let didEndTurnWithRequest = ActorIsolated(nil) @@ -113,13 +113,13 @@ class TurnBasedTests: XCTestCase { await store.receive(\.home.serverConfigResponse) { $0.home.hasChangelog = true } - await store.receive(\.home.activeMatchesResponse.success) await store.receive(\.home.dailyChallengeResponse.success) { $0.home.dailyChallenges = dailyChallenges } await store.receive(\.home.weekInReviewResponse.success) { $0.home.weekInReview = weekInReview } + await store.receive(\.home.activeMatchesResponse.success) await store.send(.home(.destination(.presented(.multiplayer(.startButtonTapped))))) @@ -274,6 +274,7 @@ class TurnBasedTests: XCTestCase { } } + @MainActor func testResumeGame() async { await withMainSerialExecutor { let listener = AsyncStreamProducer() @@ -338,13 +339,13 @@ class TurnBasedTests: XCTestCase { await store.receive(\.home.serverConfigResponse) { $0.home.hasChangelog = true } - await store.receive(\.home.activeMatchesResponse.success) await store.receive(\.home.dailyChallengeResponse.success) { $0.home.dailyChallenges = dailyChallenges } await store.receive(\.home.weekInReviewResponse.success) { $0.home.weekInReview = weekInReview } + await store.receive(\.home.activeMatchesResponse.success) listener.continuation .yield(.turnBased(.receivedTurnEventForMatch(.inProgress, didBecomeActive: true))) @@ -378,6 +379,7 @@ class TurnBasedTests: XCTestCase { } } + @MainActor func testResumeForfeitedGame() async { await withMainSerialExecutor { let listener = AsyncStreamProducer() @@ -439,14 +441,14 @@ class TurnBasedTests: XCTestCase { await store.receive(\.home.serverConfigResponse) { $0.home.hasChangelog = true } - await store.receive(\.home.activeMatchesResponse.success) await store.receive(\.home.dailyChallengeResponse.success) { $0.home.dailyChallenges = dailyChallenges } await store.receive(\.home.weekInReviewResponse.success) { $0.home.weekInReview = weekInReview } - + await store.receive(\.home.activeMatchesResponse.success) + listener.continuation .yield(.turnBased(.receivedTurnEventForMatch(.forfeited, didBecomeActive: true))) @@ -488,6 +490,7 @@ class TurnBasedTests: XCTestCase { } } + @MainActor func testRemovingCubes() async throws { let didEndTurnWithRequest = ActorIsolated(nil) let match = update(TurnBasedMatch.inProgress) { @@ -653,6 +656,7 @@ class TurnBasedTests: XCTestCase { } } + @MainActor func testRematch() async { let localParticipant = TurnBasedParticipant.local let match = update(TurnBasedMatch.inProgress) { @@ -737,6 +741,7 @@ class TurnBasedTests: XCTestCase { } } + @MainActor func testGameCenterNotification_ShowsRecentTurn() async { let localParticipant = TurnBasedParticipant.local let remoteParticipant = update(TurnBasedParticipant.remote) { @@ -805,6 +810,7 @@ class TurnBasedTests: XCTestCase { } } + @MainActor func testGameCenterNotification_DoesNotShow() async { let localParticipant = TurnBasedParticipant.local let remoteParticipant = update(TurnBasedParticipant.remote) { diff --git a/Tests/AppFeatureTests/UserNotificationsTests.swift b/Tests/AppFeatureTests/UserNotificationsTests.swift index 2a03049e..8ecc5b50 100644 --- a/Tests/AppFeatureTests/UserNotificationsTests.swift +++ b/Tests/AppFeatureTests/UserNotificationsTests.swift @@ -7,8 +7,8 @@ import XCTest @testable import AppFeature -@MainActor class UserNotificationsTests: XCTestCase { + @MainActor func testReceiveBackgroundNotification() async { let delegate = AsyncStream.makeStream() let response = UserNotificationClient.Notification.Response( @@ -43,6 +43,7 @@ class UserNotificationsTests: XCTestCase { await task.cancel() } + @MainActor func testReceiveForegroundNotification() async { let delegate = AsyncStream.makeStream() let notification = UserNotificationClient.Notification( diff --git a/Tests/DailyChallengeFeatureIntegrationTests/DailyChallengeFeatureIntegrationTests.swift b/Tests/DailyChallengeFeatureIntegrationTests/DailyChallengeFeatureIntegrationTests.swift index 1a00ada0..6da53602 100644 --- a/Tests/DailyChallengeFeatureIntegrationTests/DailyChallengeFeatureIntegrationTests.swift +++ b/Tests/DailyChallengeFeatureIntegrationTests/DailyChallengeFeatureIntegrationTests.swift @@ -13,8 +13,8 @@ import XCTest @testable import LeaderboardFeature -@MainActor class DailyChallengeFeatureTests: XCTestCase { + @MainActor func testBasics() async { await withMainSerialExecutor { let uuid = UUID.incrementing diff --git a/Tests/DailyChallengeFeatureTests/DailyChallengeFeatureTests.swift b/Tests/DailyChallengeFeatureTests/DailyChallengeFeatureTests.swift index 18cd44ff..b76efd52 100644 --- a/Tests/DailyChallengeFeatureTests/DailyChallengeFeatureTests.swift +++ b/Tests/DailyChallengeFeatureTests/DailyChallengeFeatureTests.swift @@ -7,11 +7,11 @@ import XCTest @testable import DailyChallengeFeature @testable import SharedModels -@MainActor class DailyChallengeFeatureTests: XCTestCase { let mainQueue = DispatchQueue.test let mainRunLoop = RunLoop.test + @MainActor func testOnAppear() async { let store = TestStore(initialState: DailyChallengeReducer.State()) { DailyChallengeReducer() @@ -36,6 +36,7 @@ class DailyChallengeFeatureTests: XCTestCase { } } + @MainActor func testTapGameThatWasPlayed() async { var dailyChallengeResponse = FetchTodaysDailyChallengeResponse.played dailyChallengeResponse.dailyChallenge.endsAt = Date().addingTimeInterval(60 * 60 * 2 + 1) @@ -53,6 +54,7 @@ class DailyChallengeFeatureTests: XCTestCase { } } + @MainActor func testTapGameThatWasNotStarted() async { var inProgressGame = InProgressGame.mock inProgressGame.gameStartTime = self.mainRunLoop.now.date @@ -100,6 +102,7 @@ class DailyChallengeFeatureTests: XCTestCase { await store.receive(\.delegate.startGame) } + @MainActor func testTapGameThatWasStarted_NotPlayed_HasLocalGame() async { var inProgressGame = InProgressGame.mock inProgressGame.gameStartTime = .mock @@ -132,6 +135,7 @@ class DailyChallengeFeatureTests: XCTestCase { await store.receive(\.delegate.startGame) } + @MainActor func testNotifications_OpenThenClose() async { let store = TestStore( initialState: DailyChallengeReducer.State() @@ -147,6 +151,7 @@ class DailyChallengeFeatureTests: XCTestCase { } } + @MainActor func testNotifications_GrantAccess() async { let didRegisterForRemoteNotifications = ActorIsolated(false) @@ -181,6 +186,7 @@ class DailyChallengeFeatureTests: XCTestCase { await didRegisterForRemoteNotifications.withValue { XCTAssertNoDifference($0, true) } } + @MainActor func testNotifications_DenyAccess() async { let store = TestStore(initialState: DailyChallengeReducer.State()) { DailyChallengeReducer() diff --git a/Tests/GameCoreTests/GameCoreTests.swift b/Tests/GameCoreTests/GameCoreTests.swift index 69ae3230..1990d7f4 100644 --- a/Tests/GameCoreTests/GameCoreTests.swift +++ b/Tests/GameCoreTests/GameCoreTests.swift @@ -4,8 +4,8 @@ import GameCore import GameOverFeature import XCTest -@MainActor class GameCoreTests: XCTestCase { + @MainActor func testForfeitTurnBasedGame() async { let didEndMatchInTurn = ActorIsolated(false) diff --git a/Tests/GameOverFeatureTests/GameOverFeatureTests.swift b/Tests/GameOverFeatureTests/GameOverFeatureTests.swift index d3cdd7e6..3b843628 100644 --- a/Tests/GameOverFeatureTests/GameOverFeatureTests.swift +++ b/Tests/GameOverFeatureTests/GameOverFeatureTests.swift @@ -6,15 +6,14 @@ import GameOverFeature import Overture import SharedModels import TestHelpers +import UpgradeInterstitialFeature import XCTest @testable import LocalDatabaseClient @testable import UserDefaultsClient -@MainActor class GameOverFeatureTests: XCTestCase { - let mainRunLoop = RunLoop.test - + @MainActor func testSubmitLeaderboardScore() async throws { await withMainSerialExecutor { let store = TestStore( @@ -79,6 +78,7 @@ class GameOverFeatureTests: XCTestCase { } } + @MainActor func testSubmitDailyChallenge() async { await withMainSerialExecutor { let dailyChallengeResponses = [ @@ -184,6 +184,7 @@ class GameOverFeatureTests: XCTestCase { } } + @MainActor func testTurnBased_TrackLeaderboards() async { await withMainSerialExecutor { let store = TestStore( @@ -238,7 +239,9 @@ class GameOverFeatureTests: XCTestCase { } } + @MainActor func testRequestReviewOnClose() async { + let mainRunLoop = RunLoop.test let lastReviewRequestTimeIntervalSet = ActorIsolated(nil) let requestReviewCount = ActorIsolated(0) @@ -272,7 +275,7 @@ class GameOverFeatureTests: XCTestCase { wordsFound: 1 ) } - $0.mainRunLoop = self.mainRunLoop.eraseToAnyScheduler() + $0.mainRunLoop = mainRunLoop.eraseToAnyScheduler() $0.storeKit.requestReview = { await requestReviewCount.withValue { $0 += 1 } } @@ -282,12 +285,12 @@ class GameOverFeatureTests: XCTestCase { await lastReviewRequestTimeIntervalSet.setValue(double) } } - $0.dismiss = DismissEffect {} + $0.dismissGame = DismissEffect {} } // Assert that the first time game over appears we do not request review await store.send(.closeButtonTapped) - await self.mainRunLoop.advance() + await mainRunLoop.advance() await requestReviewCount.withValue { XCTAssertNoDifference($0, 0) } await lastReviewRequestTimeIntervalSet.withValue { XCTAssertNoDifference($0, nil) } @@ -307,12 +310,13 @@ class GameOverFeatureTests: XCTestCase { await lastReviewRequestTimeIntervalSet.withValue { XCTAssertNoDifference($0, 0) } // Assert that when more than a week of time passes we again request review - await self.mainRunLoop.advance(by: .seconds(60 * 60 * 24 * 7)) + await mainRunLoop.advance(by: .seconds(60 * 60 * 24 * 7)) await store.send(.closeButtonTapped).finish() await requestReviewCount.withValue { XCTAssertNoDifference($0, 2) } await lastReviewRequestTimeIntervalSet.withValue { XCTAssertNoDifference($0, 60 * 60 * 24 * 7) } } + @MainActor func testAutoCloseWhenNoWordsPlayed() async throws { let store = TestStore( initialState: GameOver.State( @@ -330,13 +334,15 @@ class GameOverFeatureTests: XCTestCase { ) { GameOver() } withDependencies: { - $0.dismiss = DismissEffect {} + $0.dismissGame = DismissEffect {} } await store.send(.task) } + @MainActor func testShowUpgradeInterstitial() async { + let mainRunLoop = RunLoop.test let store = TestStore( initialState: GameOver.State( completedGame: .init( @@ -357,7 +363,7 @@ class GameOverFeatureTests: XCTestCase { $0.apiClient.currentPlayer = { .init(appleReceipt: nil, player: .blob) } $0.apiClient.apiRequest = { @Sendable _ in try await Task.never() } $0.database.playedGamesCount = { _ in 6 } - $0.mainRunLoop = self.mainRunLoop.eraseToAnyScheduler() + $0.mainRunLoop = mainRunLoop.eraseToAnyScheduler() $0.serverConfig.config = { .init() } $0.userNotifications.getNotificationSettings = { (try? await Task.never()) ?? .init(authorizationStatus: .notDetermined) @@ -365,15 +371,16 @@ class GameOverFeatureTests: XCTestCase { } let task = await store.send(.task) - await self.mainRunLoop.advance(by: .seconds(1)) + await mainRunLoop.advance(by: .seconds(1)) await store.receive(\.delayedShowUpgradeInterstitial) { - $0.destination = .upgradeInterstitial() + $0.destination = .upgradeInterstitial(UpgradeInterstitial.State()) } - await self.mainRunLoop.advance(by: .seconds(1)) + await mainRunLoop.advance(by: .seconds(1)) await store.receive(\.delayedOnAppear) { $0.isViewEnabled = true } await task.cancel() } + @MainActor func testSkipUpgradeIfLessThan6GamesPlayed() async { let store = TestStore( initialState: GameOver.State( diff --git a/Tests/OnboardingFeatureTests/OnboardingFeatureTests.swift b/Tests/OnboardingFeatureTests/OnboardingFeatureTests.swift index 4a67f956..fab0ceb7 100644 --- a/Tests/OnboardingFeatureTests/OnboardingFeatureTests.swift +++ b/Tests/OnboardingFeatureTests/OnboardingFeatureTests.swift @@ -10,10 +10,10 @@ extension DateGenerator { } } -@MainActor class OnboardingFeatureTests: XCTestCase { let mainQueue = DispatchQueue.test + @MainActor func testBasics_FirstLaunch() async { let isFirstLaunchOnboardingKeySet = ActorIsolated(false) @@ -308,6 +308,7 @@ class OnboardingFeatureTests: XCTestCase { await isFirstLaunchOnboardingKeySet.withValue { XCTAssert($0) } } + @MainActor func testSkip_HasSeenOnboardingBefore() async { let isFirstLaunchOnboardingKeySet = ActorIsolated(false) @@ -342,6 +343,7 @@ class OnboardingFeatureTests: XCTestCase { await isFirstLaunchOnboardingKeySet.withValue { XCTAssert($0) } } + @MainActor func testSkip_HasNotSeenOnboardingBefore() async { let isFirstLaunchOnboardingKeySet = ActorIsolated(false) diff --git a/Tests/SettingsFeatureTests/SettingsFeatureTests.swift b/Tests/SettingsFeatureTests/SettingsFeatureTests.swift index dfe841b7..4ab0e40b 100644 --- a/Tests/SettingsFeatureTests/SettingsFeatureTests.swift +++ b/Tests/SettingsFeatureTests/SettingsFeatureTests.swift @@ -28,8 +28,8 @@ extension DependencyValues { } } -@MainActor class SettingsFeatureTests: XCTestCase { + @MainActor func testUserSettingsBackwardsDecodability() { XCTAssertNoDifference( try JSONDecoder().decode(UserSettings.self, from: Data("{}".utf8)), @@ -61,7 +61,7 @@ class SettingsFeatureTests: XCTestCase { // MARK: - Notifications - // TODO: Fix once we have the TestStore binding test helper + @MainActor func testEnableNotifications_NotDetermined_GrantAuthorization() async { let didRegisterForRemoteNotifications = ActorIsolated(false) @@ -108,6 +108,7 @@ class SettingsFeatureTests: XCTestCase { await task.cancel() } + @MainActor func testEnableNotifications_NotDetermined_DenyAuthorization() async { let store = TestStore( initialState: Settings.State() @@ -149,6 +150,7 @@ class SettingsFeatureTests: XCTestCase { await task.cancel() } + @MainActor func testNotifications_PreviouslyGranted() async { let store = TestStore( initialState: Settings.State() @@ -186,6 +188,7 @@ class SettingsFeatureTests: XCTestCase { await task.cancel() } + @MainActor func testNotifications_PreviouslyDenied() async { let openedUrl = ActorIsolated(nil) let store = TestStore( @@ -238,6 +241,7 @@ class SettingsFeatureTests: XCTestCase { await task.cancel() } + @MainActor func testNotifications_RemoteSettingsUpdates() async { var userSettings = UserSettings(sendDailyChallengeReminder: false) let didUpdate = LockIsolated(false) @@ -300,6 +304,7 @@ class SettingsFeatureTests: XCTestCase { // MARK: - Sounds + @MainActor func testSetMusicVolume() async { let setMusicVolume = ActorIsolated(nil) let store = TestStore( @@ -344,6 +349,7 @@ class SettingsFeatureTests: XCTestCase { // MARK: - Appearance + @MainActor func testSetColorScheme() async { let overriddenUserInterfaceStyle = ActorIsolated(nil) let store = TestStore( @@ -371,6 +377,7 @@ class SettingsFeatureTests: XCTestCase { await overriddenUserInterfaceStyle.withValue { XCTAssertNoDifference($0, .unspecified) } } + @MainActor func testSetAppIcon() async { let overriddenIconName = ActorIsolated(nil) let store = TestStore( @@ -392,6 +399,7 @@ class SettingsFeatureTests: XCTestCase { await overriddenIconName.withValue { XCTAssertNoDifference($0, "icon-2") } } + @MainActor func testUnsetAppIcon() async { let overriddenIconName = ActorIsolated(nil) let store = TestStore( @@ -431,6 +439,7 @@ class SettingsFeatureTests: XCTestCase { // MARK: - Developer + @MainActor func testSetApiBaseUrl() async { let setBaseUrl = ActorIsolated(nil) let didLogout = ActorIsolated(false) @@ -453,6 +462,7 @@ class SettingsFeatureTests: XCTestCase { await didLogout.withValue { XCTAssert($0) } } + @MainActor func testToggleEnableGyroMotion() async { let store = TestStore( initialState: Settings.State() @@ -474,6 +484,7 @@ class SettingsFeatureTests: XCTestCase { } } + @MainActor func testToggleEnableHaptics() async { let store = TestStore( initialState: Settings.State() diff --git a/Tests/SettingsFeatureTests/SettingsPurchaseTests.swift b/Tests/SettingsFeatureTests/SettingsPurchaseTests.swift index a0dcf3e6..fd31f30a 100644 --- a/Tests/SettingsFeatureTests/SettingsPurchaseTests.swift +++ b/Tests/SettingsFeatureTests/SettingsPurchaseTests.swift @@ -22,8 +22,8 @@ fileprivate extension DependencyValues { } } -@MainActor class SettingsPurchaseTests: XCTestCase { + @MainActor func testUpgrade_HappyPath() async throws { let didAddPaymentProductIdentifier = ActorIsolated(nil) let storeKitObserver = AsyncStream @@ -41,7 +41,8 @@ class SettingsPurchaseTests: XCTestCase { $0.apiClient.currentPlayer = { .some(.blobWithoutPurchase) } $0.apiClient.refreshCurrentPlayer = { .blobWithPurchase } $0.storeKit.addPayment = { - await didAddPaymentProductIdentifier.setValue($0.productIdentifier) + let productIdentifier = $0.productIdentifier + await didAddPaymentProductIdentifier.setValue(productIdentifier) } $0.storeKit.fetchProducts = { _ in .init(invalidProductIdentifiers: [], products: [.fullGame]) @@ -77,6 +78,7 @@ class SettingsPurchaseTests: XCTestCase { await task.cancel() } + @MainActor func testRestore_HappyPath() async throws { let didRestoreCompletedTransactions = ActorIsolated(false) let storeKitObserver = AsyncStream @@ -129,6 +131,7 @@ class SettingsPurchaseTests: XCTestCase { await task.cancel() } + @MainActor func testRestore_NoPurchasesPath() async throws { let didRestoreCompletedTransactions = ActorIsolated(false) let storeKitObserver = AsyncStream @@ -174,6 +177,7 @@ class SettingsPurchaseTests: XCTestCase { await task.cancel() } + @MainActor func testRestore_ErrorPath() async throws { let didRestoreCompletedTransactions = ActorIsolated(false) let storeKitObserver = AsyncStream From 9af2a17a45ec7c6be743f6f12133e56e0b0e9abc Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 9 Apr 2024 11:20:31 -0700 Subject: [PATCH 60/61] wip --- .github/workflows/ci.yml | 6 +++--- Makefile | 2 +- Tests/SettingsFeatureTests/SettingsFeatureTests.swift | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7418add7..038add23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,13 +16,13 @@ concurrency: jobs: mac: name: macOS - runs-on: macOS-13 + runs-on: macos-14 steps: - uses: actions/checkout@v4 # - name: Setup tmate session # uses: mxschmitt/action-tmate@v2 - - name: Select Xcode 15.2 - run: sudo xcode-select -s /Applications/Xcode_15.2.app + - name: Select Xcode 15.3 + run: sudo xcode-select -s /Applications/Xcode_15.3.app - name: LFS pull run: git lfs pull - name: Install Postgres diff --git a/Makefile b/Makefile index 4fd203fc..9c6be0ae 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ else @git lfs pull endif -PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS 17.2,iPhone \d\+ Pro [^M]) +PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS 17.4,iPhone \d\+ Pro [^M]) test-client: @xcodebuild test \ -project App/isowords.xcodeproj \ diff --git a/Tests/SettingsFeatureTests/SettingsFeatureTests.swift b/Tests/SettingsFeatureTests/SettingsFeatureTests.swift index 4ab0e40b..2a14e3f3 100644 --- a/Tests/SettingsFeatureTests/SettingsFeatureTests.swift +++ b/Tests/SettingsFeatureTests/SettingsFeatureTests.swift @@ -325,6 +325,7 @@ class SettingsFeatureTests: XCTestCase { await setMusicVolume.withValue { XCTAssertNoDifference($0, 0.5) } } + @MainActor func testSetSoundEffectsVolume() async { let setSoundEffectsVolume = ActorIsolated(nil) let store = TestStore( From f68c1cddf5646e3c28428e4102bb9d6677956a33 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 9 Apr 2024 11:47:01 -0700 Subject: [PATCH 61/61] wip --- Tests/ChangelogFeatureTests/ChangelogFeatureTests.swift | 3 ++- .../GameOverFeatureIntegrationTests.swift | 2 +- .../LeaderboardFeatureIntegrationTests.swift | 3 ++- .../LeaderboardFeatureTests.swift | 5 +++-- .../LeaderboardResultsTests.swift | 5 ++++- .../MultiplayerFeatureTests.swift | 4 +++- Tests/MultiplayerFeatureTests/PastGamesTests.swift | 5 ++++- .../UpgradeInterstitialFeatureTests.swift | 9 +++++++-- 8 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Tests/ChangelogFeatureTests/ChangelogFeatureTests.swift b/Tests/ChangelogFeatureTests/ChangelogFeatureTests.swift index 8f4d4357..7a73a314 100644 --- a/Tests/ChangelogFeatureTests/ChangelogFeatureTests.swift +++ b/Tests/ChangelogFeatureTests/ChangelogFeatureTests.swift @@ -6,8 +6,8 @@ import XCTest @testable import ChangelogFeature @testable import UserDefaultsClient -@MainActor class ChangelogFeatureTests: XCTestCase { + @MainActor func testOnAppear_IsUpToDate() async { let changelog = Changelog( changes: [ @@ -45,6 +45,7 @@ class ChangelogFeatureTests: XCTestCase { } } + @MainActor func testOnAppear_IsUpBehind() async { let changelog = Changelog( changes: [ diff --git a/Tests/GameOverFeatureIntegrationTests/GameOverFeatureIntegrationTests.swift b/Tests/GameOverFeatureIntegrationTests/GameOverFeatureIntegrationTests.swift index 7dbdf47e..daa62373 100644 --- a/Tests/GameOverFeatureIntegrationTests/GameOverFeatureIntegrationTests.swift +++ b/Tests/GameOverFeatureIntegrationTests/GameOverFeatureIntegrationTests.swift @@ -6,8 +6,8 @@ import SharedModels import SiteMiddleware import XCTest -@MainActor class GameOverFeatureIntegrationTests: XCTestCase { + @MainActor func testSubmitSoloScore() async { await withMainSerialExecutor { let ranks: [TimeScope: LeaderboardScoreResult.Rank] = [ diff --git a/Tests/LeaderboardFeatureTests/LeaderboardFeatureIntegrationTests.swift b/Tests/LeaderboardFeatureTests/LeaderboardFeatureIntegrationTests.swift index 8d4b2038..d1ec8ae0 100644 --- a/Tests/LeaderboardFeatureTests/LeaderboardFeatureIntegrationTests.swift +++ b/Tests/LeaderboardFeatureTests/LeaderboardFeatureIntegrationTests.swift @@ -9,8 +9,8 @@ import XCTest @testable import LeaderboardFeature -@MainActor class LeaderboardFeatureIntegrationTests: XCTestCase { + @MainActor func testSoloIntegrationWithLeaderboardResults() async { await withMainSerialExecutor { let fetchLeaderboardsEntries = [ @@ -59,6 +59,7 @@ class LeaderboardFeatureIntegrationTests: XCTestCase { } } + @MainActor func testVocabIntegrationWithLeaderboardResults() async { let fetchVocabEntries = [ FetchVocabLeaderboardResponse.Entry.init( diff --git a/Tests/LeaderboardFeatureTests/LeaderboardFeatureTests.swift b/Tests/LeaderboardFeatureTests/LeaderboardFeatureTests.swift index 5883ef6b..72dd966a 100644 --- a/Tests/LeaderboardFeatureTests/LeaderboardFeatureTests.swift +++ b/Tests/LeaderboardFeatureTests/LeaderboardFeatureTests.swift @@ -9,8 +9,8 @@ import XCTest @testable import LeaderboardFeature @testable import SharedModels -@MainActor class LeaderboardFeatureTests: XCTestCase { + @MainActor func testScopeSwitcher() async { let store = TestStore(initialState: Leaderboard.State()) { Leaderboard() @@ -24,6 +24,7 @@ class LeaderboardFeatureTests: XCTestCase { } } + @MainActor func testTimeScopeSynchronization() async { let store = TestStore(initialState: Leaderboard.State()) { Leaderboard() @@ -49,6 +50,7 @@ class LeaderboardFeatureTests: XCTestCase { await task2.cancel() } + @MainActor func testCubePreview() async { let wordId = Word.Id(rawValue: UUID(uuidString: "00000000-0000-0000-0000-00000000304d")!) let vocabEntry = FetchVocabLeaderboardResponse.Entry( @@ -104,7 +106,6 @@ class LeaderboardFeatureTests: XCTestCase { $0.mainQueue = .immediate } - await store.send(.vocab(.task)) { $0.vocab.isLoading = true $0.vocab.resultEnvelope = .placeholder diff --git a/Tests/LeaderboardFeatureTests/LeaderboardResultsTests.swift b/Tests/LeaderboardFeatureTests/LeaderboardResultsTests.swift index 803ed6b0..1e225d18 100644 --- a/Tests/LeaderboardFeatureTests/LeaderboardResultsTests.swift +++ b/Tests/LeaderboardFeatureTests/LeaderboardResultsTests.swift @@ -9,8 +9,8 @@ import XCTest @testable import LeaderboardFeature -@MainActor class LeaderboardTests: XCTestCase { + @MainActor func testOnAppear() async { let store = TestStore( initialState: LeaderboardResults.State(timeScope: TimeScope.lastWeek) @@ -28,6 +28,7 @@ class LeaderboardTests: XCTestCase { } } + @MainActor func testChangeGameMode() async { let store = TestStore( initialState: LeaderboardResults.State(timeScope: TimeScope.lastWeek) @@ -45,6 +46,7 @@ class LeaderboardTests: XCTestCase { } } + @MainActor func testChangeTimeScope() async { let store = TestStore( initialState: LeaderboardResults.State(timeScope: TimeScope.lastWeek) @@ -66,6 +68,7 @@ class LeaderboardTests: XCTestCase { } } + @MainActor func testUnhappyPath() async { let store = TestStore( initialState: LeaderboardResults.State(timeScope: .lastWeek) diff --git a/Tests/MultiplayerFeatureTests/MultiplayerFeatureTests.swift b/Tests/MultiplayerFeatureTests/MultiplayerFeatureTests.swift index 1035ef8b..70211771 100644 --- a/Tests/MultiplayerFeatureTests/MultiplayerFeatureTests.swift +++ b/Tests/MultiplayerFeatureTests/MultiplayerFeatureTests.swift @@ -3,8 +3,8 @@ import XCTest @testable import MultiplayerFeature -@MainActor class MultiplayerFeatureTests: XCTestCase { + @MainActor func testStartGame_GameCenterAuthenticated() async { let didPresentMatchmakerViewController = ActorIsolated(false) let store = TestStore(initialState: Multiplayer.State(hasPastGames: false)) { @@ -20,6 +20,7 @@ class MultiplayerFeatureTests: XCTestCase { await didPresentMatchmakerViewController.withValue { XCTAssertTrue($0) } } + @MainActor func testStartGame_GameCenterNotAuthenticated() async { let didPresentAuthentication = ActorIsolated(false) let store = TestStore( @@ -37,6 +38,7 @@ class MultiplayerFeatureTests: XCTestCase { await didPresentAuthentication.withValue { XCTAssertTrue($0) } } + @MainActor func testNavigateToPastGames() async { let store = TestStore( initialState: Multiplayer.State(hasPastGames: true) diff --git a/Tests/MultiplayerFeatureTests/PastGamesTests.swift b/Tests/MultiplayerFeatureTests/PastGamesTests.swift index 2f1d8d7d..55261d4c 100644 --- a/Tests/MultiplayerFeatureTests/PastGamesTests.swift +++ b/Tests/MultiplayerFeatureTests/PastGamesTests.swift @@ -8,8 +8,8 @@ import XCTest @testable import MultiplayerFeature -@MainActor class PastGamesTests: XCTestCase { + @MainActor func testLoadMatches() async { let store = TestStore(initialState: PastGames.State()) { PastGames() @@ -24,6 +24,7 @@ class PastGamesTests: XCTestCase { } } + @MainActor func testOpenMatch() async { let store = TestStore(initialState: PastGames.State(pastGames: [pastGameState])) { PastGames() @@ -36,6 +37,7 @@ class PastGamesTests: XCTestCase { await store.receive(\.pastGames[id: "id"].delegate.openMatch) } + @MainActor func testRematch() async { let store = TestStore(initialState: PastGames.State(pastGames: [pastGameState])) { PastGames() @@ -58,6 +60,7 @@ class PastGamesTests: XCTestCase { await store.receive(\.pastGames[id: "id"].delegate.openMatch) } + @MainActor func testRematch_Failure() async { struct RematchFailure: Error, Equatable {} diff --git a/Tests/UpgradeInterstitialFeatureTests/UpgradeInterstitialFeatureTests.swift b/Tests/UpgradeInterstitialFeatureTests/UpgradeInterstitialFeatureTests.swift index d69257ac..63a83f9a 100644 --- a/Tests/UpgradeInterstitialFeatureTests/UpgradeInterstitialFeatureTests.swift +++ b/Tests/UpgradeInterstitialFeatureTests/UpgradeInterstitialFeatureTests.swift @@ -10,10 +10,10 @@ import XCTest @testable import ServerConfigClient -@MainActor class UpgradeInterstitialFeatureTests: XCTestCase { let scheduler = RunLoop.test + @MainActor func testUpgrade() async { await withMainSerialExecutor { let dismissed = LockIsolated(false) @@ -47,7 +47,10 @@ class UpgradeInterstitialFeatureTests: XCTestCase { $0.dismiss = .init { dismissed.setValue(true) } $0.mainRunLoop = .immediate $0.serverConfig.config = { .init() } - $0.storeKit.addPayment = { await paymentAdded.setValue($0.productIdentifier) } + $0.storeKit.addPayment = { + let productIdentifier = $0.productIdentifier + await paymentAdded.setValue(productIdentifier) + } $0.storeKit.observer = { observer.stream } $0.storeKit.fetchProducts = { _ in .init( @@ -84,6 +87,7 @@ class UpgradeInterstitialFeatureTests: XCTestCase { } } + @MainActor func testWaitAndDismiss() async { let dismissed = LockIsolated(false) let store = TestStore( @@ -122,6 +126,7 @@ class UpgradeInterstitialFeatureTests: XCTestCase { XCTAssert(dismissed.value) } + @MainActor func testMaybeLater_Dismissable() async { let dismissed = LockIsolated(false) let store = TestStore(