diff --git a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d2cb4c1a..d0037ec4 100644 --- a/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/App/isowords.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "/~https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "revision" : "115fe5af41d333b6156d4924d7c7058bc77fd580", - "version" : "1.9.2" + "branch" : "shared-state-beta", + "revision" : "0293b055101b7283ddbe332c7945b424aca5eb72" } }, { diff --git a/Package.swift b/Package.swift index b9538df5..10ac090c 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.9.2"), + .package(url: "/~https://github.com/pointfreeco/swift-composable-architecture", branch: "shared-state-beta"), .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"), @@ -933,6 +933,7 @@ if ProcessInfo.processInfo.environment["TEST_SERVER"] == nil { name: "UserSettingsClient", dependencies: [ "Styleguide", + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "Dependencies", package: "swift-dependencies"), ] ), diff --git a/Sources/AppFeature/AppDelegate.swift b/Sources/AppFeature/AppDelegate.swift index 31061143..055107aa 100644 --- a/Sources/AppFeature/AppDelegate.swift +++ b/Sources/AppFeature/AppDelegate.swift @@ -23,7 +23,6 @@ public struct AppDelegateReducer { @Dependency(\.remoteNotifications.register) var registerForRemoteNotifications @Dependency(\.applicationClient.setUserInterfaceStyle) var setUserInterfaceStyle @Dependency(\.userNotifications) var userNotifications - @Dependency(\.userSettings) var userSettings public init() {} @@ -65,6 +64,7 @@ public struct AppDelegateReducer { } group.addTask { + @Shared(.userSettings) var userSettings await self.audioPlayer.setGlobalVolumeForSoundEffects(userSettings.soundEffectsVolume) await self.audioPlayer.setGlobalVolumeForMusic( self.audioPlayer.secondaryAudioShouldBeSilencedHint() diff --git a/Sources/AppFeature/GameCenterCore.swift b/Sources/AppFeature/GameCenterCore.swift index 056763cf..09817796 100644 --- a/Sources/AppFeature/GameCenterCore.swift +++ b/Sources/AppFeature/GameCenterCore.swift @@ -29,7 +29,7 @@ public struct GameCenterLogic { for await event in self.gameCenter.localPlayer.listener() { await send(.gameCenter(.listener(event))) } - } + } catch: { _, _ in } case .destination( .presented(.game(.destination(.presented(.gameOver(.rematchButtonTapped))))) diff --git a/Sources/ComposableGameCenter/LiveKey.swift b/Sources/ComposableGameCenter/LiveKey.swift index 9c4751e1..9c364ad4 100644 --- a/Sources/ComposableGameCenter/LiveKey.swift +++ b/Sources/ComposableGameCenter/LiveKey.swift @@ -10,7 +10,19 @@ localPlayer: .live, reportAchievements: { try await GKAchievement.report($0) }, showNotificationBanner: { - await GKNotificationBanner.show(withTitle: $0.title, message: $0.message) + if #available(iOS 16.1, *) { + var content = UNMutableNotificationContent() + content.title = $0.title ?? content.title + content.body = $0.message ?? content.body + let request = UNNotificationRequest( + identifier: UUID().uuidString, + content: content, + trigger: nil + ) + try? await UNUserNotificationCenter.current().add(request) + } else { + await GKNotificationBanner.show(withTitle: $0.title, message: $0.message) + } }, turnBasedMatch: .live, turnBasedMatchmakerViewController: .live diff --git a/Sources/CubePreview/CubePreviewView.swift b/Sources/CubePreview/CubePreviewView.swift index 07669e69..c38be651 100644 --- a/Sources/CubePreview/CubePreviewView.swift +++ b/Sources/CubePreview/CubePreviewView.swift @@ -14,13 +14,12 @@ public struct CubePreview { @ObservableState public struct State: Equatable { var cubes: Puzzle - var enableGyroMotion: Bool - var isAnimationReduced: Bool var isOnLowPowerMode: Bool var moveIndex: Int var moves: Moves var nub: CubeSceneView.ViewState.NubState var selectedCubeFaces: [IndexedCubeFace] + @Shared(.userSettings) var userSettings public init( cubes: ArchivablePuzzle, @@ -30,14 +29,9 @@ public struct CubePreview { nub: CubeSceneView.ViewState.NubState = .init(), selectedCubeFaces: [IndexedCubeFace] = [] ) { - @Dependency(\.userSettings) var userSettings - var cubes = Puzzle(archivableCubes: cubes) apply(moves: moves[0..: View where Content: View { ) .ignoresSafeArea() .transition( - store.isAnimationReduced + store.userSettings.enableReducedAnimation ? .opacity : .asymmetric(insertion: .offset(y: 50), removal: .offset(y: 50)) .combined(with: .opacity) @@ -126,7 +126,7 @@ public struct GameView: View where Content: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .background { - if !store.isAnimationReduced { + if !store.userSettings.enableReducedAnimation { BloomBackground( size: proxy.size, word: store.selectedWordString diff --git a/Sources/OnboardingFeature/OnboardingView.swift b/Sources/OnboardingFeature/OnboardingView.swift index b10ada14..82a437e6 100644 --- a/Sources/OnboardingFeature/OnboardingView.swift +++ b/Sources/OnboardingFeature/OnboardingView.swift @@ -20,6 +20,7 @@ public struct Onboarding { public var game: Game.State public var presentationStyle: PresentationStyle public var step: Step + @Shared(.userSettings) var userSettings public init( alert: AlertState? = nil, @@ -182,7 +183,6 @@ public struct Onboarding { @Dependency(\.feedbackGenerator) var feedbackGenerator @Dependency(\.mainQueue) var mainQueue @Dependency(\.userDefaults) var userDefaults - @Dependency(\.userSettings) var userSettings public init() {} @@ -380,7 +380,7 @@ public struct Onboarding { Scope(state: \.game, action: \.game) { Game() .haptics( - isEnabled: { _ in self.userSettings.enableHaptics }, + isEnabled: \.userSettings.enableHaptics, triggerOnChangeOf: \.selectedWord ) } diff --git a/Sources/SettingsFeature/Settings.swift b/Sources/SettingsFeature/Settings.swift index 35f727a0..5189d496 100644 --- a/Sources/SettingsFeature/Settings.swift +++ b/Sources/SettingsFeature/Settings.swift @@ -53,7 +53,7 @@ public struct Settings { public var isRestoring: Bool public var stats: Stats.State public var userNotificationSettings: UserNotificationClient.Notification.Settings? - public var userSettings: UserSettings + @Shared(.userSettings) public var userSettings public struct ProductError: Error, Equatable {} @@ -68,7 +68,6 @@ public struct Settings { stats: Stats.State = .init(), userNotificationSettings: UserNotificationClient.Notification.Settings? = nil ) { - @Dependency(\.userSettings) var userSettings self.alert = alert self.buildNumber = buildNumber self.developer = developer @@ -78,7 +77,6 @@ public struct Settings { self.isRestoring = isRestoring self.stats = stats self.userNotificationSettings = userNotificationSettings - self.userSettings = userSettings.get() } public var isFullGamePurchased: Bool { @@ -117,7 +115,6 @@ public struct Settings { @Dependency(\.serverConfig.config) var serverConfig @Dependency(\.storeKit) var storeKit @Dependency(\.userNotifications) var userNotifications - @Dependency(\.userSettings) var userSettings public init() {} @@ -456,14 +453,6 @@ public struct Settings { } } .ifLet(\.$alert, action: \.alert) - .onChange(of: \.userSettings) { _, userSettings in - Reduce { _, _ in - enum CancelID { case saveDebounce } - - return .run { _ in await self.userSettings.set(userSettings) } - .debounce(id: CancelID.saveDebounce, for: .seconds(0.5), scheduler: self.mainQueue) - } - } Scope(state: \.stats, action: \.stats) { Stats() diff --git a/Sources/SettingsFeature/SoundsSettingsView.swift b/Sources/SettingsFeature/SoundsSettingsView.swift index 350aa560..e79f51a8 100644 --- a/Sources/SettingsFeature/SoundsSettingsView.swift +++ b/Sources/SettingsFeature/SoundsSettingsView.swift @@ -53,18 +53,14 @@ struct SoundsSettingsView: View { struct SoundsSettingsView_Previews: PreviewProvider { static var previews: some View { - Preview { + @Shared(.userSettings) var userSettings + userSettings.musicVolume = 0.5 + userSettings.soundEffectsVolume = 0.5 + return Preview { NavigationView { SoundsSettingsView( store: Store(initialState: Settings.State()) { Settings() - } withDependencies: { - $0.userSettings = .mock( - initialUserSettings: UserSettings( - musicVolume: 0.5, - soundEffectsVolume: 0.5 - ) - ) } ) } diff --git a/Sources/UserSettingsClient/UserSettingsClient.swift b/Sources/UserSettingsClient/UserSettingsClient.swift index 096b249a..65bf6ed3 100644 --- a/Sources/UserSettingsClient/UserSettingsClient.swift +++ b/Sources/UserSettingsClient/UserSettingsClient.swift @@ -1,81 +1,95 @@ -import Combine -import Dependencies -import UIKit +import ComposableArchitecture -@dynamicMemberLookup -public struct UserSettingsClient { - public var get: @Sendable () -> UserSettings - public var set: @Sendable (UserSettings) async -> Void - public var stream: @Sendable () -> AsyncStream - - public subscript(dynamicMember keyPath: KeyPath) -> Value { - self.get()[keyPath: keyPath] - } - - @_disfavoredOverload - public subscript( - dynamicMember keyPath: KeyPath - ) -> AsyncStream { - // TODO: This should probably remove duplicates. - self.stream().map { $0[keyPath: keyPath] }.eraseToStream() - } - - public func modify(_ operation: (inout UserSettings) -> Void) async { - var userSettings = self.get() - operation(&userSettings) - await self.set(userSettings) +extension PersistenceKey where Self == PersistenceKeyDefault> { + public static var userSettings: Self { + PersistenceKeyDefault(.fileStorage(.userSettings), UserSettings()) } } -extension UserSettingsClient: DependencyKey { - public static var liveValue: UserSettingsClient { - let initialUserSettingsData = (try? Data(contentsOf: .userSettings)) ?? Data() - let initialUserSettings = - (try? JSONDecoder().decode(UserSettings.self, from: initialUserSettingsData)) - ?? UserSettings() - - let userSettings = LockIsolated(initialUserSettings) - let subject = PassthroughSubject() - return Self( - get: { - userSettings.value - }, - set: { updatedUserSettings in - userSettings.withValue { - $0 = updatedUserSettings - subject.send(updatedUserSettings) - try? JSONEncoder().encode(updatedUserSettings).write(to: .userSettings) - } - }, - stream: { - subject.values.eraseToStream() - } - ) - } - - public static let testValue = Self.mock() - - public static func mock(initialUserSettings: UserSettings = UserSettings()) -> Self { - let userSettings = LockIsolated(initialUserSettings) - let subject = PassthroughSubject() - return Self( - get: { userSettings.value }, - set: { updatedUserSettings in - userSettings.withValue { - $0 = updatedUserSettings - subject.send(updatedUserSettings) - } - }, - stream: { - subject.values.eraseToStream() - } - ) - } +struct State { + // @AppStorage("count") var count = 0 + // @AppStorage("count") var count = false + @Shared(.userSettings) var userSettings } -extension DependencyValues { - public var userSettings: UserSettingsClient { - get { self[UserSettingsClient.self] } - set { self[UserSettingsClient.self] = newValue } - } -} +//import Combine +//import Dependencies +//import UIKit +// +//@dynamicMemberLookup +//public struct UserSettingsClient { +// public var get: @Sendable () -> UserSettings +// public var set: @Sendable (UserSettings) async -> Void +// public var stream: @Sendable () -> AsyncStream +// +// public subscript(dynamicMember keyPath: KeyPath) -> Value { +// self.get()[keyPath: keyPath] +// } +// +// @_disfavoredOverload +// public subscript( +// dynamicMember keyPath: KeyPath +// ) -> AsyncStream { +// // TODO: This should probably remove duplicates. +// self.stream().map { $0[keyPath: keyPath] }.eraseToStream() +// } +// +// public func modify(_ operation: (inout UserSettings) -> Void) async { +// var userSettings = self.get() +// operation(&userSettings) +// await self.set(userSettings) +// } +//} +// +//extension UserSettingsClient: DependencyKey { +// public static var liveValue: UserSettingsClient { +// let initialUserSettingsData = (try? Data(contentsOf: .userSettings)) ?? Data() +// let initialUserSettings = +// (try? JSONDecoder().decode(UserSettings.self, from: initialUserSettingsData)) +// ?? UserSettings() +// +// let userSettings = LockIsolated(initialUserSettings) +// let subject = PassthroughSubject() +// return Self( +// get: { +// userSettings.value +// }, +// set: { updatedUserSettings in +// userSettings.withValue { +// $0 = updatedUserSettings +// subject.send(updatedUserSettings) +// try? JSONEncoder().encode(updatedUserSettings).write(to: .userSettings) +// } +// }, +// stream: { +// subject.values.eraseToStream() +// } +// ) +// } +// +// public static let testValue = Self.mock() +// +// public static func mock(initialUserSettings: UserSettings = UserSettings()) -> Self { +// let userSettings = LockIsolated(initialUserSettings) +// let subject = PassthroughSubject() +// return Self( +// get: { userSettings.value }, +// set: { updatedUserSettings in +// userSettings.withValue { +// $0 = updatedUserSettings +// subject.send(updatedUserSettings) +// } +// }, +// stream: { +// subject.values.eraseToStream() +// } +// ) +// } +//} +// +//extension DependencyValues { +// public var userSettings: UserSettingsClient { +// get { self[UserSettingsClient.self] } +// set { self[UserSettingsClient.self] = newValue } +// } +//}