diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 689039b8..038add23 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,11 +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.3
+ run: sudo xcode-select -s /Applications/Xcode_15.3.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
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/App/isowords.xcodeproj/project.pbxproj b/App/isowords.xcodeproj/project.pbxproj
index 94678a2d..9579f478 100644
--- a/App/isowords.xcodeproj/project.pbxproj
+++ b/App/isowords.xcodeproj/project.pbxproj
@@ -1339,7 +1339,6 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = TESTFLIGHT;
@@ -1825,7 +1824,6 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -1882,7 +1880,6 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- 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 cae75f33..d2cb4c1a 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" : "a5521dde99570789d8cb7c43e51418d7cd1a87ca",
- "version" : "1.1.2"
+ "revision" : "79623dbe2c7672f5e450d8325613d231454390b3",
+ "version" : "1.3.2"
}
},
{
@@ -104,8 +104,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
- "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307",
- "version" : "1.0.5"
+ "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb",
+ "version" : "1.1.0"
}
},
{
@@ -113,8 +113,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
"state" : {
- "revision" : "ae491c9e3f66631e72d58db8bb4c27dfc3d3afd4",
- "version" : "1.6.0"
+ "revision" : "115fe5af41d333b6156d4924d7c7058bc77fd580",
+ "version" : "1.9.2"
}
},
{
@@ -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" : "9783b58167f7618cb86011156e741cbc6f4cc864",
- "version" : "1.1.2"
+ "revision" : "d3a5af3038a09add4d7682f66555d6212058a3c0",
+ "version" : "1.2.2"
}
},
{
@@ -176,8 +176,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types",
"state" : {
- "revision" : "1827dc94bdab2eb5f2fc804e9b0cb43574282566",
- "version" : "1.0.2"
+ "revision" : "12358d55a3824bd5fed310b999ea8cf83a9a1a65",
+ "version" : "1.0.3"
}
},
{
@@ -194,8 +194,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
- "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed",
- "version" : "1.5.3"
+ "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5",
+ "version" : "1.5.4"
}
},
{
@@ -212,8 +212,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
- "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c",
- "version" : "2.62.0"
+ "revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982",
+ "version" : "2.64.0"
}
},
{
@@ -221,8 +221,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
- "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51",
- "version" : "1.20.0"
+ "revision" : "a3b640d7dc567225db7c94386a6e71aded1bfa63",
+ "version" : "1.22.0"
}
},
{
@@ -230,8 +230,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
- "revision" : "3bd9004b9d685ed6b629760fc84903e48efec806",
- "version" : "1.29.0"
+ "revision" : "0904bf0feb5122b7e5c3f15db7df0eabe623dd87",
+ "version" : "1.30.0"
}
},
{
@@ -239,8 +239,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
- "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9",
- "version" : "2.25.0"
+ "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5",
+ "version" : "2.26.0"
}
},
{
@@ -248,8 +248,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
- "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e",
- "version" : "1.20.0"
+ "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce",
+ "version" : "1.20.1"
}
},
{
@@ -279,6 +279,15 @@
"version" : "0.13.0"
}
},
+ {
+ "identity" : "swift-perception",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-perception",
+ "state" : {
+ "revision" : "520c458a832d1287e6b698c5f657ae848fd696ff",
+ "version" : "1.1.4"
+ }
+ },
{
"identity" : "swift-prelude",
"kind" : "remoteSourceControl",
@@ -293,8 +302,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
- "revision" : "59b663f68e69f27a87b45de48cb63264b8194605",
- "version" : "1.15.1"
+ "revision" : "625ccca8570773dd84a34ee51a81aa2bc5a4f97a",
+ "version" : "1.16.0"
}
},
{
@@ -302,8 +311,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax",
"state" : {
- "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036",
- "version" : "509.0.2"
+ "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd",
+ "version" : "510.0.1"
+ }
+ },
+ {
+ "identity" : "swift-system",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-system.git",
+ "state" : {
+ "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496",
+ "version" : "1.2.1"
}
},
{
@@ -347,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"
}
},
{
@@ -356,8 +374,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
- "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631",
- "version" : "1.0.2"
+ "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2",
+ "version" : "1.1.2"
}
}
],
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/Package.swift b/Package.swift
index 4d0dae36..b9538df5 100644
--- a/Package.swift
+++ b/Package.swift
@@ -28,7 +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.5.6"),
+ .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/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)
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/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 030deb0e..dcf417e4 100644
--- a/Sources/AppFeature/AppView.swift
+++ b/Sources/AppFeature/AppView.swift
@@ -12,35 +12,22 @@ import SwiftUI
@Reducer
public struct AppReducer {
- @Reducer
- public struct Destination {
- 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
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
@@ -279,6 +266,7 @@ public struct AppReducer {
)
async let refresh = self.refreshServerConfig()
_ = try await (register, refresh)
+ } catch: { _, _ in
}
case .didChangeScenePhase:
@@ -305,67 +293,55 @@ public struct AppReducer {
}
}
.ifLet(\.$destination, action: \.destination) {
- Destination()
+ Destination.body
}
}
}
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
+ )
+}
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/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 6e8715ae..88176955 100644
--- a/Sources/BottomMenu/ComposableBottomMenu.swift
+++ b/Sources/BottomMenu/ComposableBottomMenu.swift
@@ -70,44 +70,25 @@ extension BottomMenuState: _EphemeralState {
extension View {
public func bottomMenu(
- store: Store>, PresentationAction>
+ _ item: Binding, MenuAction>?>
) -> 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)
- }
- ) { 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)
- }
- )
- },
- 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
}
- )
+ }
)
- }
+ )
}
}
@@ -155,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 {
@@ -198,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/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())
}
}
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() }
}
}
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/CubePreview/CubePreviewView.swift b/Sources/CubePreview/CubePreviewView.swift
index c76e0f70..07669e69 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,48 +249,37 @@ 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(
- self.viewStore.isAnimationReduced
- ? nil
- : BloomBackground(
+ .background {
+ if !store.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: 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("")
}
}
-
-private func absurd(_: Never) -> A {}
diff --git a/Sources/DailyChallengeFeature/CalendarView.swift b/Sources/DailyChallengeFeature/CalendarView.swift
index 8615f189..6624ec3e 100644
--- a/Sources/DailyChallengeFeature/CalendarView.swift
+++ b/Sources/DailyChallengeFeature/CalendarView.swift
@@ -63,18 +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)
@@ -85,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))")
@@ -98,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
- )
+ }
+ }
}
}
}
@@ -112,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")
}
@@ -125,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
- )
+ }
+ }
}
}
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
diff --git a/Sources/DailyChallengeFeature/DailyChallengeView.swift b/Sources/DailyChallengeFeature/DailyChallengeView.swift
index 957b0e05..08602325 100644
--- a/Sources/DailyChallengeFeature/DailyChallengeView.swift
+++ b/Sources/DailyChallengeFeature/DailyChallengeView.swift
@@ -11,36 +11,19 @@ import SwiftUI
@Reducer
public struct DailyChallengeReducer {
- @Reducer
- public struct Destination {
- 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
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?
@@ -58,6 +41,11 @@ public struct DailyChallengeReducer {
self.inProgressDailyChallengeUnlimited = inProgressDailyChallengeUnlimited
self.userNotificationSettings = userNotificationSettings
}
+
+ var isNotificationStatusDetermined: Bool {
+ ![.notDetermined, .provisional]
+ .contains(self.userNotificationSettings?.authorizationStatus)
+ }
}
public enum Action {
@@ -206,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")
@@ -251,42 +239,31 @@ public struct DailyChallengeView: View {
@Environment(\.adaptiveSize) var adaptiveSize
@Environment(\.colorScheme) var colorScheme
@Environment(\.date) var date
- let 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
- }
+ @Bindable var store: StoreOf
- 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(self.store, observe: ViewState.init)
}
public var body: some View {
@@ -297,12 +274,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!"))
}
@@ -329,29 +306,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")
@@ -368,15 +345,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)
@@ -387,20 +364,22 @@ 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,
- action: { .notificationsAuthAlert($0) }
+ $store.scope(
+ state: \.destination?.notificationsAuthAlert,
+ action: \.destination.notificationsAuthAlert
+ )
)
}
}
-extension DailyChallengeView.ViewState.ButtonState {
+extension DailyChallengeView.ButtonState {
init(
fetchedResponse: FetchTodaysDailyChallengeResponse?,
inProgressGame: InProgressGame?
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)
}
diff --git a/Sources/GameCore/GameCore.swift b/Sources/GameCore/GameCore.swift
index b8bfa686..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,58 +21,35 @@ import UserSettingsClient
@Reducer
public struct Game {
- @Reducer
- public struct Destination {
- 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
}
}
+ @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
@@ -218,7 +196,8 @@ public struct Game {
}
.filterActionsForYourTurn()
.ifLet(\.$destination, action: \.destination) {
- Destination(dismissGame: self.dismiss)
+ Destination.body
+ .dependency(\.dismissGame, self.dismiss)
}
.sounds()
}
@@ -239,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))):
@@ -299,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))))):
@@ -433,12 +412,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 = [] }
@@ -568,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/GameCore/Views/GameFooterView.swift b/Sources/GameCore/Views/GameFooterView.swift
index 601e8e90..49de9ea8 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)
@@ -47,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,
@@ -63,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 {
@@ -78,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) {
@@ -116,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) {
+ .onChange(of: store.playedWords) {
guard self.isLeftToRight else { return }
withAnimation {
reader.scrollTo(SpacerId(), anchor: self.isLeftToRight ? .trailing : .leading)
@@ -140,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,
@@ -152,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
diff --git a/Sources/GameCore/Views/GameHeaderView.swift b/Sources/GameCore/Views/GameHeaderView.swift
index 026449d5..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 }
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)
diff --git a/Sources/GameCore/Views/GameView.swift b/Sources/GameCore/Views/GameView.swift
index 6a6b259e..7c33d2cc 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,40 +50,36 @@ 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))
+ : .asymmetric(insertion: .offset(y: 50), removal: .offset(y: 50))
.combined(with: .opacity)
)
}
ActiveGamesView(
- store: self.store.scope(state: \.activeGames, action: \.activeGames),
+ store: store.scope(state: \.activeGames, action: \.activeGames),
showMenuItems: false
)
.adaptivePadding(.vertical, 8)
@@ -121,76 +97,56 @@ 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
- ? nil
- : BloomBackground(
+ .background {
+ if !store.isAnimationReduced {
+ BloomBackground(
size: proxy.size,
- store: self.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()
)
- .bottomMenu(
- store: self.store.scope(state: \.$destination, action: \.destination),
- state: \.bottomMenu,
- action: { .bottomMenu($0) }
- )
- .alert(store: self.store.scope(state: \.$destination.alert, action: \.destination.alert))
+ .bottomMenu($store.scope(state: \.destination?.bottomMenu, action: \.destination.bottomMenu))
+ .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() }
}
}
-
-private func absurd(_: Never) -> A {}
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
diff --git a/Sources/GameCore/Views/WordSubmitButton.swift b/Sources/GameCore/Views/WordSubmitButton.swift
index 4a107947..457d4991 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
@@ -23,6 +24,7 @@ public struct WordSubmitButtonFeature {
}
}
+ @ObservableState
public struct ButtonState: Equatable {
public var areReactionsOpen: Bool
public var favoriteReactions: [Move.Reaction]
@@ -138,19 +140,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 +162,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 +180,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 +190,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,32 +205,25 @@ 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) }
}
}
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))
@@ -240,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
)
}
}
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 e1395972..a93559c5 100644
--- a/Sources/GameOverFeature/GameOverView.swift
+++ b/Sources/GameOverFeature/GameOverView.swift
@@ -17,32 +17,17 @@ import UserDefaultsClient
@Reducer
public struct GameOver {
- @Reducer
- public struct Destination {
- 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
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
@@ -52,6 +37,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] = [],
@@ -110,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
@@ -131,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)
}
}
@@ -150,7 +167,7 @@ public struct GameOver {
return .none
case .delayedShowUpgradeInterstitial:
- state.destination = .upgradeInterstitial()
+ state.destination = .upgradeInterstitial(UpgradeInterstitial.State())
return .none
case .delegate:
@@ -191,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:
@@ -259,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
}
@@ -341,7 +358,7 @@ public struct GameOver {
}
}
.ifLet(\.$destination, action: \.destination) {
- Destination()
+ Destination.body
}
}
@@ -371,76 +388,13 @@ public struct GameOverView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.opponentImage) var defaultOpponentImage
@Environment(\.yourImage) var defaultYourImage
- let store: StoreOf
- @ObservedObject var viewStore: ViewStore
+ @Bindable var store: StoreOf
@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 {
@@ -450,10 +404,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")
}
@@ -462,7 +416,7 @@ public struct GameOverView: View {
.font(.system(size: 24))
.adaptivePadding()
- switch self.viewStore.gameContext {
+ switch store.completedGame.gameContext {
case .dailyChallenge:
self.dailyChallengeResults
case .shared:
@@ -474,7 +428,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()
@@ -494,17 +448,15 @@ 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))
}
- 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)
}
@@ -514,22 +466,23 @@ 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(state: \.$destination, action: \.destination),
- state: \.notificationsAuthAlert,
- action: { .notificationsAuthAlert($0) }
+ $store.scope(
+ state: \.destination?.notificationsAuthAlert,
+ action: \.destination.notificationsAuthAlert
+ )
)
.sheet(isPresented: self.$isSharePresented) {
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 {
@@ -547,14 +500,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)) {
@@ -575,12 +525,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)
@@ -588,7 +538,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: [
@@ -601,22 +551,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)
@@ -628,9 +578,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)
@@ -651,7 +601,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 \
@@ -663,22 +613,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)
@@ -698,7 +645,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(
@@ -708,7 +655,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)
@@ -725,23 +672,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(
@@ -760,7 +704,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)
}
@@ -785,7 +729,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,
@@ -793,7 +737,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)
@@ -842,7 +786,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,
@@ -850,7 +794,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)
@@ -874,10 +818,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
}
}
@@ -885,7 +829,7 @@ public struct GameOverView: View {
}
var color: Color {
- switch self.viewStore.gameContext {
+ switch store.completedGame.gameContext {
case .dailyChallenge:
return .dailyChallenge
case .shared, .solo:
@@ -904,7 +848,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,
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..ad59735b 100644
--- a/Sources/HomeFeature/Home.swift
+++ b/Sources/HomeFeature/Home.swift
@@ -22,54 +22,23 @@ public struct ActiveMatchResponse: Equatable {
@Reducer
public struct Home {
- @Reducer
- public struct Destination {
- 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
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
@@ -163,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()
@@ -286,7 +255,7 @@ public struct Home {
return .none
case .leaderboardButtonTapped:
- state.destination = .leaderboard()
+ state.destination = .leaderboard(Leaderboard.State())
return .none
case .multiplayerButtonTapped:
@@ -297,7 +266,7 @@ public struct Home {
return .none
case .settingsButtonTapped:
- state.destination = .settings()
+ state.destination = .settings(Settings.State())
return .none
case .soloButtonTapped:
@@ -410,28 +379,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 +392,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 +414,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 +426,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 +451,7 @@ public struct HomeView: View {
)
)
- if self.viewStore.isNagBannerVisible {
+ if store.nagBanner != nil {
Spacer().frame(height: 80)
}
}
@@ -526,22 +475,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)
+ }
}
}
diff --git a/Sources/LeaderboardFeature/Leaderboard.swift b/Sources/LeaderboardFeature/Leaderboard.swift
index 40ec3d35..3eabf569 100644
--- a/Sources/LeaderboardFeature/Leaderboard.swift
+++ b/Sources/LeaderboardFeature/Leaderboard.swift
@@ -31,25 +31,14 @@ public enum LeaderboardScope: CaseIterable, Equatable {
@Reducer
public struct Leaderboard {
- @Reducer
- public struct Destination {
- 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
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)
@@ -141,7 +130,7 @@ public struct Leaderboard {
}
}
.ifLet(\.$destination, action: \.destination) {
- Destination()
+ Destination.body
}
Scope(state: \.solo, action: \.solo) {
@@ -155,12 +144,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 +155,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 +168,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 +191,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 +220,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)
+ }
}
}
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() }
}
}
diff --git a/Sources/MultiplayerFeature/MultiplayerView.swift b/Sources/MultiplayerFeature/MultiplayerView.swift
index 7134619f..a1b9c1d7 100644
--- a/Sources/MultiplayerFeature/MultiplayerView.swift
+++ b/Sources/MultiplayerFeature/MultiplayerView.swift
@@ -4,25 +4,14 @@ import TcaHelpers
@Reducer
public struct Multiplayer {
- @Reducer
- public struct Destination {
- 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
public struct State: Equatable {
- @PresentationState public var destination: Destination.State?
+ @Presents public var destination: Destination.State?
public var hasPastGames: Bool
public init(
@@ -65,7 +54,7 @@ public struct Multiplayer {
}
}
.ifLet(\.$destination, action: \.destination) {
- Destination()
+ Destination.body
}
}
}
@@ -73,20 +62,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 +90,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 +105,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 +128,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/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
+ )
}
}
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,
diff --git a/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift b/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift
index ef017fd8..acd8dfc8 100644
--- a/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift
+++ b/Sources/NotificationsAuthAlert/NotificationsAuthAlert.swift
@@ -56,98 +56,81 @@ 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: Binding?>
) -> 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 {
+ @Binding var store: Store?
func body(content: Content) -> some View {
- WithViewStore(
- self.store, observe: { $0.wrappedValue.flatMap(self.toAlertState) }
- ) { 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(fromAlertAction($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: {})
}
}
diff --git a/Sources/OnboardingFeature/OnboardingStepView.swift b/Sources/OnboardingFeature/OnboardingStepView.swift
index 0b994776..8e4770a3 100644
--- a/Sources/OnboardingFeature/OnboardingStepView.swift
+++ b/Sources/OnboardingFeature/OnboardingStepView.swift
@@ -3,229 +3,166 @@ import Styleguide
import SwiftUI
struct OnboardingStepView: View {
- let store: StoreOf
- @ObservedObject var viewStore: ViewStore
+ @Bindable var store: StoreOf
@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 {
GeometryReader { proxy in
let height = proxy.size.height / 4
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 +184,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 +214,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,18 +244,18 @@ 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
)
)
}
}
.padding()
.transition(
- AnyTransition.asymmetric(
+ .asymmetric(
insertion: .offset(x: 0, y: 50),
removal: .offset(x: 0, y: 50)
)
@@ -326,8 +263,8 @@ struct OnboardingStepView: View {
)
}
}
- .task { await self.viewStore.send(.task).finish() }
- .alert(store: self.store.scope(state: \.$alert, action: \.alert))
+ .task { await store.send(.task).finish() }
+ .alert($store.scope(state: \.alert, action: \.alert))
}
}
@@ -362,6 +299,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)
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)) {
-
}
)
}
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/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
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/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)
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)
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,
diff --git a/Sources/StatsFeature/StatsFeature.swift b/Sources/StatsFeature/StatsFeature.swift
index e9a2c08c..1c9e1fa1 100644
--- a/Sources/StatsFeature/StatsFeature.swift
+++ b/Sources/StatsFeature/StatsFeature.swift
@@ -6,26 +6,15 @@ import VocabFeature
@Reducer
public struct Stats {
- @Reducer
- public struct Destination {
- 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
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?
@@ -111,18 +100,16 @@ public struct Stats {
}
}
.ifLet(\.$destination, action: \.destination) {
- Destination()
+ Destination.body
}
}
}
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 +118,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 +134,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 +147,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 +163,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 +180,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 +205,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"))
}
}
diff --git a/Sources/TrailerFeature/Trailer.swift b/Sources/TrailerFeature/Trailer.swift
index b376900b..f09a2a0c 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,46 +242,39 @@ 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)
)
}
- .background(
+ .background {
BloomBackground(
size: proxy.size,
- store: self.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 : [],
.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: "")
+ }
}
}
@@ -319,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 {}
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 }
)
}
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()
+ )
}
}
}
diff --git a/Sources/VocabFeature/Vocab.swift b/Sources/VocabFeature/Vocab.swift
index c34742da..fa9a9cc7 100644
--- a/Sources/VocabFeature/Vocab.swift
+++ b/Sources/VocabFeature/Vocab.swift
@@ -5,25 +5,14 @@ import SwiftUI
@Reducer
public struct Vocab: Reducer {
- @Reducer
- public struct Destination: Reducer {
- 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
public struct State: Equatable {
- @PresentationState var destination: Destination.State?
+ @Presents var destination: Destination.State?
var isAnimationReduced: Bool
var vocab: LocalDatabaseClient.Vocab?
@@ -116,13 +105,13 @@ public struct Vocab: Reducer {
}
}
.ifLet(\.$destination, action: \.destination) {
- Destination()
+ Destination.body
}
}
}
public struct VocabView: View {
- public let store: StoreOf
+ @Bindable var store: StoreOf
public init(store: StoreOf) {
self.store = store
@@ -130,42 +119,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"))
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/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/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/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/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/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/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 d4411232..2a14e3f3 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)
@@ -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
}
@@ -108,6 +108,7 @@ class SettingsFeatureTests: XCTestCase {
await task.cancel()
}
+ @MainActor
func testEnableNotifications_NotDetermined_DenyAuthorization() async {
let store = TestStore(
initialState: Settings.State()
@@ -138,7 +139,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
}
@@ -149,6 +150,7 @@ class SettingsFeatureTests: XCTestCase {
await task.cancel()
}
+ @MainActor
func testNotifications_PreviouslyGranted() async {
let store = TestStore(
initialState: Settings.State()
@@ -179,13 +181,14 @@ 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
}
await task.cancel()
}
+ @MainActor
func testNotifications_PreviouslyDenied() async {
let openedUrl = ActorIsolated(nil)
let store = TestStore(
@@ -223,7 +226,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
}
@@ -238,6 +241,7 @@ class SettingsFeatureTests: XCTestCase {
await task.cancel()
}
+ @MainActor
func testNotifications_RemoteSettingsUpdates() async {
var userSettings = UserSettings(sendDailyChallengeReminder: false)
let didUpdate = LockIsolated(false)
@@ -288,7 +292,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
}
@@ -300,6 +304,7 @@ class SettingsFeatureTests: XCTestCase {
// MARK: - Sounds
+ @MainActor
func testSetMusicVolume() async {
let setMusicVolume = ActorIsolated(nil)
let store = TestStore(
@@ -313,13 +318,14 @@ 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
}
await setMusicVolume.withValue { XCTAssertNoDifference($0, 0.5) }
}
+ @MainActor
func testSetSoundEffectsVolume() async {
let setSoundEffectsVolume = ActorIsolated(nil)
let store = TestStore(
@@ -335,7 +341,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
}
@@ -344,6 +350,7 @@ class SettingsFeatureTests: XCTestCase {
// MARK: - Appearance
+ @MainActor
func testSetColorScheme() async {
let overriddenUserInterfaceStyle = ActorIsolated(nil)
let store = TestStore(
@@ -359,18 +366,19 @@ 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) }
}
+ @MainActor
func testSetAppIcon() async {
let overriddenIconName = ActorIsolated(nil)
let store = TestStore(
@@ -386,12 +394,13 @@ 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") }
}
+ @MainActor
func testUnsetAppIcon() async {
let overriddenIconName = ActorIsolated(nil)
let store = TestStore(
@@ -421,7 +430,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) }
@@ -431,6 +440,7 @@ class SettingsFeatureTests: XCTestCase {
// MARK: - Developer
+ @MainActor
func testSetApiBaseUrl() async {
let setBaseUrl = ActorIsolated(nil)
let didLogout = ActorIsolated(false)
@@ -446,13 +456,14 @@ 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")!) }
await didLogout.withValue { XCTAssert($0) }
}
+ @MainActor
func testToggleEnableGyroMotion() async {
let store = TestStore(
initialState: Settings.State()
@@ -465,15 +476,16 @@ 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
}
}
+ @MainActor
func testToggleEnableHaptics() async {
let store = TestStore(
initialState: Settings.State()
@@ -487,11 +499,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
}
}
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
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(