diff --git a/.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved b/.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1b27355aecfd..e22e4e42663d 100644 --- a/.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,24 +9,6 @@ "version" : "1.0.2" } }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", - "state" : { - "revision" : "41982a3656a71c768319979febd796c6fd111d5c", - "version" : "1.5.0" - } - }, - { - "identity" : "swift-benchmark", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/swift-benchmark", - "state" : { - "revision" : "8163295f6fe82356b0bcf8e1ab991645de17d096", - "version" : "0.1.2" - } - }, { "identity" : "swift-case-paths", "kind" : "remoteSourceControl", @@ -77,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "fd1fb25b68fdb9756cd61d23dbd9e2614b340085", - "version" : "1.4.0" + "revision" : "7d2eb4ad20efb2838269645410d26b64ca48d8aa", + "version" : "1.6.1" } }, { @@ -122,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "e834b3760731160d7d448509ee6a1408c8582a6b", - "version" : "2.2.0" + "revision" : "16a27ab7ae0abfefbbcba73581b3e2380b47a579", + "version" : "2.2.2" } }, { @@ -131,8 +113,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "bc67aa8e461351c97282c2419153757a446ae1c9", - "version" : "1.3.5" + "revision" : "8d52279b9809ef27eabe7d5420f03734528f19da", + "version" : "1.4.1" + } + }, + { + "identity" : "swift-sharing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-sharing", + "state" : { + "revision" : "f923992fc911368447074a56da13feadf87f07a0", + "version" : "1.0.2" } }, { @@ -158,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "bc2a151366f2cd0e347274544933bc2acb00c9fe", - "version" : "1.4.0" + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" } } ], diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 33ea83a65a95..000000000000 --- a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,159 +0,0 @@ -{ - "originHash" : "345ca5e011bfdb9a07d4b2a72a36fac771eb8263e2cd8901042f6e807a599841", - "pins" : [ - { - "identity" : "combine-schedulers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/combine-schedulers", - "state" : { - "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", - "version" : "1.0.2" - } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "bc92c4b27f9a84bfb498cdbfdf35d5a357e9161f", - "version" : "1.5.6" - } - }, - { - "identity" : "swift-clocks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-clocks", - "state" : { - "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", - "version" : "1.0.5" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", - "version" : "1.1.4" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "6054df64b55186f08b6d0fd87152081b8ad8d613", - "version" : "1.2.0" - } - }, - { - "identity" : "swift-custom-dump", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", - "state" : { - "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", - "version" : "1.3.3" - } - }, - { - "identity" : "swift-dependencies", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", - "state" : { - "revision" : "0fc0255e780bf742abeef29dec80924f5f0ae7b9", - "version" : "1.4.1" - } - }, - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-plugin", - "state" : { - "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", - "version" : "1.4.3" - } - }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-identified-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-identified-collections", - "state" : { - "revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-macro-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-macro-testing", - "state" : { - "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4", - "version" : "0.5.2" - } - }, - { - "identity" : "swift-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-navigation", - "state" : { - "revision" : "16a27ab7ae0abfefbbcba73581b3e2380b47a579", - "version" : "2.2.2" - } - }, - { - "identity" : "swift-perception", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-perception", - "state" : { - "revision" : "bc67aa8e461351c97282c2419153757a446ae1c9", - "version" : "1.3.5" - } - }, - { - "identity" : "swift-snapshot-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", - "state" : { - "revision" : "7b0bbbae90c41f848f90ac7b4df6c4f50068256d", - "version" : "1.17.5" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax", - "state" : { - "revision" : "0687f71944021d616d34d922343dcef086855920", - "version" : "600.0.1" - } - }, - { - "identity" : "swift-tagged", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-tagged.git", - "state" : { - "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", - "version" : "0.10.0" - } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "770f990d3e4eececb57ac04a6076e22f8c97daeb", - "version" : "1.4.2" - } - } - ], - "version" : 3 -} diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj index 44e0c92e25a7..f574e93e9cce 100644 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj @@ -31,7 +31,6 @@ CAA9ADCC2446615B0003A984 /* 03-Effects-LongLivingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADCB2446615B0003A984 /* 03-Effects-LongLivingTests.swift */; }; CABC4F3926AEE00C00D5FA2C /* 03-Effects-Refreshable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABC4F3826AEE00C00D5FA2C /* 03-Effects-Refreshable.swift */; }; CABC4F3B26AEE20200D5FA2C /* 03-Effects-RefreshableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABC4F3A26AEE20200D5FA2C /* 03-Effects-RefreshableTests.swift */; }; - CACA7FBC2BC707F2002DF110 /* 02-SharedState-Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = CACA7FBB2BC707F2002DF110 /* 02-SharedState-Notifications.swift */; }; CADECDB62B5CA228009DC881 /* 02-SharedState-InMemory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CADECDB52B5CA228009DC881 /* 02-SharedState-InMemory.swift */; }; CADECDB82B5CA425009DC881 /* 02-SharedState-FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CADECDB72B5CA425009DC881 /* 02-SharedState-FileStorage.swift */; }; CADECDBA2B5CA613009DC881 /* 02-GettingStarted-SharedStateUserDefaultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CADECDB92B5CA613009DC881 /* 02-GettingStarted-SharedStateUserDefaultsTests.swift */; }; @@ -176,7 +175,6 @@ CAA9ADCB2446615B0003A984 /* 03-Effects-LongLivingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Effects-LongLivingTests.swift"; sourceTree = ""; }; CABC4F3826AEE00C00D5FA2C /* 03-Effects-Refreshable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Effects-Refreshable.swift"; sourceTree = ""; }; CABC4F3A26AEE20200D5FA2C /* 03-Effects-RefreshableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Effects-RefreshableTests.swift"; sourceTree = ""; }; - CACA7FBB2BC707F2002DF110 /* 02-SharedState-Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-SharedState-Notifications.swift"; sourceTree = ""; }; CADECDB52B5CA228009DC881 /* 02-SharedState-InMemory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-SharedState-InMemory.swift"; sourceTree = ""; }; CADECDB72B5CA425009DC881 /* 02-SharedState-FileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-SharedState-FileStorage.swift"; sourceTree = ""; }; CADECDB92B5CA613009DC881 /* 02-GettingStarted-SharedStateUserDefaultsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-GettingStarted-SharedStateUserDefaultsTests.swift"; sourceTree = ""; }; @@ -405,7 +403,6 @@ DCC68EDC2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift */, CADECDB72B5CA425009DC881 /* 02-SharedState-FileStorage.swift */, CADECDB52B5CA228009DC881 /* 02-SharedState-InMemory.swift */, - CACA7FBB2BC707F2002DF110 /* 02-SharedState-Notifications.swift */, CADECDBF2B5DE7C1009DC881 /* 02-SharedState-Onboarding.swift */, CA7BC8ED245CCFE4001FB69F /* 02-SharedState-UserDefaults.swift */, CAA9ADC12446587C0003A984 /* 03-Effects-Basics.swift */, @@ -748,7 +745,6 @@ DCC68EE12447C4630037F998 /* 01-GettingStarted-Composition-TwoCounters.swift in Sources */, DC072322244663B1003A8B65 /* 04-Navigation-Sheet-LoadThenPresent.swift in Sources */, DC89C45324465452006900B9 /* 04-Navigation-Lists-NavigateAndLoad.swift in Sources */, - CACA7FBC2BC707F2002DF110 /* 02-SharedState-Notifications.swift in Sources */, DCC68EE32447C8540037F998 /* 05-HigherOrderReducers-ReusableFavoriting.swift in Sources */, CA3E421F26B8337500581ABC /* 01-GettingStarted-FocusState.swift in Sources */, DCC68EDF2447BC810037F998 /* TemplateText.swift in Sources */, diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift index eef92539acf8..758a091f6883 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift @@ -83,15 +83,6 @@ struct RootView: View { SharedStateFileStorageView(store: store) } } - NavigationLink("Notifications") { - Demo( - store: Store(initialState: SharedStateNotifications.State()) { - SharedStateNotifications() - } - ) { store in - SharedStateNotificationsView(store: store) - } - } Button("Sign up flow") { isSignUpCaseStudyPresented = true } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-FileStorage.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-FileStorage.swift index 0c8da379d597..1edeca499bdc 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-FileStorage.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-FileStorage.swift @@ -99,11 +99,11 @@ extension SharedStateFileStorage { return .none case .decrementButtonTapped: - state.stats.decrement() + state.$stats.withLock { $0.decrement() } return .none case .incrementButtonTapped: - state.stats.increment() + state.$stats.withLock { $0.increment() } return .none case .isPrimeButtonTapped: @@ -136,7 +136,7 @@ extension SharedStateFileStorage { Reduce { state, action in switch action { case .resetStatsButtonTapped: - state.stats = Stats() + state.$stats.withLock { $0 = Stats() } return .none } } @@ -223,7 +223,7 @@ struct Stats: Codable, Equatable { } } -extension PersistenceReaderKey where Self == FileStorageKey { +extension SharedKey where Self == FileStorageKey { fileprivate static var stats: Self { fileStorage(.documentsDirectory.appending(component: "stats.json")) } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-InMemory.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-InMemory.swift index 2f531146e532..9b80dc909ed9 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-InMemory.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-InMemory.swift @@ -98,11 +98,11 @@ extension SharedStateInMemory { return .none case .decrementButtonTapped: - state.stats.decrement() + state.$stats.withLock { $0.decrement() } return .none case .incrementButtonTapped: - state.stats.increment() + state.$stats.withLock { $0.increment() } return .none case .isPrimeButtonTapped: @@ -135,7 +135,7 @@ extension SharedStateInMemory { Reduce { state, action in switch action { case .resetStatsButtonTapped: - state.stats = Stats() + state.$stats.withLock { $0 = Stats() } return .none } } @@ -211,7 +211,7 @@ private struct ProfileTabView: View { ) } -extension PersistenceReaderKey where Self == InMemoryKey { +extension SharedKey where Self == InMemoryKey { fileprivate static var stats: Self { inMemory("stats") } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-Notifications.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-Notifications.swift deleted file mode 100644 index 79cc444fd2b1..000000000000 --- a/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-Notifications.swift +++ /dev/null @@ -1,132 +0,0 @@ -import ComposableArchitecture -import SwiftUI - -private let readMe = """ - This application demonstrates how to use the `@SharedReader` tool to introduce a piece of \ - read-only state to your feature whose true value lives in an external system. In this case, \ - the state is the number of times a screenshot is taken, which is counted from the \ - `userDidTakeScreenshotNotification` notification. - - Run this application in the simulator, and take a few screenshots by going to \ - *Device › Trigger Screenshot* in the menu, and observe that the UI counts the number of times \ - that happens. - - The `@SharedReader` state will update automatically when the screenshot notification is posted \ - by the system, and further you can use the `.publisher` property on `@SharedReader` to listen \ - for any changes to the data. - """ - -@Reducer -struct SharedStateNotifications { - @ObservableState - struct State: Equatable { - var fact: String? - @SharedReader(.screenshotCount) var screenshotCount = 0 - } - enum Action { - case factResponse(Result) - case onAppear - } - @Dependency(\.factClient) var factClient - var body: some ReducerOf { - Reduce { state, action in - switch action { - case let .factResponse(.success(fact)): - state.fact = fact - return .none - - case .factResponse(.failure): - return .none - - case .onAppear: - return .run { [screenshotCount = state.$screenshotCount] send in - for await count in screenshotCount.publisher.values { - await send(.factResponse(Result { try await factClient.fetch(count) })) - } - } - } - } - } -} - -struct SharedStateNotificationsView: View { - let store: StoreOf - - var body: some View { - Form { - Section { - AboutView(readMe: readMe) - } - - Text("A screenshot of this screen has been taken \(store.screenshotCount) times.") - .font(.headline) - - if let fact = store.fact { - Text("\(fact)") - } - } - .navigationTitle("Long-living effects") - .task { await store.send(.onAppear).finish() } - } -} - -extension PersistenceReaderKey where Self == NotificationReaderKey { - static var screenshotCount: Self { - NotificationReaderKey( - initialValue: 0, - name: MainActor.assumeIsolated { - UIApplication.userDidTakeScreenshotNotification - } - ) { value, _ in - value += 1 - } - } -} - -struct NotificationReaderKey: PersistenceReaderKey { - let name: Notification.Name - private let transform: @Sendable (Notification) -> Value - - init( - initialValue: Value, - name: Notification.Name, - transform: @Sendable @escaping (inout Value, Notification) -> Void - ) { - self.name = name - let value = LockIsolated(initialValue) - self.transform = { notification in - value.withValue { [notification = UncheckedSendable(notification)] in - transform(&$0, notification.wrappedValue) - } - return value.value - } - } - - var id: some Hashable { self.name } - - func load(initialValue: Value?) -> Value? { nil } - - func subscribe( - initialValue: Value?, - didSet: @Sendable @escaping (Value?) -> Void - ) -> Shared.Subscription { - let token = NotificationCenter.default.addObserver( - forName: name, - object: nil, - queue: nil, - using: { notification in - didSet(transform(notification)) - } - ) - return Shared.Subscription { - NotificationCenter.default.removeObserver(token) - } - } - - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.name == rhs.name - } - func hash(into hasher: inout Hasher) { - hasher.combine(name) - } -} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-Onboarding.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-Onboarding.swift index a1f7ea5c3c82..1063389a3923 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-Onboarding.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-Onboarding.swift @@ -63,7 +63,7 @@ private struct SignUpFeature { struct SignUpFlow: View { @Bindable private var store = Store( - initialState: SignUpFeature.State(signUpData: Shared(SignUpData())) + initialState: SignUpFeature.State(signUpData: Shared(value: SignUpData())) ) { SignUpFeature() } @@ -441,7 +441,7 @@ private struct SummaryStep: View { #Preview("Basics") { NavigationStack { BasicsStep( - store: Store(initialState: BasicsFeature.State(signUpData: Shared(SignUpData()))) { + store: Store(initialState: BasicsFeature.State(signUpData: Shared(value: SignUpData()))) { BasicsFeature() } ) @@ -451,7 +451,7 @@ private struct SummaryStep: View { #Preview("Personal info") { NavigationStack { PersonalInfoStep( - store: Store(initialState: PersonalInfoFeature.State(signUpData: Shared(SignUpData()))) { + store: Store(initialState: PersonalInfoFeature.State(signUpData: Shared(value: SignUpData()))) { PersonalInfoFeature() } ) @@ -461,7 +461,7 @@ private struct SummaryStep: View { #Preview("Topics") { NavigationStack { TopicsStep( - store: Store(initialState: TopicsFeature.State(topics: Shared([]))) { + store: Store(initialState: TopicsFeature.State(topics: Shared(value: []))) { TopicsFeature() } ) @@ -474,7 +474,7 @@ private struct SummaryStep: View { store: Store( initialState: SummaryFeature.State( signUpData: Shared( - SignUpData( + value: SignUpData( email: "blob@pointfree.co", firstName: "Blob", lastName: "McBlob", diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-UserDefaults.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-UserDefaults.swift index a6370aeaa911..5a8e9b3a0a3a 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-UserDefaults.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-UserDefaults.swift @@ -95,11 +95,11 @@ extension SharedStateUserDefaults { return .none case .decrementButtonTapped: - state.count -= 1 + state.$count.withLock { $0 -= 1 } return .none case .incrementButtonTapped: - state.count += 1 + state.$count.withLock { $0 += 1 } return .none case .isPrimeButtonTapped: @@ -132,7 +132,7 @@ extension SharedStateUserDefaults { Reduce { state, action in switch action { case .resetStatsButtonTapped: - state.count = 0 + state.$count.withLock { $0 = 0 } return .none } } @@ -199,7 +199,7 @@ private struct ProfileTabView: View { } } -extension PersistenceReaderKey where Self == AppStorageKey { +extension SharedKey where Self == AppStorageKey { fileprivate static var count: Self { appStorage("sharedStateDemoCount") } diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateFileStorageTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateFileStorageTests.swift index 20846bcfc0b5..556918cbfbcc 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateFileStorageTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateFileStorageTests.swift @@ -26,15 +26,15 @@ struct SharedStateFileStorageTests { } await store.send(\.counter.incrementButtonTapped) { - $0.counter.stats.increment() + $0.counter.$stats.withLock { $0.increment() } } await store.send(\.counter.decrementButtonTapped) { - $0.counter.stats.decrement() + $0.counter.$stats.withLock { $0.decrement() } } await store.send(\.profile.resetStatsButtonTapped) { - $0.profile.stats = Stats() + $0.profile.$stats.withLock { $0 = Stats() } } } diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateInMemoryTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateInMemoryTests.swift index 557e1c712f5f..0bbadbd1f39b 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateInMemoryTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateInMemoryTests.swift @@ -26,15 +26,15 @@ struct SharedStateInMemoryTests { } await store.send(.counter(.incrementButtonTapped)) { - $0.counter.stats.increment() + $0.counter.$stats.withLock { $0.increment() } } await store.send(.counter(.decrementButtonTapped)) { - $0.counter.stats.decrement() + $0.counter.$stats.withLock { $0.decrement() } } await store.send(.profile(.resetStatsButtonTapped)) { - $0.profile.stats = Stats() + $0.profile.$stats.withLock { $0 = Stats() } } } diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateUserDefaultsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateUserDefaultsTests.swift index 2c41b1a6d827..21e7a04b4e7b 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateUserDefaultsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/02-GettingStarted-SharedStateUserDefaultsTests.swift @@ -26,15 +26,15 @@ struct SharedStateUserDefaultsTests { } await store.send(.counter(.incrementButtonTapped)) { - $0.counter.count = 1 + $0.counter.$count.withLock { $0 = 1 } } await store.send(.counter(.decrementButtonTapped)) { - $0.counter.count = 0 + $0.counter.$count.withLock { $0 = 0 } } await store.send(.profile(.resetStatsButtonTapped)) { - $0.profile.count = 0 + $0.profile.$count.withLock { $0 = 0 } } } diff --git a/Examples/SyncUps/SyncUps/RecordMeeting.swift b/Examples/SyncUps/SyncUps/RecordMeeting.swift index c35770063ee4..63a6adbf5e91 100644 --- a/Examples/SyncUps/SyncUps/RecordMeeting.swift +++ b/Examples/SyncUps/SyncUps/RecordMeeting.swift @@ -44,7 +44,7 @@ struct RecordMeeting { return .run { _ in await dismiss() } case .alert(.presented(.confirmSave)): - state.syncUp.insert(transcript: state.transcript) + state.$syncUp.withLock { $0.insert(transcript: state.transcript) } return .run { _ in await dismiss() } case .alert: @@ -93,7 +93,7 @@ struct RecordMeeting { let secondsPerAttendee = Int(state.syncUp.durationPerAttendee.components.seconds) if state.secondsElapsed.isMultiple(of: secondsPerAttendee) { if state.secondsElapsed == state.syncUp.duration.components.seconds { - state.syncUp.insert(transcript: state.transcript) + state.$syncUp.withLock { $0.insert(transcript: state.transcript) } return .run { _ in await dismiss() } } state.speakerIndex += 1 @@ -385,7 +385,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Examples/SyncUps/SyncUps/SyncUpDetail.swift b/Examples/SyncUps/SyncUps/SyncUpDetail.swift index 53e9c98cd034..87c12b739b74 100644 --- a/Examples/SyncUps/SyncUps/SyncUpDetail.swift +++ b/Examples/SyncUps/SyncUps/SyncUpDetail.swift @@ -57,14 +57,14 @@ struct SyncUpDetail { return .none case let .deleteMeetings(atOffsets: indices): - state.syncUp.meetings.remove(atOffsets: indices) + state.$syncUp.withLock { $0.meetings.remove(atOffsets: indices) } return .none case let .destination(.presented(.alert(alertAction))): switch alertAction { case .confirmDeletion: @Shared(.syncUps) var syncUps - syncUps.remove(id: state.syncUp.id) + $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) } return .run { _ in await dismiss() } case .continueWithoutRecording: @@ -80,7 +80,7 @@ struct SyncUpDetail { case .doneEditingButtonTapped: guard case let .some(.edit(editState)) = state.destination else { return .none } - state.syncUp = editState.syncUp + state.$syncUp.withLock { $0 = editState.syncUp } state.destination = nil return .none @@ -266,7 +266,7 @@ extension AlertState where Action == SyncUpDetail.Destination.Alert { #Preview { NavigationStack { SyncUpDetailView( - store: Store(initialState: SyncUpDetail.State(syncUp: Shared(.mock))) { + store: Store(initialState: SyncUpDetail.State(syncUp: Shared(value: .mock))) { SyncUpDetail() } ) diff --git a/Examples/SyncUps/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUps/SyncUpsList.swift index dfe399440265..b7bd011c72c5 100644 --- a/Examples/SyncUps/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUps/SyncUpsList.swift @@ -54,7 +54,7 @@ struct SyncUpsList { ?? Attendee(id: Attendee.ID(uuid())) ) } - state.syncUps.append(syncUp) + state.$syncUps.withLock { $0.append(syncUp) } state.destination = nil return .none @@ -66,7 +66,7 @@ struct SyncUpsList { return .none case let .onDelete(indexSet): - state.syncUps.remove(atOffsets: indexSet) + state.$syncUps.withLock { $0.remove(atOffsets: indexSet) } return .none } } @@ -80,7 +80,7 @@ struct SyncUpsListView: View { var body: some View { List { - ForEach(store.$syncUps.elements) { $syncUp in + ForEach(store.$syncUps) { $syncUp in NavigationLink(state: AppFeature.Path.State.detail(SyncUpDetail.State(syncUp: $syncUp))) { CardView(syncUp: syncUp) } @@ -180,12 +180,8 @@ extension LabelStyle where Self == TrailingIconLabelStyle { ) } -extension PersistenceReaderKey -where Self == PersistenceKeyDefault>> { +extension SharedKey where Self == FileStorageKey>.Default { static var syncUps: Self { - PersistenceKeyDefault( - .fileStorage(.documentsDirectory.appending(component: "sync-ups.json")), - [] - ) + Self[.fileStorage(.documentsDirectory.appending(component: "sync-ups.json")), default: []] } } diff --git a/Examples/SyncUps/SyncUpsTests/AppFeatureTests.swift b/Examples/SyncUps/SyncUpsTests/AppFeatureTests.swift index d370cca13ded..750c5bab880a 100644 --- a/Examples/SyncUps/SyncUpsTests/AppFeatureTests.swift +++ b/Examples/SyncUps/SyncUpsTests/AppFeatureTests.swift @@ -37,7 +37,7 @@ struct AppFeatureTests { await store.send(\.path[id: 0].detail.doneEditingButtonTapped) { $0.path[id: 0]?.modify(\.detail) { $0.destination = nil - $0.syncUp.title = "Blob" + $0.$syncUp.withLock { $0.title = "Blob" } } } .finish() @@ -63,7 +63,7 @@ struct AppFeatureTests { await store.send(\.path[id: 0].detail.destination.alert.confirmDeletion) { $0.path[id: 0]?.modify(\.detail) { $0.destination = nil } - $0.syncUpsList.syncUps = [] + $0.syncUpsList.$syncUps.withLock { $0 = [] } } await store.receive(\.path.popFrom) { @@ -87,7 +87,7 @@ struct AppFeatureTests { duration: .seconds(6) ) - let sharedSyncUp = Shared(syncUp) + let sharedSyncUp = Shared(value: syncUp) let store = TestStore( initialState: AppFeature.State( path: StackState([ @@ -119,13 +119,15 @@ struct AppFeatureTests { await store.finish() store.assert { $0.path[id: 0]?.modify(\.detail) { - $0.syncUp.meetings = [ - Meeting( - id: Meeting.ID(UUID(0)), - date: Date(timeIntervalSince1970: 1_234_567_890), - transcript: "I completed the project" - ) - ] + $0.$syncUp.withLock { + $0.meetings = [ + Meeting( + id: Meeting.ID(UUID(0)), + date: Date(timeIntervalSince1970: 1_234_567_890), + transcript: "I completed the project" + ) + ] + } } } } diff --git a/Examples/SyncUps/SyncUpsTests/RecordMeetingTests.swift b/Examples/SyncUps/SyncUpsTests/RecordMeetingTests.swift index 4f8264a57f9a..93c7ce6f865d 100644 --- a/Examples/SyncUps/SyncUpsTests/RecordMeetingTests.swift +++ b/Examples/SyncUps/SyncUpsTests/RecordMeetingTests.swift @@ -15,7 +15,7 @@ struct RecordMeetingTests { let store = TestStore( initialState: RecordMeeting.State( syncUp: Shared( - SyncUp( + value: SyncUp( id: SyncUp.ID(), attendees: [ Attendee(id: Attendee.ID()), @@ -76,14 +76,16 @@ struct RecordMeetingTests { await store.receive(\.timerTick) { $0.speakerIndex = 2 $0.secondsElapsed = 6 - $0.syncUp.meetings.insert( - Meeting( - id: Meeting.ID(UUID(0)), - date: Date(timeIntervalSince1970: 1_234_567_890), - transcript: "" - ), - at: 0 - ) + $0.$syncUp.withLock { + _ = $0.meetings.insert( + Meeting( + id: Meeting.ID(UUID(0)), + date: Date(timeIntervalSince1970: 1_234_567_890), + transcript: "" + ), + at: 0 + ) + } #expect($0.durationRemaining == .seconds(0)) } } @@ -95,7 +97,7 @@ struct RecordMeetingTests { let store = TestStore( initialState: RecordMeeting.State( syncUp: Shared( - SyncUp( + value: SyncUp( id: SyncUp.ID(), attendees: [ Attendee(id: Attendee.ID()), @@ -144,7 +146,7 @@ struct RecordMeetingTests { await store.finish() store.assert { - $0.syncUp.meetings[0].transcript = "I completed the project" + $0.$syncUp.withLock { $0.meetings[0].transcript = "I completed the project" } } } @@ -152,7 +154,7 @@ struct RecordMeetingTests { func endMeetingSave() async { let clock = TestClock() - let store = TestStore(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + let store = TestStore(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } withDependencies: { $0.continuousClock = clock @@ -174,14 +176,16 @@ struct RecordMeetingTests { await store.send(\.alert.confirmSave) { $0.alert = nil - $0.syncUp.meetings.insert( - Meeting( - id: Meeting.ID(UUID(0)), - date: Date(timeIntervalSince1970: 1_234_567_890), - transcript: "" - ), - at: 0 - ) + $0.$syncUp.withLock { + _ = $0.meetings.insert( + Meeting( + id: Meeting.ID(UUID(0)), + date: Date(timeIntervalSince1970: 1_234_567_890), + transcript: "" + ), + at: 0 + ) + } } } @@ -189,7 +193,7 @@ struct RecordMeetingTests { func endMeetingDiscard() async { let clock = TestClock() - let store = TestStore(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + let store = TestStore(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } withDependencies: { $0.continuousClock = clock @@ -214,7 +218,7 @@ struct RecordMeetingTests { let store = TestStore( initialState: RecordMeeting.State( syncUp: Shared( - SyncUp( + value: SyncUp( id: SyncUp.ID(), attendees: [ Attendee(id: Attendee.ID()), @@ -252,14 +256,16 @@ struct RecordMeetingTests { await store.send(\.alert.confirmSave) { $0.alert = nil - $0.syncUp.meetings.insert( - Meeting( - id: Meeting.ID(UUID(0)), - date: Date(timeIntervalSince1970: 1_234_567_890), - transcript: "" - ), - at: 0 - ) + $0.$syncUp.withLock { + _ = $0.meetings.insert( + Meeting( + id: Meeting.ID(UUID(0)), + date: Date(timeIntervalSince1970: 1_234_567_890), + transcript: "" + ), + at: 0 + ) + } } } @@ -270,7 +276,7 @@ struct RecordMeetingTests { let store = TestStore( initialState: RecordMeeting.State( syncUp: Shared( - SyncUp( + value: SyncUp( id: SyncUp.ID(), attendees: [ Attendee(id: Attendee.ID()), @@ -333,7 +339,7 @@ struct RecordMeetingTests { func discardAfterSpeechRecognitionFailure() async { let clock = TestClock() - let store = TestStore(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + let store = TestStore(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } withDependencies: { $0.continuousClock = clock diff --git a/Examples/SyncUps/SyncUpsTests/SyncUpDetailTests.swift b/Examples/SyncUps/SyncUpsTests/SyncUpDetailTests.swift index 9eaf600d6d7a..18fc991c3105 100644 --- a/Examples/SyncUps/SyncUpsTests/SyncUpDetailTests.swift +++ b/Examples/SyncUps/SyncUpsTests/SyncUpDetailTests.swift @@ -9,7 +9,7 @@ struct SyncUpDetailTests { @Test func speechRestricted() async { - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(.mock))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: .mock))) { SyncUpDetail() } withDependencies: { $0.speechClient.authorizationStatus = { .restricted } @@ -22,7 +22,7 @@ struct SyncUpDetailTests { @Test func speechDenied() async throws { - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(.mock))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: .mock))) { SyncUpDetail() } withDependencies: { $0.speechClient.authorizationStatus = { @@ -42,7 +42,7 @@ struct SyncUpDetailTests { let store = TestStore( initialState: SyncUpDetail.State( destination: .alert(.speechRecognitionDenied), - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() @@ -62,7 +62,7 @@ struct SyncUpDetailTests { let store = TestStore( initialState: SyncUpDetail.State( destination: .alert(.speechRecognitionDenied), - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() @@ -79,7 +79,7 @@ struct SyncUpDetailTests { @Test func speechAuthorized() async throws { - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(.mock))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: .mock))) { SyncUpDetail() } withDependencies: { $0.speechClient.authorizationStatus = { .authorized } @@ -93,7 +93,7 @@ struct SyncUpDetailTests { @Test func edit() async { var syncUp = SyncUp.mock - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } withDependencies: { $0.uuid = .incrementing @@ -110,7 +110,7 @@ struct SyncUpDetailTests { await store.send(.doneEditingButtonTapped) { $0.destination = nil - $0.syncUp.title = "Blob's Meeting" + $0.$syncUp.withLock { $0.title = "Blob's Meeting" } } } diff --git a/Examples/SyncUps/SyncUpsTests/SyncUpsListTests.swift b/Examples/SyncUps/SyncUpsTests/SyncUpsListTests.swift index a325106e1cad..f8b428b97156 100644 --- a/Examples/SyncUps/SyncUpsTests/SyncUpsListTests.swift +++ b/Examples/SyncUps/SyncUpsTests/SyncUpsListTests.swift @@ -33,7 +33,7 @@ struct SyncUpsListTests { await store.send(.confirmAddSyncUpButtonTapped) { $0.destination = nil - $0.syncUps = [syncUp] + $0.$syncUps.withLock { $0 = [syncUp] } } } @@ -64,15 +64,17 @@ struct SyncUpsListTests { await store.send(.confirmAddSyncUpButtonTapped) { $0.destination = nil - $0.syncUps = [ - SyncUp( - id: SyncUp.ID(uuidString: "deadbeef-dead-beef-dead-beefdeadbeef")!, - attendees: [ - Attendee(id: Attendee.ID(UUID(0))) - ], - title: "Design" - ) - ] + $0.$syncUps.withLock { + $0 = [ + SyncUp( + id: SyncUp.ID(uuidString: "deadbeef-dead-beef-dead-beefdeadbeef")!, + attendees: [ + Attendee(id: Attendee.ID(UUID(0))) + ], + title: "Design" + ) + ] + } } } } diff --git a/Package.resolved b/Package.resolved index 18b8e14815e2..e80c6991a14c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e2618e836df1ca46810fbd99802b7402f1b1f9397b7b0d4d9f5ed2a60edd0a1f", + "originHash" : "daf106e4a50354e80d4874c54dee4b63ae0e8650346403e3489835d2a6005ea8", "pins" : [ { "identity" : "combine-schedulers", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "642e6aab8e03e5f992d9c83e38c5be98cfad5078", - "version" : "1.5.5" + "revision" : "bc92c4b27f9a84bfb498cdbfdf35d5a357e9161f", + "version" : "1.5.6" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", - "version" : "1.1.3" + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "6054df64b55186f08b6d0fd87152081b8ad8d613", - "version" : "1.2.0" + "revision" : "163409ef7dae9d960b87f34b51587b6609a76c1f", + "version" : "1.3.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "fd1fb25b68fdb9756cd61d23dbd9e2614b340085", - "version" : "1.4.0" + "revision" : "7d2eb4ad20efb2838269645410d26b64ca48d8aa", + "version" : "1.6.1" } }, { @@ -114,17 +114,26 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "bc67aa8e461351c97282c2419153757a446ae1c9", - "version" : "1.3.5" + "revision" : "8d52279b9809ef27eabe7d5420f03734528f19da", + "version" : "1.4.1" + } + }, + { + "identity" : "swift-sharing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-sharing", + "state" : { + "revision" : "5d3d1193225567d156dae16b2bb353ef261011f9", + "version" : "1.0.2" } }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", "state" : { - "revision" : "7b0bbbae90c41f848f90ac7b4df6c4f50068256d", - "version" : "1.17.5" + "revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7", + "version" : "1.17.6" } }, { @@ -132,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "515f79b522918f83483068d99c68daeb5116342d", - "version" : "600.0.0-prerelease-2024-09-04" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { @@ -141,8 +150,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "bc2a151366f2cd0e347274544933bc2acb00c9fe", - "version" : "1.4.0" + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" } } ], diff --git a/Package.swift b/Package.swift index e4c6478781e4..6f531d62f48f 100644 --- a/Package.swift +++ b/Package.swift @@ -28,9 +28,10 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0"), .package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.2.2"), .package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"), + .package(url: "https://github.com/pointfreeco/swift-sharing", "0.1.2"..<"2.0.0"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.3.0"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"), + .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0"), ], targets: [ .target( @@ -47,6 +48,7 @@ let package = Package( .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "Perception", package: "swift-perception"), + .product(name: "Sharing", package: "swift-sharing"), .product(name: "SwiftUINavigation", package: "swift-navigation"), .product(name: "UIKitNavigation", package: "swift-navigation"), ], diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 466578b604b2..bbccd895f980 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -28,9 +28,10 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0"), .package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.2.2"), .package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"), + .package(url: "https://github.com/pointfreeco/swift-sharing", "0.1.2"..<"2.0.0"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.3.0"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"), + .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0"), ], targets: [ .target( @@ -47,6 +48,7 @@ let package = Package( .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "Perception", package: "swift-perception"), + .product(name: "Sharing", package: "swift-sharing"), .product(name: "SwiftUINavigation", package: "swift-navigation"), .product(name: "UIKitNavigation", package: "swift-navigation"), ], diff --git a/README.md b/README.md index 99b2fb8a112d..cae5274af1db 100644 --- a/README.md +++ b/README.md @@ -532,13 +532,14 @@ advanced usages. The documentation for releases and `main` are available here: * [`main`](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/) -* [1.16.0](https://pointfreeco.github.io/swift-composable-architecture/1.16.0/documentation/composablearchitecture/) ([migration guide](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.16)) +* [1.17.0](https://pointfreeco.github.io/swift-composable-architecture/1.17.0/documentation/composablearchitecture/) ([migration guide](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.17))
Other versions + * [1.16.0](https://pointfreeco.github.io/swift-composable-architecture/1.16.0/documentation/composablearchitecture/) ([migration guide](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.16)) * [1.15.0](https://pointfreeco.github.io/swift-composable-architecture/1.15.0/documentation/composablearchitecture/) ([migration guide](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.15)) * [1.14.0](https://pointfreeco.github.io/swift-composable-architecture/1.14.0/documentation/composablearchitecture/) ([migration guide](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.14)) * [1.13.0](https://pointfreeco.github.io/swift-composable-architecture/1.13.0/documentation/composablearchitecture/) ([migration guide](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.13)) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides.md index e9e2773d446d..0b32ad55472d 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides.md @@ -14,6 +14,7 @@ APIs, and these guides contain tips to do so. ## Topics +- - - - diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.10.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.10.md index 0d6080afa559..062fd7f80fd9 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.10.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.10.md @@ -1,7 +1,7 @@ # Migrating to 1.10 -Update your code to make use of the new state sharing tools in the library, such as the ``Shared`` -property wrapper, and the ``AppStorageKey`` and ``FileStorageKey`` persistence strategies. +Update your code to make use of the new state sharing tools in the library, such as the `Shared` +property wrapper, and the `appStorage` and `fileStorage` persistence strategies. ## Overview @@ -18,7 +18,7 @@ The new tools added are concerned with allowing one to seamlessly share state wi application that is easy to understand, and most importantly, testable. See the dedicated article for more information on how to use these new tools. -To share state in one feature with another feature, simply use the ``Shared`` property wrapper: +To share state in one feature with another feature, simply use the `Shared` property wrapper: ```swift @ObservableState @@ -32,8 +32,8 @@ This will require that `SignUpData` be passed in from the parent, and any change will be instantly observed by all features holding onto it. Further, there are persistence strategies one can employ in `@Shared`. For example, if you want any -changes of `signUpData` to be automatically persisted to the file system you can use the -``PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` and specify a URL: +changes of `signUpData` to be automatically persisted to the file system you can use +`fileStorage(_:decoder:encoder:)` and specify a URL: ```swift @ObservableState @@ -48,7 +48,7 @@ will automatically be persisted to disk. Further, if the disk version changes, a `signUpData` in the application will automatically update. There is another persistence strategy for storing simple data types in user defaults, called -``PersistenceReaderKey/appStorage(_:)-4l5b``. It can refer to a value in user defaults by a string +`appStorage`. It can refer to a value in user defaults by a string key: ```swift @@ -59,7 +59,7 @@ struct State { } ``` -Similar to ``PersistenceReaderKey/fileStorage(_:decoder:encoder:)``, upon launch of the application the initial +Similar to `fileStorage(_:decoder:encoder:)`, upon launch of the application the initial value of `isOn` will be populated from user defaults, and any change to `isOn` will be automatically persisted to user defaults. Further, if the user defaults value changes, all instances of `isOn` in the application will automatically update. diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.11.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.11.md index ff404842bedc..ce1f64c140c1 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.11.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.11.md @@ -1,7 +1,7 @@ # Migrating to 1.11 -Update your code to use the new ``Shared/withLock(_:)`` method for mutating shared state from -asynchronous contexts, rather than mutating the underlying wrapped value directly. +Update your code to use the new `withLock` method for mutating shared state from asynchronous +contexts, rather than mutating the underlying wrapped value directly. ## Overview @@ -46,10 +46,10 @@ to it from two different threads. However, allowing direct mutation does make th to race conditions. If you were to perform `count += 1` from 1,000 threads, it is possible for the final value to not be 1,000. -We wanted the [`@Shared`]() type to be as ergonomic as possible, and that is why we make -it directly mutable, but we should not be allowing these mutations to happen from asynchronous -contexts. And so now the ``Shared/wrappedValue`` setter has been marked unavailable from -asynchronous contexts, with a helpful message of how to fix: +We wanted the `@Shared` type to be as ergonomic as possible, and that is why we make it directly +mutable, but we should not be allowing these mutations to happen from asynchronous contexts. And so +now the `wrappedValue` setter has been marked unavailable from asynchronous contexts, with +a helpful message of how to fix: ```swift case .delayedIncrementButtonTapped: @@ -59,8 +59,7 @@ case .delayedIncrementButtonTapped: } ``` -To fix this deprecation you can use the new ``Shared/withLock(_:)`` method on the projected value of -`@Shared`: +To fix this deprecation you can use the new `withLock` method on the projected value of `@Shared`: ```swift case .delayedIncrementButtonTapped: @@ -82,14 +81,13 @@ $count.withLock { $0 = currentCount + 1 } But there is no way to 100% prevent race conditions in code. Even actors are susceptible to problems due to re-entrancy. To avoid problems like the above we recommend wrapping as many mutations of the -shared state as possible in a single ``Shared/withLock(_:)``. That will make sure that the full unit -of work is guarded by a lock. +shared state as possible in a single `withLock`. That will make sure that the full unit of work is +guarded by a lock. ## Supplying mock read-only state to previews -A new ``SharedReader/constant(_:)`` helper on ``SharedReader`` has been introduced to simplify -supplying mock data to Xcode previews. It works like SwiftUI's `Binding.constant`, but for shared -references: +A new `constant` helper on `SharedReader` has been introduced to simplify supplying mock data to +Xcode previews. It works like SwiftUI's `Binding.constant`, but for shared references: ```swift #Preview { @@ -110,13 +108,13 @@ A few bug fixes landed in 1.11.2 that may be source breaking. They are described ### `withLock` is now `@MainActor` In [version 1.11]() of the library we deprecated mutating shared state from -asynchronous contexts, such as effects, and instead recommended using the new -``Shared/withLock(_:)`` method. Doing so made it possible to lock all mutations to the shared state -and prevent race conditions (see the [migration guide]() for more info). +asynchronous contexts, such as effects, and instead recommended using the new `withLock` method. +Doing so made it possible to lock all mutations to the shared state and prevent race conditions (see +the [migration guide]() for more info). However, this did leave open the possibility for deadlocks if shared state was read from and written -to on different threads. To fix this we have now restricted ``Shared/withLock(_:)`` to the -`@MainActor`, and so you will now need to `await` its usage: +to on different threads. To fix this we have now restricted `withLock` to the `@MainActor`, and so +you will now need to `await` its usage: ```diff -sharedCount.withLock { $0 += 1 } @@ -127,7 +125,7 @@ The compiler should suggest this fix-it for you. ### Optional dynamic member lookup on `Shared` is deprecated/disfavored -When the ``Shared`` property wrapper was first introduced, its dynamic member lookup was overloaded +When the `@Shared` property wrapper was first introduced, its dynamic member lookup was overloaded to automatically unwrap optionals for ergonomic purposes: ```swift @@ -145,12 +143,12 @@ $shared.optionalProperty // Shared?, *not* Shared …and required casting and other tricks to transform shared values into what one might expect. And so this dynamic member lookup is deprecated and has been disfavored, and will eventually be -removed entirely. Instead, you can use ``Shared/init(_:)`` to explicitly unwrap a shared optional +removed entirely. Instead, you can use `Shared.init(_:)` to explicitly unwrap a shared optional value. Disfavoring it does have the consequence of being source breaking in the case of `if let` and `guard let` expressions, where Swift does not select the optional overload automatically. To -migrate, use ``Shared/init(_:)``: +migrate, use `Shared.init(_:)`: ```diff -if let sharedUnwrappedProperty = $shared.optionalProperty { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.12.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.12.md index 6891e13add8f..34dc7c48c9f3 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.12.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.12.md @@ -16,12 +16,11 @@ API, as well as beta support for Swift Testing. Version 1.10 of the Composable Architecture introduced a powerful tool for [sharing state]() amongst your features, and included several built-in persistence -strategies, including [file storage](). This -strategy, however, was not very flexible, and only supported the default JSON encoding and decoding -offered by Swift. +strategies, including file storage. This strategy, however, was not very flexible, and only +supported the default JSON encoding and decoding offered by Swift. In this version, you can now define custom encoding and decoding logic using -``PersistenceReaderKey/fileStorage(_:decode:encode:)``. +`fileStorage(_:decode:encode:)`. ## Swift Testing diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.17.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.17.md new file mode 100644 index 000000000000..54e817bdf07a --- /dev/null +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.17.md @@ -0,0 +1,28 @@ +# Migrating to 1.17 + +The `@Shared` property wrapper and related tools have been extracted to their own +library so that they can be used in non-Composable Architecture applications. This a +backwards compatible change, but some new deprecations have been introduced. + +## Overview + +The [Sharing][sharing-gh] package is a general purpose, state-sharing and persistence toolkit that +works on all platforms supported by Swift, including iOS/macOS, Linux, Windows, Wasm, and more. +We released two versions of this package simultaneously: a [0.1][0.1-release] version that is a +backwards-compatible version of the tools that shipped with the Composable Architecture <1.16, as +well as a [1.0][1.0-release] version with some non-backwards compatible changes. + +If you wish to remain on the backwards-compatible version of Sharing for the time being, then you +can add an explicit dependency on the library to pin to any version less than 1.0: + +```swift +.package(url: "https://github.com/pointfreeco/swift-sharing", from: "0.1.0"), +``` + +If you are ready to upgrade to 1.0, then you can follow the +[1.0 migration guide][1.0-migration] from that package. + +[sharing-gh]: https://github.com/pointfreeco/swift-sharing +[1.0-migration]: https://swiftpackageindex.com/pointfreeco/swift-sharing/main/documentation/sharing/migratingto1.0 +[0.1-release]: https://github.com/pointfreeco/swift-sharing/releases/0.1.0 +[1.0-release]: https://github.com/pointfreeco/swift-sharing/releases/1.0.0 diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md index f831be22249c..6066d29887d5 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md @@ -12,11 +12,14 @@ shared. Because the Composable Architecture highly prefers modeling domains with than reference types, sharing state can be tricky. This is why the library comes with a few tools for sharing state with many parts of your -application. There are two main kinds of shared state in the library: explicitly passed state and +application. The majority of these tools exist outside of the Composable Architecture, and are in +a separate library called [Sharing](https://github.com/pointfreeco/swift-sharing). You can refer +to that library's documentation for more information, but we have also repeated some of the most +important concepts in this article. + +There are two main kinds of shared state in the library: explicitly passed state and persisted state. And there are 3 persistence strategies shipped with the library: -[in-memory](), -[user defaults](), and -[file storage](). You can also implement +in-memory, user defaults, and file storage. You can also implement your own persistence strategy if you want to use something other than user defaults or the file system, such as SQLite. @@ -30,7 +33,7 @@ system, such as SQLite. * [Observing changes to shared state](#Observing-changes-to-shared-state) * [Initialization rules](#Initialization-rules) * [Deriving shared state](#Deriving-shared-state) -* [Testing](#Testing) +* [Testing shared state](#Testing-shared-state) * [Testing when using persistence](#Testing-when-using-persistence) * [Testing when using custom persistence strategies](#Testing-when-using-custom-persistence-strategies) * [Overriding shared state in tests](#Overriding-shared-state-in-tests) @@ -58,12 +61,13 @@ And second, applications typically do not have a _single_ source of truth. That simplistic. If your application loads data from an API, or from disk, or from user defaults, then the "truth" for that data does not lie in your application. It lies externally. -In reality, there are _two_ sources of "truth" in any application. There is the state the -application needs to execute its logic and behavior. This is the kind of state that determines if a -button is enabled or disabled, drives navigation such as sheets and drill-downs, and handles -validation of forms. Such state only makes sense for the application. +In reality, there are _two_ sources of "truth" in any application: + +1. There is the state the application needs to execute its logic and behavior. This is the kind of +state that determines if a button is enabled or disabled, drives navigation such as sheets and +drill-downs, and handles validation of forms. Such state only makes sense for the application. -Then there is a second source of "truth" in an application, which is the data that lies in some +2. Then there is a second source of "truth" in an application, which is the data that lies in some external system and needs to be loaded into the application. Such state is best modeled as a dependency or using the shared state tools discussed in this article. @@ -73,7 +77,7 @@ This is the simplest kind of shared state to get started with. It allows you to many features without any persistence. The data is only held in memory, and will be cleared out the next time the application is run. -To share data in this style, use the [`@Shared`]() property wrapper with no arguments. +To share data in this style, use the `@Shared` property wrapper with no arguments. For example, suppose you have a feature that holds a count and you want to be able to hand a shared reference to that count to other features. You can do so by holding onto a `@Shared` property in the feature's state: @@ -90,10 +94,6 @@ struct ParentFeature { } ``` -> Important: It is not possible to provide a default to a `@Shared` value. It must be passed to the -> feature's state from the outside. See for more -> information about how to initialize types that use `@Shared`. - Then suppose that this feature can present a child feature that wants access to this shared `count` value. It too would hold onto a `@Shared` property to a count: @@ -110,7 +110,7 @@ struct ChildFeature { ``` When the parent features creates the child feature's state, it can pass a _reference_ to the shared -count rather than the actual count value by using the `$count` ``Shared/projectedValue``: +count rather than the actual count value by using the `$count` projected value: ```swift case .presentButtonTapped: @@ -126,7 +126,7 @@ Now any mutation the `ChildFeature` makes to its `count` will be instantly made Explicitly shared state discussed above is a nice, lightweight way to share a piece of data with many parts of your application. However, sometimes you want to share state with the entire application without having to pass it around explicitly. One can do this by passing a -``PersistenceKey`` to the `@Shared` property wrapper, and the library comes with three persistence +`SharedKey` to the `@Shared` property wrapper, and the library comes with three persistence strategies, as well as the ability to create custom persistence strategies. #### In-memory @@ -135,7 +135,7 @@ This is the simplest persistence strategy in that it doesn't actually persist at the data in memory and makes it available to every part of the application, but when the app is relaunched the data will be reset back to its default. -It can be used by passing ``PersistenceReaderKey/inMemory(_:)`` to the `@Shared` property wrapper. +It can be used by passing `inMemory` to the `@Shared` property wrapper. For example, suppose you want to share an integer count value with the entire application so that any feature can read from and write to the integer. This can be done like so: @@ -160,7 +160,7 @@ get out of sync. #### User defaults If you would like to persist your shared value across application launches, then you can use the -``PersistenceReaderKey/appStorage(_:)-4l5b`` strategy with `@Shared` in order to automatically +`appStorage` strategy with `@Shared` in order to automatically persist any changes to the value to user defaults. It works similarly to in-memory sharing discussed above. It requires a key to store the value in user defaults, as well as a default value that will be used when there is no value in the user defaults: @@ -175,13 +175,12 @@ automatically loaded the next time the application launches. This form of persistence only works for simple data types because that is what works best with `UserDefaults`. This includes strings, booleans, integers, doubles, URLs, data, and more. If you need to store more complex data, such as custom data types serialized to JSON, then you will want -to use the [`.fileStorage`]() strategy or a -[custom persistence]() strategy. +to use the `.fileStorage` strategy or a custom persistence strategy. #### File storage If you would like to persist your shared value across application launches, and your value is -complex (such as a custom data type), then you can use the ``PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` +complex (such as a custom data type), then you can use the `fileStorage` strategy with `@Shared`. It automatically persists any changes to the file system. It works similarly to the in-memory sharing discussed above, but it requires a URL to store the data @@ -198,19 +197,19 @@ when loading from disk. For this reason the value held in `@Shared(.fileStorage( #### Custom persistence It is possible to define all new persistence strategies for the times that user defaults or JSON -files are not sufficient. To do so, define a type that conforms to the ``PersistenceKey`` protocol: +files are not sufficient. To do so, define a type that conforms to the `SharedKey` protocol: ```swift -public final class CustomPersistenceKey: PersistenceKey { +public final class CustomSharedKey: SharedKey { // ... } ``` -And then define a static function on the ``PersistenceKey`` protocol for creating your new +And then define a static function on the `SharedKey` protocol for creating your new persistence strategy: ```swift -extension PersistenceReaderKey { +extension SharedReaderKey { public static func custom(/*...*/) -> Self where Self == CustomPersistence { CustomPersistence(/* ... */) @@ -219,22 +218,22 @@ extension PersistenceReaderKey { ``` With those steps done you can make use of the strategy in the same way one does for -``PersistenceReaderKey/appStorage(_:)-4l5b`` and ``PersistenceReaderKey/fileStorage(_:decoder:encoder:)``: +`appStorage` and `fileStorage`: ```swift @Shared(.custom(/* ... */)) var myValue: Value ``` -The ``PersistenceKey`` protocol represents loading from _and_ saving to some external storage, +The `SharedKey` protocol represents loading from _and_ saving to some external storage, such as the file system or user defaults. Sometimes saving is not a valid operation for the external system, such as if your server holds onto a remote configuration file that your app uses to customize its appearance or behavior. In those situations you can conform to the -``PersistenceReaderKey`` protocol. See for more +`SharedReaderKey` protocol. See for more information. ## Observing changes to shared state -The ``Shared`` property wrapper exposes a ``Shared/publisher`` property so that you can observe +The `@Shared` property wrapper exposes a `publisher` property so that you can observe changes to the reference from any part of your application. For example, if some feature in your app wants to listen for changes to some shared `count` value, then it can introduce an `onAppear` action that kicks off a long-living effect that subscribes to changes of `count`: @@ -280,7 +279,7 @@ shared state. It is common to need to provide a custom initializer to your feature's ``Reducer/State`` type, especially when modularizing. When using -[`@Shared`]() in your `State` that can become complicated. +`@Shared` in your `State` that can become complicated. Depending on your exact situation you can do one of the following: * You are using non-persisted shared state (i.e. no argument is passed to `@Shared`), and the @@ -316,10 +315,10 @@ should take a plain, non-`Shared` value and you construct the `Shared` value in ``` * You are using a persistence strategy with shared state (_e.g._ -``PersistenceReaderKey/appStorage(_:)-4l5b``, ``PersistenceReaderKey/fileStorage(_:decoder:encoder:)``, _etc._), +`appStorage`, `fileStorage`, _etc._), then the initializer should take a plain, non-`Shared` value and you construct the `Shared` value in -the initializer using ``Shared/init(wrappedValue:_:fileID:line:)-9kfmy`` which takes a -``PersistenceKey`` as the second argument: +the initializer using the initializer which takes a +`SharedKey` as the second argument: ```swift public struct State { @@ -338,7 +337,7 @@ the initializer using ``Shared/init(wrappedValue:_:fileID:line:)-9kfmy`` which t > Important: The value passed to this initializer is only used if the external storage does not > already have a value. If a value exists in the storage then it is not used. In fact, the - > `wrappedValue` argument of ``Shared/init(wrappedValue:_:fileID:line:)-9kfmy`` is an + > `wrappedValue` argument of `Shared.init(wrappedValue:)` is an > `@autoclosure` so that it is not even evaluated if not used. For that reason you > may prefer to make the argument to the initializer an `@autoclosure` so that it too is evaluated > only if actually used: @@ -383,7 +382,7 @@ case .nextButtonTapped: ) ``` -Here we are using the ``Shared/projectedValue`` value using `$` syntax, `$signUpData`, and then +Here we are using the projected value of `@Shared` value using `$` syntax, `$signUpData`, and then further dot-chaining onto that projection to derive a `Shared`. This can be a powerful way for features to hold onto only the bare minimum of shared state it needs to do its job. @@ -435,57 +434,28 @@ responsible for persisting and deriving shared state to pass to the child. If your shared state is a collection, and in particular an `IdentifiedArray`, then we have another tool for deriving shared state to a particular element of the array. You can subscript into a -``Shared`` collection with the `[id:]` subscript, and that will give a piece of optional shared -state (thanks to a dynamic member overload ``Shared/subscript(dynamicMember:)-9xw64``), which you -can then unwrap to turn into honest shared state: +`Shared` collection with the `[id:]` subscript, and that will give a piece of shared optional +state, which you can then unwrap to turn into honest shared state using a special `Shared` +initializer: ```swift @Shared(.fileStorage(.todos)) var todos: IdentifiedArrayOf = [] -guard let todo = $todos[id: todoID] +guard let todo = Shared($todos[id: todoID]) else { return } todo // Shared ``` -There is another tool for deriving shared state, and it is the computed property ``Shared/elements`` -that is defined on shared collections. It derives a collection of shared elements so that you can -get access to a shared reference of just one particular element in a collection. - -However, it is only appropriate to use this in conjunction with `ForEach` in order to derive a -shared reference for each element of a collection: - -```swift -struct State { - @Shared(.fileStorage(.todos)) var todos: IdentifiedArrayOf = [] - // ... -} - -// ... - -ForEach(store.$todos.elements) { $todo in - NavigationLink( - // $todo: Shared - // todo: Todo - state: Path.State.todo(TodoFeature.State(todo: $todo)) - ) { - Text(todo.title) - } -} -``` - -> Important: We do not recommend using ``Shared/elements`` outside of using it with `ForEach`, -> `List`, and other SwiftUI views that take collections. - -## Testing +## Testing shared state Shared state behaves quite a bit different from the regular state held in Composable Architecture features. It is capable of being changed by any part of the application, not just when an action is sent to the store, and it has reference semantics rather than value semantics. Typically references cause serious problems with testing, especially exhaustive testing that the library prefers (see -), because references cannot be copied and so one cannot inspect the changes before and -after an action is sent. +), because references cannot be copied and so one cannot inspect the changes +before and after an action is sent. -For this reason, the ``Shared`` property wrapper does extra work during testing to preserve a +For this reason, the `@Shared` property wrapper does extra work during testing to preserve a previous snapshot of the state so that one can still exhaustively assert on shared state, even though it is a reference. @@ -557,7 +527,7 @@ func increment() async { (Expected: −, Actual: +) ``` -This works even though the `@Shared` count is a reference type. The ``TestStore`` and ``Shared`` +This works even though the `@Shared` count is a reference type. The ``TestStore`` and `@Shared` type work in unison to snapshot the state before and after the action is sent, allowing us to still assert in an exhaustive manner. @@ -609,7 +579,8 @@ Call 'Shared.assert' to exhaustively test these changes, or call 'skipChang ``` In order to get this test passing we have to explicitly assert on the shared counter state at -the end of the test, which we can do using the ``Shared/assert(_:fileID:file:line:column:)`` method: +the end of the test, which we can do using the ``TestStore/assert(_:fileID:file:line:column:)`` +method: ```swift @Test @@ -618,8 +589,8 @@ func increment() async { SimpleFeature() } await store.send(.incrementButtonTapped) - store.state.$count.assert { - $0 = 1 + store.assert { + $0.count = 1 } } ``` @@ -632,8 +603,8 @@ to its reference semantics, it is still possible to get exhaustive test coverage #### Testing when using persistence It is also possible to test when using one of the persistence strategies provided by the library, -which are ``PersistenceReaderKey/appStorage(_:)-4l5b`` and -``PersistenceReaderKey/fileStorage(_:decoder:encoder:)``. Typically persistence is difficult to test because the +which are `appStorage` and +`fileStorage`. Typically persistence is difficult to test because the persisted data bleeds over from test to test, making it difficult to exhaustively prove how each test behaves in isolation. @@ -642,8 +613,8 @@ default the `.appStorage` strategy uses a non-persisting user defaults so that c actually persisted across test runs. And the `.fileStorage` strategy uses a mock file system so that changes to state are not actually persisted to the file system. -This means that if we altered the `SimpleFeature` of the section above to -use app storage: +This means that if we altered the `SimpleFeature` of the +section above to use app storage: ```swift struct State: Equatable { @@ -656,13 +627,13 @@ struct State: Equatable { #### Testing when using custom persistence strategies When creating your own custom persistence strategies you must careful to do so in a style that -is amenable to testing. For example, the ``PersistenceReaderKey/appStorage(_:)-4l5b`` persistence -strategy that comes with the library injects a ``Dependencies/DependencyValues/defaultAppStorage`` +is amenable to testing. For example, the `appStorage` persistence +strategy that comes with the library injects a `defaultAppStorage` dependency so that one can inject a custom `UserDefaults` in order to execute in a controlled -environment. By default ``Dependencies/DependencyValues/defaultAppStorage`` uses a non-persisting +environment. By default `defaultAppStorage` uses a non-persisting user defaults, but you can also customize it to use any kind of defaults. -Similarly the ``PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` persistence strategy uses an internal +Similarly the `fileStorage` persistence strategy uses an internal dependency for changing how files are written to the disk and loaded from disk. In tests the dependency will forgo any interaction with the file system and instead write data to a `[URL: Data]` dictionary, and load data from that dictionary. That emulates how the file system works, but without @@ -687,8 +658,8 @@ func basics() { However, if your test suite is a part of an app target, then the entry point of the app will execute and potentially cause an early access of `@Shared`, thus capturing a different default value than what is specified above. This quirk of tests in app targets is documented in - of the article, and a similar quirk exists for Xcode -previews and is discussed below in . + of the article, and a similar quirk +exists for Xcode previews and is discussed below in . The most robust workaround to this issue is to simply not execute your app's entry point when tests are running, which we detail in . This makes it so that you @@ -714,8 +685,8 @@ When UI testing your app you must take extra care so that shared state is not pe app runs because that can cause one test to bleed over into another test, making it difficult to write deterministic tests that always pass. To fix this, you can set an environment value from your UI test target, and then if that value is present in the app target you can override the -``Dependencies/DependencyValues/defaultAppStorage`` and -``Dependencies/DependencyValues/defaultFileStorage`` dependencies so that they use in-memory +`defaultAppStorage` and +`defaultFileStorage` dependencies so that they use in-memory storage, i.e. they do not persist ever: ```swift @@ -829,14 +800,14 @@ struct State { ## Read-only shared state -The [`@Shared`]() property wrapper described above gives you access to a piece of shared +The `@Shared` property wrapper described above gives you access to a piece of shared state that is both readable and writable. That is by far the most common use case when it comes to shared state, but there are times when one wants to express access to shared state for which you are not allowed to write to it, or possibly it doesn't even make sense to write to it. -For those times there is the [`@SharedReader`]() property wrapper. It represents +For those times there is the `@SharedReader` property wrapper. It represents a reference to some piece of state shared with multiple parts of the application, but you are not -allowed to write to it. Every persistence strategy discussed above works with ``SharedReader``, +allowed to write to it. Every persistence strategy discussed above works with `SharedReader`, however if you try to mutate the state you will get a compiler error: ```swift @@ -845,8 +816,8 @@ isOn = true // 🛑 ``` It is also possible to make custom persistence strategies that only have the notion of loading and -subscribing, but cannot write. To do this you will conform only to the ``PersistenceReaderKey`` -protocol instead of the full ``PersistenceKey`` protocol. +subscribing, but cannot write. To do this you will conform only to the `SharedReaderKey` +protocol instead of the full `SharedKey` protocol. For example, you could create a `.remoteConfig` strategy that loads (and subscribes to) a remote configuration file held on your server so that it is kept automatically in sync: @@ -859,7 +830,7 @@ configuration file held on your server so that it is kept automatically in sync: Due to the nature of persisting data to external systems, you lose some type safety when shuffling data from your app to the persistence storage and back. For example, if you are using the -``PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` strategy to save an array of users to disk you might do so +`fileStorage` strategy to save an array of users to disk you might do so like this: ```swift @@ -883,18 +854,18 @@ instead of a plain array: But if you forget to convert _all_ shared user arrays to the new identified array your application will still compile, but it will be broken. The two types of storage will not share state. -To add some type-safety and reusability to this process you can extend the ``PersistenceReaderKey`` +To add some type-safety and reusability to this process you can extend the `SharedReaderKey` protocol to add a static variable for describing the details of your persistence: ```swift -extension PersistenceReaderKey where Self == FileStorageKey> { +extension SharedReaderKey where Self == FileStorageKey> { static var users: Self { fileStorage(.users) } } ``` -Then when using [`@Shared`]() you can specify this key directly without `.fileStorage`: +Then when using `@Shared` you can specify this key directly without `.fileStorage`: ```swift @Shared(.users) var users: IdentifiedArrayOf = [] @@ -913,7 +884,7 @@ This technique works for all types of persistence strategies. For example, a typ key can be constructed like so: ```swift -extension PersistenceReaderKey where Self == InMemoryKey> { +extension SharedReaderKey where Self == InMemoryKey> { static var users: Self { inMemory("users") } @@ -923,7 +894,7 @@ extension PersistenceReaderKey where Self == InMemoryKey And a type-safe `.appStorage` key can be constructed like so: ```swift -extension PersistenceReaderKey where Self == AppStorageKey { +extension SharedReaderKey where Self == AppStorageKey { static var count: Self { appStorage("count") } @@ -933,16 +904,12 @@ extension PersistenceReaderKey where Self == AppStorageKey { And this technique also works on [custom persistence]() strategies. -Further, you can use the ``PersistenceKeyDefault`` type to also provide a default that is used -with the persistence strategy. For example, to use a default value of `[]` with the `.users` -persistence strategy described above, we can do the following: +Further, you can also bake in the default of the shared value into your key by doing the following: ```swift -extension PersistenceReaderKey -where Self == PersistenceKeyDefault>> -{ +extension SharedReaderKey where Self == FileStorageKey>.Default { static var users: Self { - PersistenceKeyDefault(.fileStorage(.users), []) + Self[.fileStorage(.users), default: []] } } ``` @@ -956,7 +923,7 @@ you can even leave off the type annotation: ## Shared state in pre-observation apps -It is possible to use [`@Shared`]() in features that have not yet been updated with +It is possible to use `@Shared` in features that have not yet been updated with the observation tools released in 1.7, such as the ``ObservableState()`` macro. In the reducer you can use `@Shared` regardless of your use of the observation tools. @@ -1000,16 +967,16 @@ struct FeatureView: View { ## Concurrent mutations to shared state While the [`@Shared`]() property wrapper makes it possible to treat shared state -_mostly_ like regular state, you do have to perform some extra steps to mutate shared state from -an asynchronous context. This is because shared state is technically a reference deep down, even +_mostly_ like regular state, you do have to perform some extra steps to mutate shared state. +This is because shared state is technically a reference deep down, even though we take extra steps to make it appear value-like. And this means it's possible to mutate the same piece of shared state from multiple threads, and hence race conditions are possible. -To mutate a piece of shared state in an isolated fashion, use the ``Shared/withLock(_:)`` method +To mutate a piece of shared state in an isolated fashion, use the `withLock` method defined on the `@Shared` projected value: ```swift -await state.$count.withLock { $0 += 1 } +state.$count.withLock { $0 += 1 } ``` That locks the entire unit of work of reading the current count, incrementing it, and storing it @@ -1019,36 +986,14 @@ Technically it is still possible to write code that has race conditions, such as ```swift let currentCount = state.count -await state.$count.withLock { $0 = currentCount + 1 } +state.$count.withLock { $0 = currentCount + 1 } ``` But there is no way to 100% prevent race conditions in code. Even actors are susceptible to problems due to re-entrancy. To avoid problems like the above we recommend wrapping as many -mutations of the shared state as possible in a single ``Shared/withLock(_:)``. That will make +mutations of the shared state as possible in a single `withLock`. That will make sure that the full unit of work is guarded by a lock. -> Note: You may encounter a deprecation warning when simply _accessing_ shared state from an -> asynchronous context when you chain into a subscript: -> -> ```swift -> return .run { _ in -> @Shared(.posts) var posts -> let post = posts[id: id] // ⚠️ Setter is unavailable from asynchronous contexts -> // ... -> } -> ``` -> -> This is a [known issue](https://github.com/apple/swift/issues/74203) in the Swift compiler, but -> can be worked around using ``Shared/withLock(_:)`` to access the underlying value instead: -> -> ```swift -> return .run { _ in -> @Shared(.posts) var posts -> let post = await $posts.withLock { $0[id: id] } -> // ... -> } -> ``` - ## Gotchas of @Shared There are a few gotchas to be aware of when using shared state in the Composable Architecture. @@ -1070,9 +1015,8 @@ own implementations of `encode(to:)` and `init(from:)` that do the appropriate t For example, if the data type is sharing state with a persistence strategy, you can decode by delegating to the memberwise initializer that implicitly loads the shared value from the property -wrapper's persistence strategy, or you can explicitly initialize a shared value via -``Shared/init(wrappedValue:_:fileID:line:)-9kfmy``. And for encoding you can often skip encoding -the shared value: +wrapper's persistence strategy, or you can explicitly initialize a shared value. And for encoding +you can often skip encoding the shared value: ```swift struct AppState { @@ -1162,28 +1106,3 @@ Alternatively you can take an extra step to override shared state in your previe ``` The second assignment of `isOn` will guarantee that it holds a value of `true`. - -## Topics - -### Essentials - -- ``Shared`` - -### Persistence strategies - -- ``AppStorageKey`` -- ``FileStorageKey`` -- ``InMemoryKey`` - -### Custom persistence - -- ``PersistenceKey`` - -### Read-only persistence - -- ``SharedReader`` -- ``PersistenceReaderKey`` - -### Default values - -- ``PersistenceKeyDefault`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/AppStorageKey.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/AppStorageKey.md deleted file mode 100644 index 18f7749b9575..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/AppStorageKey.md +++ /dev/null @@ -1,34 +0,0 @@ -# ``ComposableArchitecture/AppStorageKey`` - -## Topics - -### Storing a value - -- ``PersistenceReaderKey/appStorage(_:)-4l5b`` -- ``PersistenceReaderKey/appStorage(_:)-6d47p`` -- ``PersistenceReaderKey/appStorage(_:)-6tsph`` -- ``PersistenceReaderKey/appStorage(_:)-69h4r`` -- ``PersistenceReaderKey/appStorage(_:)-xphy`` -- ``PersistenceReaderKey/appStorage(_:)-617ld`` -- ``PersistenceReaderKey/appStorage(_:)-6k27r`` -- ``PersistenceReaderKey/appStorage(_:)-m54v`` - -### Storing an optional value - -- ``PersistenceReaderKey/appStorage(_:)-4s3s5`` -- ``PersistenceReaderKey/appStorage(_:)-2dfnh`` -- ``PersistenceReaderKey/appStorage(_:)-5wv8g`` -- ``PersistenceReaderKey/appStorage(_:)-40e42`` -- ``PersistenceReaderKey/appStorage(_:)-4veqp`` -- ``PersistenceReaderKey/appStorage(_:)-7rox5`` -- ``PersistenceReaderKey/appStorage(_:)-2cfq9`` -- ``PersistenceReaderKey/appStorage(_:)-9j150`` - -### Key-path access - -- ``PersistenceReaderKey/appStorage(_:)-69h4r`` -- ``AppStorageKeyPathKey`` - -### Overriding app storage - -- ``Dependencies/DependencyValues/defaultAppStorage`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/FileStorageKey.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/FileStorageKey.md deleted file mode 100644 index 439a152e0322..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/FileStorageKey.md +++ /dev/null @@ -1,17 +0,0 @@ -# ``ComposableArchitecture/FileStorageKey`` - -## Topics - -### Storing a value - -- ``PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` - -### Overriding storage - -- ``Dependencies/DependencyValues/defaultFileStorage`` -- ``FileStorage`` - -### Deprecations - -- ``LiveFileStorage()`` -- ``InMemoryFileStorage()`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/InMemoryKey.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/InMemoryKey.md deleted file mode 100644 index 882b9b1a7597..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/InMemoryKey.md +++ /dev/null @@ -1,12 +0,0 @@ -# ``ComposableArchitecture/InMemoryKey`` - -## Topics - -### Storing a value - -- ``PersistenceReaderKey/inMemory(_:)`` - -### Overriding storage - -- ``Dependencies/DependencyValues/defaultInMemoryStorage`` -- ``InMemoryStorage`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Reducer.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Reducer.md index 5c1a26bac4d1..f471139f2e1c 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Reducer.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Reducer.md @@ -584,12 +584,6 @@ xcodebuild -skipMacroValidation … - ``forEach(_:action:element:fileID:filePath:line:column:)-6zye8`` - -### Sharing state - -- -- ``Shared`` -- ``PersistenceKey`` - ### Supporting reducers - ``EmptyReducer`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Shared.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Shared.md deleted file mode 100644 index ced8205c5144..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Shared.md +++ /dev/null @@ -1,44 +0,0 @@ -# ``ComposableArchitecture/Shared`` - -## Topics - -### Creating a shared value - -- ``init(_:fileID:line:)-9d3q`` -- ``init(_:)`` -- ``init(projectedValue:)`` - -### Creating a persisted value - -- ``init(wrappedValue:_:fileID:line:)-9kfmy`` -- ``init(wrappedValue:_:fileID:line:)-7ndwc`` -- ``init(_:fileID:line:)-8zcy1`` -- ``init(_:fileID:line:)-8jqg5`` -- ``init(_:fileID:line:)-9d3q`` -- ``init(_:fileID:line:)-1q6ev`` - -### Accessing the value - -- ``wrappedValue`` -- ``projectedValue`` -- ``reader`` -- ``subscript(dynamicMember:)-6dq81`` -- ``subscript(dynamicMember:)-7n9xc`` -- ``subscript(dynamicMember:)-6f2x`` -- ``subscript(dynamicMember:)-9xw64`` - -### Isolating the value - -- ``withLock(_:)`` - -### Unit testing the value - -- ``assert(_:fileID:file:line:column:)`` - -### SwiftUI integration - -- ``elements`` - -### Combine integration - -- ``publisher`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/SharedReader.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/SharedReader.md deleted file mode 100644 index bcac6b355642..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/SharedReader.md +++ /dev/null @@ -1,33 +0,0 @@ -# ``ComposableArchitecture/SharedReader`` - -## Topics - -### Creating a shared value - -- ``init(_:)-3a38z`` -- ``init(_:)-42f43`` -- ``init(projectedValue:)`` -- ``constant(_:)`` - -### Creating a persisted value - -- ``init(wrappedValue:_:fileID:line:)-7f68o`` -- ``init(wrappedValue:_:fileID:line:)-galu`` -- ``init(_:fileID:line:)-41rb8`` -- ``init(_:fileID:line:)-3lxyf`` -- ``init(_:fileID:line:)-5bxk6`` - -### Getting the value - -- ``wrappedValue`` -- ``projectedValue`` -- ``subscript(dynamicMember:)-pigs`` -- ``subscript(dynamicMember:)-2barb`` - -### SwiftUI integration - -- ``elements`` - -### Combine integration - -- ``publisher`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps-01-code-0004.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps-01-code-0004.swift index b5cac8dfa36a..ce4d051ec77d 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps-01-code-0004.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps-01-code-0004.swift @@ -51,12 +51,8 @@ struct SyncUpsList { } } -extension PersistenceReaderKey -where Self == PersistenceKeyDefault>> { +extension SharedKey where Self == FileStorageKey>.Default { static var syncUps: Self { - PersistenceKeyDefault( - .fileStorage(.documentsDirectory.appending(component: "sync-ups.json")), - [] - ) + Self[.fileStorage(.documentsDirectory.appending(component: "sync-ups.json")), default: []] } } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps.tutorial b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps.tutorial index a514ab57c8a6..1393abc4816b 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps.tutorial +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/05-PersistingSyncUps/PersistingSyncUps.tutorial @@ -1,20 +1,20 @@ @Tutorial(time: 5) { @Intro(title: "Persisting sync-ups") { Now that we have the ability to add and remove sync-ups from the application, let's add some - persistence. This will involve using the "shared state" tools from the library, such as - [`@Shared`]() and ``ComposableArchitecture/PersistenceReaderKey/fileStorage(_:decoder:encoder:)``. + persistence. This will involve using the "shared state" tools from the library, such as + `@Shared` and `fileStorage`. } @Section(title: "Persisting data to disk") { @ContentAndMedia { - To persist state to an external system you must make use of the [`@Shared`]() - property wrapper with a persistence strategy. + To persist state to an external system you must make use of the `@Shared` property wrapper + with a persistence strategy. } @Steps { @Step { - Go back to the SyncUpsList.swift file, and start by applying the [`@Shared`]() - property wrapper to the `syncUps` field. + Go back to the SyncUpsList.swift file, and start by applying the `@Shared` property wrapper + to the `syncUps` field. > Note: Our changes will not compile right now, but they will soon. @@ -27,30 +27,28 @@ information on all of the various strategies. @Step { - Further customize the [`@Shared`]() property wrapper with a persistence - strategy. The library comes with a few strategies you can use, but the most appropriate here - is ``ComposableArchitecture/PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` since we are trying to - save a complex data structure. + Further customize the `@Shared` property wrapper with a persistence strategy. The library + comes with a few strategies you can use, but the most appropriate here is `fileStorage` + since we are trying to save a complex data structure. @Code(name: "SyncUpsList.swift", file: PersistingSyncUps-01-code-0002.swift) } - The ``ComposableArchitecture/PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` persistence strategy saves - data to disk anytime the value in [`@Shared`]() changes, but it spaces the saves - out a bit so as to not thrash the file system with every single change. + The `fileStorage` persistence strategy saves data to disk anytime the value in `@Shared` + changes, but it spaces the saves out a bit so as to not thrash the file system with every + single change. @Step { - The ``ComposableArchitecture/PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` persistence strategy + The `fileStorage` persistence strategy needs to be provided a URL for where to save the data. Add an extension to `URL` at the bottom of the file to define such a URL, and then provide it to the `.fileStorage` value. @Code(name: "SyncUpsList.swift", file: PersistingSyncUps-01-code-0003.swift) } - With that change the project should be compiling. It is worth noting that - ``ComposableArchitecture/PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` only works with `Codable` data - types, and earlier in the tutorial when we added models to Models.swift we made them codable from - the beginning. + With that change the project should be compiling. It is worth noting that `fileStorage` only + works with `Codable` data types, and earlier in the tutorial when we added models to + Models.swift we made them codable from the beginning. @Step { Before moving on, we can still make this better. @@ -86,8 +84,8 @@ @Section(title: "Testing persistence") { @ContentAndMedia { - Testing state that is held in [`@Shared`]() with `.fileStorage` persistence works - exactly like regular state without [`@Shared`](). The + Testing state that is held in `@Shared` with `.fileStorage` persistence works + exactly like regular state without `@Shared`. The ``ComposableArchitecture/TestStore`` forces you to exhaustively prove how all state changes. } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0005-previous.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0005-previous.swift index 0c652db55e95..90f5c13c5ba1 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0005-previous.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0005-previous.swift @@ -84,7 +84,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0005.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0005.swift index e3b605e935da..fa9c8dbdbe77 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0005.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0005.swift @@ -84,7 +84,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0006.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0006.swift index b527500f7586..60478b929b35 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0006.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0006.swift @@ -89,7 +89,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0007.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0007.swift index 6998b6b3b15e..2a54fe1d1050 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0007.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0007.swift @@ -100,7 +100,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0008.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0008.swift index 27b24954b5ff..dae342adaf98 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0008.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-01-code-0008.swift @@ -101,7 +101,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-02-code-0014-previous.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-02-code-0014-previous.swift index 758892711fed..51f6caeafc7c 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-02-code-0014-previous.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-02-code-0014-previous.swift @@ -102,7 +102,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-02-code-0014.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-02-code-0014.swift index 7820572efb3a..110b7ab04863 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-02-code-0014.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-02-code-0014.swift @@ -103,7 +103,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-03-code-0013-previous.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-03-code-0013-previous.swift index 7820572efb3a..110b7ab04863 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-03-code-0013-previous.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-03-code-0013-previous.swift @@ -103,7 +103,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-03-code-0013.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-03-code-0013.swift index 5cfdb06bd076..2a1b5d6a865e 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-03-code-0013.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp-03-code-0013.swift @@ -105,7 +105,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp.tutorial b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp.tutorial index 04ae2989f7f7..0b5a58395c2f 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp.tutorial +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/EditingAndDeletingSyncUp.tutorial @@ -250,19 +250,17 @@ have in state from the collection of sync-ups that is in the `SyncUpsList` feature. However, remember that in we showed how to persist the collection - of sync-ups using the [`@Shared`]() property wrapper with the - ``ComposableArchitecture/PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` persistence strategy. This - gives the app _global_ access to that state, and we can make edits to it from anywhere. We can - even do it directly inline in the `.confirmButtonTapped` action. + of sync-ups using the `@Shared` property wrapper with the `fileStorage` persistence strategy. + This gives the app _global_ access to that state, and we can make edits to it from anywhere. + We can even do it directly inline in the `.confirmButtonTapped` action. @Step { - Use the ``Shared`` property wrapper with the - ``ComposableArchitecture/PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` persistence strategy to get - a reference to the sync-ups loaded from disk. + Use the `@Shared` property wrapper with the `fileStorage` persistence strategy to get a + reference to the sync-ups loaded from disk. > Note: This does not actually load data from disk. The data has already been loaded from - > disk and cached in the [`@Shared`]() reference. This is only giving us access - > to that reference. + > disk and cached in the `@Shared` reference. This is only giving us access to that + > reference. @Code(name: "SyncUpDetail.swift", file: EditingAndDeletingSyncUp-02-code-0010.swift) } @@ -278,7 +276,7 @@ you that we are reaching out to a seemingly global `syncUps` variable and mutating it. However, this is no different than making an API request to delete data on some external server. Typically for that situation we use dependencies to make the API operation testable, - but we don't need to do that with [`@Shared`](). It is testable by default. + but we don't need to do that with `@Shared`. It is testable by default. If it truly bothers you to access the global `syncUps` state from within the detail feature, then you can instead send a "delegate" action from the detail that the parent feature diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/SyncUpDetail-01-code-0007.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/SyncUpDetail-01-code-0007.swift index 0c652db55e95..90f5c13c5ba1 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/SyncUpDetail-01-code-0007.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/SyncUpDetail-01-code-0007.swift @@ -84,7 +84,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/SyncUpDetail.tutorial b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/SyncUpDetail.tutorial index af7c781f4edf..815b3c8f49e6 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/SyncUpDetail.tutorial +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/SyncUpDetail.tutorial @@ -25,10 +25,10 @@ Add a `syncUp` field to the `State` struct since we need that data to populate the UI. There will be more state in this feature later, but we only need a `SyncUp` for now. - Further, this field will be annotated with the [`@Shared`]() property - wrapper in order to indicate that state is shared with another feature, in particular - the `SyncUpsList` feature. This will make it possible for this feature to make edits - to the state, and for the data to automatically be updated in `SyncUpsList.State`. + Further, this field will be annotated with the `@Shared` property wrapper in order to + indicate that state is shared with another feature, in particular the `SyncUpsList` feature. + This will make it possible for this feature to make edits to the state, and for the data to + automatically be updated in `SyncUpsList.State`. > Note: See the article for more information about sharing state and persistence strategies. diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0002.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0002.swift index 681d5e980702..6a087c7f00df 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0002.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0002.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0003.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0003.swift index e51aafd2bf04..61368c85651c 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0003.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0003.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0004.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0004.swift index b58582486f3e..4d5674170f41 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0004.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0004.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0005.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0005.swift index d67a10210dcd..ab7add5665b4 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0005.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0005.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0006.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0006.swift index 722b177a51d4..6cee1f425837 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0006.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-01-code-0006.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0001.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0001.swift index c04801a68260..31432d9f7833 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0001.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0001.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0002.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0002.swift index ff5c23eb9c7f..f0a0a536ae7a 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0002.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0002.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0003.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0003.swift index 3b171cdaa30d..2d7ba88ec7b4 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0003.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0003.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0004.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0004.swift index f7b2fa931e4f..6d64fd075fb1 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0004.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0004.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0005.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0005.swift index 5567108e5382..e2173b345535 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0005.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0005.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0006.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0006.swift index 5567108e5382..e2173b345535 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0006.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0006.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0007.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0007.swift index 5997506a9665..d442dd4ac9c9 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0007.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/06-SyncUpDetail/TestingSyncUpDetail-02-code-0007.swift @@ -11,7 +11,7 @@ struct SyncUpDetailTests { id: SyncUp.ID(), title: "Point-Free Morning Sync" ) - let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(syncUp))) { + let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) { SyncUpDetail() } withDependencies: { } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/MeetingNavigation-02-code-0004-previous.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/MeetingNavigation-02-code-0004-previous.swift index 4086bb50fcfa..eef78095e680 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/MeetingNavigation-02-code-0004-previous.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/MeetingNavigation-02-code-0004-previous.swift @@ -104,7 +104,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/MeetingNavigation-02-code-0004.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/MeetingNavigation-02-code-0004.swift index 0bd2157692bf..6cae4d94585a 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/MeetingNavigation-02-code-0004.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/MeetingNavigation-02-code-0004.swift @@ -106,7 +106,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation-03-code-0003.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation-03-code-0003.swift index c9d7ebed5381..c9230fd31cfd 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation-03-code-0003.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation-03-code-0003.swift @@ -11,7 +11,7 @@ struct SyncUpsListView: View { var body: some View { List { - ForEach(store.$syncUps.elements) { $syncUp in + ForEach(store.$syncUps) { $syncUp in NavigationLink( state: AppFeature.Path.State.detail(SyncUpDetail.State(syncUp: <#Shared#>)) ) { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation-03-code-0004.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation-03-code-0004.swift index 0ab0096da0e1..cf897f2f41a6 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation-03-code-0004.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation-03-code-0004.swift @@ -11,7 +11,7 @@ struct SyncUpsListView: View { var body: some View { List { - ForEach(store.$syncUps.elements) { $syncUp in + ForEach(store.$syncUps) { $syncUp in NavigationLink( state: AppFeature.Path.State.detail(SyncUpDetail.State(syncUp: $syncUp)) ) { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation.tutorial b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation.tutorial index 92998c84a134..96a5b757017b 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation.tutorial +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/SyncUpDetailNavigation.tutorial @@ -234,13 +234,12 @@ detail feature can be allowed to make edits to the state have those edits be automatically made to `SyncUpsList.State`. - One can use the projected value of [`@Shared`]() along with the - ``ComposableArchitecture/Shared/elements`` property defined on shared collections in order to - derive a `Shared` value for each element in the collection. + One can use the projected value of `@Shared` in order to derive a `Shared` value for each + element in the collection. @Step { - Use the ``ComposableArchitecture/Shared/elements`` property in the `ForEach` to derive a - `Shared` for each element in the `Shared>`. + Pass the shared collection to the `ForEach` to derive a `Shared` for each element in + the `Shared>`. @Code(name: "SyncUpsList.swift", file: SyncUpDetailNavigation-03-code-0003.swift) } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/TestingNavigation.tutorial b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/TestingNavigation.tutorial index 1a9aa0cf0e9a..d45f8adf03ea 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/TestingNavigation.tutorial +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/07-SyncUpDetailNavigation/TestingNavigation.tutorial @@ -26,11 +26,11 @@ Typically we would start a new test by constructing a ``ComposableArchitecture/TestStore`` that holds onto the feature's initial state. However, this time the initial state is seeded by shared state, which is also persisted to disk. For this reason it is easier to use the - [`@Shared`]() property wrapper directly to set the initial state of the sync-ups. + `@Shared` property wrapper directly to set the initial state of the sync-ups. @Step { - Use the [`@Shared`]() property wrapper with the `.syncUps` persistence strategy - we defined earlier to initialize the state with a single sync-up. + Use the `@Shared` property wrapper with the `.syncUps` persistence strategy we defined + earlier to initialize the state with a single sync-up. @Code(name: "AppFeatureTests.swift", file: TestingNavigation-01-code-0002.swift) } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0001-previous.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0001-previous.swift index 2f5d2f0ffda1..4906f0130e7e 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0001-previous.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0001-previous.swift @@ -212,7 +212,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0001.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0001.swift index e43780c753d2..c47b8f1b58ae 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0001.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0001.swift @@ -214,7 +214,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0002.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0002.swift index f68ec27a36a7..8c38b61e48fd 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0002.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0002.swift @@ -229,7 +229,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0003.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0003.swift index bdbeb7a8cb08..46529019f627 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0003.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0003.swift @@ -230,7 +230,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0004.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0004.swift index d05af497524b..b268ee4e6cdc 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0004.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0004.swift @@ -233,7 +233,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0005.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0005.swift index 544311234100..41ffa432a71e 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0005.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0005.swift @@ -234,7 +234,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0006.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0006.swift index e514dbfe92b7..66efbed27a0f 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0006.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0006.swift @@ -238,7 +238,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0007.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0007.swift index 57d88055f736..1fef0c3b869f 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0007.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0007.swift @@ -239,7 +239,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0008.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0008.swift index 0f4da046f1fb..b45fa2752d40 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0008.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0008.swift @@ -243,7 +243,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0009.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0009.swift index 6d0d391baa7e..c27ff94f0347 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0009.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0009.swift @@ -249,7 +249,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0010.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0010.swift index a2a33d959ecb..b23bb1ff1725 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0010.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-01-code-0010.swift @@ -252,7 +252,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0001-previous.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0001-previous.swift index a2a33d959ecb..b23bb1ff1725 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0001-previous.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0001-previous.swift @@ -252,7 +252,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0001.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0001.swift index 3d172cf4c1be..b0a3f4230ac3 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0001.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0001.swift @@ -254,7 +254,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0002.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0002.swift index b88604931034..2bda7b54d4a1 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0002.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0002.swift @@ -254,7 +254,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0003.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0003.swift index 9cd49fee3a92..dd1bbe16b537 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0003.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0003.swift @@ -255,7 +255,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0004.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0004.swift index 4de321cb134d..0eacfe9bcfe9 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0004.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-02-code-0004.swift @@ -254,7 +254,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0001-previous.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0001-previous.swift index 4de321cb134d..0eacfe9bcfe9 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0001-previous.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0001-previous.swift @@ -254,7 +254,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0001.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0001.swift index 3e071523667e..2de09ec9e37f 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0001.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0001.swift @@ -262,7 +262,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0002.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0002.swift index 617c403393dd..56fc49194d33 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0002.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0002.swift @@ -263,7 +263,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0003.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0003.swift index 7196923f7239..87ec1166fb07 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0003.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0003.swift @@ -269,7 +269,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0004.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0004.swift index 8cfdbc4a2c15..aa2880c1b931 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0004.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0004.swift @@ -270,7 +270,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0005.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0005.swift index c109fe671f58..924dfb79ba3e 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0005.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0005.swift @@ -271,7 +271,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0006.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0006.swift index 58ac9f0203e6..983e84498330 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0006.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0006.swift @@ -291,7 +291,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0007.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0007.swift index 8ed578ad4bd1..28b43cc01d86 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0007.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0007.swift @@ -304,7 +304,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0008.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0008.swift index a889e5106e03..3a514b83d0cd 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0008.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-03-code-0008.swift @@ -307,7 +307,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0002.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0002.swift index a25213a97583..0409d73b1e9d 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0002.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0002.swift @@ -17,7 +17,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0003.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0003.swift index 6362ee0658b3..c34e21fb4f86 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0003.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0003.swift @@ -17,7 +17,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0004.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0004.swift index 059b43fe3247..8bb8e4e2b3dc 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0004.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0004.swift @@ -17,7 +17,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0005.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0005.swift index cae29c826dd7..9451f20b120b 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0005.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0005.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0006.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0006.swift index f1dc6ad09103..3e408e374e30 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0006.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0006.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0007.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0007.swift index 1d4b1e441609..fd6f18823a76 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0007.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0007.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0008.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0008.swift index 436e533a9a45..bc8d278b360e 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0008.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0008.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0009.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0009.swift index ffbc81b49938..98c492832ece 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0009.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0009.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0010.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0010.swift index 56a2d36e8d14..b16200905d37 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0010.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0010.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0011.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0011.swift index 03c7e4ba5b67..5d228008b43f 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0011.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0011.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0012.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0012.swift index d901ff969825..b52f85ed9f4d 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0012.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0012.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0013.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0013.swift index a41344c33e16..a49164efdc75 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0013.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0013.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0014.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0014.swift index 6f5224c426cc..d3c5097a06fd 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0014.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0014.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0015.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0015.swift index f5defd5cd00e..49eaa4e7ac24 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0015.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0015.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0016.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0016.swift index fbbdae119803..682f7ce66bc9 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0016.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0016.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0017.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0017.swift index 6d500355dbec..7c1f6239ea5d 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0017.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0017.swift @@ -18,7 +18,7 @@ struct RecordMeetingTests { title: "Morning Sync" ) let store = TestStore( - initialState: RecordMeeting.State(syncUp: Shared(syncUp)) + initialState: RecordMeeting.State(syncUp: Shared(value: syncUp)) ) { RecordMeeting() } withDependencies: { diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer.tutorial b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer.tutorial index 1572984887de..faff98a50715 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer.tutorial +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer.tutorial @@ -113,8 +113,7 @@ Let's run the feature in the Xcode preview to make sure it works. However, we don't want to wait for the full sync-up duration to pass just to see how this feature behaves. It would be far better if we could stub in a sync-up that has a particularly short duration to make it - easier to preview. Luckily we can do that easily with the [`@Shared`]() property - wrapper. + easier to preview. Luckily we can do that easily with the `@Shared` property wrapper. @Step { Go to the preview at the bottom of the AppFeature.swift file and override the shared diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-01-code-0003.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-01-code-0003.swift index 2f5d2f0ffda1..4906f0130e7e 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-01-code-0003.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-01-code-0003.swift @@ -212,7 +212,7 @@ struct MeetingFooterView: View { #Preview { NavigationStack { RecordMeetingView( - store: Store(initialState: RecordMeeting.State(syncUp: Shared(.mock))) { + store: Store(initialState: RecordMeeting.State(syncUp: Shared(value: .mock))) { RecordMeeting() } ) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-02-code-0003-previous.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-02-code-0003-previous.swift index 0bd2157692bf..6cae4d94585a 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-02-code-0003-previous.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-02-code-0003-previous.swift @@ -106,7 +106,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-02-code-0003.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-02-code-0003.swift index 41e95599f667..72a7917e6a2d 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-02-code-0003.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/RecordMeetingFeature-02-code-0003.swift @@ -107,7 +107,7 @@ struct SyncUpDetailView: View { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( - syncUp: Shared(.mock) + syncUp: Shared(value: .mock) ) ) { SyncUpDetail() diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/01-YourFirstPresentation/02-01-YourFirstPresentation.tutorial b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/01-YourFirstPresentation/02-01-YourFirstPresentation.tutorial index 8ace123d86fd..fcbf3b1cc301 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/01-YourFirstPresentation/02-01-YourFirstPresentation.tutorial +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/01-YourFirstPresentation/02-01-YourFirstPresentation.tutorial @@ -258,10 +258,10 @@ > Important: Delegate actions are the most general way of communicating from the child domain back to the parent, but there are other techniques. We could have also utilized the - [`@Shared`]() property wrapper for the collection of sync ups, which would allow - the `AddContactFeature` to insert a new contact directly into the parent collection without - any further steps. This can be powerful, but we will use delegate actions for this tutorial. - To read more about `@Shared` see the article, and see the + `@Shared` property wrapper for the collection of sync ups, which would allow the + `AddContactFeature` to insert a new contact directly into the parent collection without any + further steps. This can be powerful, but we will use delegate actions for this tutorial. To + read more about `@Shared` see the article, and see the tutorial where we use this technique, in particular in section. diff --git a/Sources/ComposableArchitecture/Internal/Exports.swift b/Sources/ComposableArchitecture/Internal/Exports.swift index e67b07d659fe..e171ce657694 100644 --- a/Sources/ComposableArchitecture/Internal/Exports.swift +++ b/Sources/ComposableArchitecture/Internal/Exports.swift @@ -8,5 +8,6 @@ @_exported import IdentifiedCollections @_exported import Observation @_exported import Perception +@_exported import Sharing @_exported import SwiftUINavigation @_exported import UIKitNavigation diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/DebugReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/DebugReducer.swift index f738d1995295..ec9fb655188d 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/DebugReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/DebugReducer.swift @@ -1,5 +1,6 @@ import Combine import Dispatch +@_spi(SharedChangeTracking) import Sharing extension Reducer { /// Enhances a reducer with debug logging of received actions and state mutations for the given @@ -85,7 +86,8 @@ public struct _PrintChangesReducer: Reducer { into state: inout Base.State, action: Base.Action ) -> Effect { if let printer = self.printer { - return withSharedChangeTracking { changeTracker in + let changeTracker = SharedChangeTracker() + return changeTracker.track { let oldState = UncheckedSendable(state) let effects = self.base.reduce(into: &state, action: action) return withEscapedDependencies { continuation in diff --git a/Sources/ComposableArchitecture/SharedState/PersistenceKey.swift b/Sources/ComposableArchitecture/SharedState/PersistenceKey.swift deleted file mode 100644 index 13d008a580bd..000000000000 --- a/Sources/ComposableArchitecture/SharedState/PersistenceKey.swift +++ /dev/null @@ -1,95 +0,0 @@ -/// A type that can load and subscribe to state in an external system. -/// -/// Conform to this protocol to express loading state from an external system, and subscribing to -/// state changes in the external system. It is only necessary to conform to this protocol if the -/// ``AppStorageKey``, ``FileStorageKey``, or ``InMemoryKey`` strategies are not sufficient for your -/// use case. -/// -/// See the article for more information, in particular the -/// section. -public protocol PersistenceReaderKey: Sendable { - /// A type that can be loaded or subscribed to in an external system. - associatedtype Value: Sendable - - /// A type representing the hashable identity of a persistence key. - associatedtype ID: Hashable = Self - - /// The hashable identity of a persistence key. - /// - /// Used to look up existing shared references associated with this persistence key. - var id: ID { get } - - /// Loads the freshest value from storage. Returns `nil` if there is no value in storage. - /// - /// - Parameter initialValue: An initial value assigned to the `@Shared` property. - /// - Returns: An initial value provided by an external system, or `nil`. - func load(initialValue: Value?) -> Value? - - /// Subscribes to external updates. - /// - /// - Parameters: - /// - initialValue: An initial value assigned to the `@Shared` property. - /// - didSet: A closure that is invoked with new values from an external system, or `nil` if the - /// external system no longer holds a value. - /// - Returns: A subscription to updates from an external system. If it is cancelled or - /// deinitialized, the `didSet` closure will no longer be invoked. - func subscribe( - initialValue: Value?, - didSet: @escaping @Sendable (_ newValue: Value?) -> Void - ) -> Shared.Subscription -} - -extension PersistenceReaderKey where ID == Self { - public var id: ID { self } -} - -extension PersistenceReaderKey { - public func subscribe( - initialValue: Value?, - didSet: @escaping @Sendable (_ newValue: Value?) -> Void - ) -> Shared.Subscription { - Shared.Subscription {} - } -} - -/// A type that can persist shared state to an external storage. -/// -/// Conform to this protocol to express persistence to some external storage by describing how to -/// save to and load from the external storage, and providing a stream of values that represents -/// when the external storage is changed from the outside. It is only necessary to conform to this -/// protocol if the ``AppStorageKey``, ``FileStorageKey``, or ``InMemoryKey`` strategies are not -/// sufficient for your use case. -/// -/// See the article for more information, in particular the -/// section. -public protocol PersistenceKey: PersistenceReaderKey { - /// Saves a value to storage. - func save(_ value: Value) -} - -extension Shared { - /// A subscription to a ``PersistenceReaderKey``'s updates. - /// - /// This object is returned from ``PersistenceReaderKey/subscribe(initialValue:didSet:)``, which - /// will feed updates from an external system for its lifetime, or till ``cancel()`` is called. - public class Subscription { - var onCancel: (() -> Void)? - - /// Initializes the subscription with the given cancel closure. - /// - /// - Parameter cancel: A closure that the `cancel()` method executes. - public init(_ cancel: @escaping () -> Void) { - self.onCancel = cancel - } - - deinit { - self.cancel() - } - - /// Cancels the subscription. - public func cancel() { - self.onCancel?() - self.onCancel = nil - } - } -} diff --git a/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKey.swift b/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKey.swift deleted file mode 100644 index e97547d39e06..000000000000 --- a/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKey.swift +++ /dev/null @@ -1,521 +0,0 @@ -import Dependencies -import Foundation - -extension PersistenceReaderKey { - /// Creates a persistence key that can read and write to a boolean user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to an integer user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to a double user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to a string user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to a URL user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to a Date user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to a user default as data. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to an integer user default, transforming - /// that to a `RawRepresentable` data type. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage>(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to a string user default, transforming - /// that to a `RawRepresentable` data type. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage>(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to an optional boolean user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to an optional integer user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to an optional double user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to an optional string user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to an optional URL user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to an optional Date user default. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to a user default as optional data. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to an optional integer user default, - /// transforming that to a `RawRepresentable` data type. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Value.RawValue == Int, Self == AppStorageKey { - AppStorageKey(key) - } - - /// Creates a persistence key that can read and write to an optional string user default, - /// transforming that to a `RawRepresentable` data type. - /// - /// - Parameter key: The key to read and write the value to in the user defaults store. - /// - Returns: A user defaults persistence key. - public static func appStorage(_ key: String) -> Self - where Value.RawValue == String, Self == AppStorageKey { - AppStorageKey(key) - } -} - -/// A type defining a user defaults persistence strategy. -/// -/// See ``PersistenceReaderKey/appStorage(_:)-4l5b`` to create values of this type. -public struct AppStorageKey: Sendable { - private let lookup: any Lookup - private let key: String - private let store: UncheckedSendable - - public var id: AnyHashable { - AppStorageKeyID(key: self.key, store: self.store.wrappedValue) - } - - fileprivate init(_ key: String) where Value == Bool { - @Dependency(\.defaultAppStorage) var store - self.lookup = CastableLookup() - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == Int { - @Dependency(\.defaultAppStorage) var store - self.lookup = CastableLookup() - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == Double { - @Dependency(\.defaultAppStorage) var store - self.lookup = CastableLookup() - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == String { - @Dependency(\.defaultAppStorage) var store - self.lookup = CastableLookup() - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == URL { - @Dependency(\.defaultAppStorage) var store - self.lookup = URLLookup() - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == Date { - @Dependency(\.defaultAppStorage) var store - self.lookup = CastableLookup() - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == Data { - @Dependency(\.defaultAppStorage) var store - self.lookup = CastableLookup() - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value: RawRepresentable { - @Dependency(\.defaultAppStorage) var store - self.lookup = RawRepresentableLookup(base: CastableLookup()) - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value: RawRepresentable { - @Dependency(\.defaultAppStorage) var store - self.lookup = RawRepresentableLookup(base: CastableLookup()) - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == Bool? { - @Dependency(\.defaultAppStorage) var store - self.lookup = OptionalLookup(base: CastableLookup()) - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == Int? { - @Dependency(\.defaultAppStorage) var store - self.lookup = OptionalLookup(base: CastableLookup()) - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == Double? { - @Dependency(\.defaultAppStorage) var store - self.lookup = OptionalLookup(base: CastableLookup()) - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == String? { - @Dependency(\.defaultAppStorage) var store - self.lookup = OptionalLookup(base: CastableLookup()) - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == URL? { - @Dependency(\.defaultAppStorage) var store - self.lookup = OptionalLookup(base: URLLookup()) - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == Date? { - @Dependency(\.defaultAppStorage) var store - self.lookup = OptionalLookup(base: CastableLookup()) - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init(_ key: String) where Value == Data? { - @Dependency(\.defaultAppStorage) var store - self.lookup = OptionalLookup(base: CastableLookup()) - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init>(_ key: String) where Value == R? { - @Dependency(\.defaultAppStorage) var store - self.lookup = OptionalLookup(base: RawRepresentableLookup(base: CastableLookup())) - self.key = key - self.store = UncheckedSendable(store) - } - - fileprivate init>(_ key: String) where Value == R? { - @Dependency(\.defaultAppStorage) var store - self.lookup = OptionalLookup(base: RawRepresentableLookup(base: CastableLookup())) - self.key = key - self.store = UncheckedSendable(store) - } -} - -extension AppStorageKey: PersistenceKey { - public func load(initialValue: Value?) -> Value? { - self.lookup.loadValue(from: self.store.wrappedValue, at: self.key, default: initialValue) - } - - public func save(_ value: Value) { - self.lookup.saveValue(value, to: self.store.wrappedValue, at: self.key) - } - - public func subscribe( - initialValue: Value?, - didSet: @escaping @Sendable (_ newValue: Value?) -> Void - ) -> Shared.Subscription { - let previousValue = LockIsolated(initialValue) - let removeObserver: () -> Void - if key.hasPrefix("@") || key.contains(".") { - let userDefaultsDidChange = NotificationCenter.default.addObserver( - forName: UserDefaults.didChangeNotification, - object: store.wrappedValue, - queue: .main - ) { _ in - let newValue = load(initialValue: initialValue) - defer { previousValue.withValue { $0 = newValue } } - func isEqual(_ lhs: T, _ rhs: T) -> Bool? { - func open(_ lhs: U) -> Bool { - lhs == rhs as? U - } - guard let lhs = lhs as? any Equatable else { return nil } - return open(lhs) - } - guard - !(isEqual(newValue, previousValue.value) ?? false) - || (isEqual(newValue, initialValue) ?? true) - else { - return - } - guard !SharedAppStorageLocals.isSetting - else { return } - didSet(newValue) - } - removeObserver = { NotificationCenter.default.removeObserver(userDefaultsDidChange) } - } else { - let observer = Observer { - guard !SharedAppStorageLocals.isSetting - else { return } - didSet(load(initialValue: initialValue)) - } - store.wrappedValue.addObserver(observer, forKeyPath: key, options: .new, context: nil) - removeObserver = { store.wrappedValue.removeObserver(observer, forKeyPath: key) } - } - let willEnterForeground: (any NSObjectProtocol)? - if let willEnterForegroundNotificationName { - willEnterForeground = NotificationCenter.default.addObserver( - forName: willEnterForegroundNotificationName, - object: nil, - queue: nil - ) { _ in - didSet(load(initialValue: initialValue)) - } - } else { - willEnterForeground = nil - } - return Shared.Subscription { - removeObserver() - if let willEnterForeground { - NotificationCenter.default.removeObserver(willEnterForeground) - } - } - } - - private class Observer: NSObject { - let didChange: () -> Void - init(didChange: @escaping () -> Void) { - self.didChange = didChange - super.init() - } - override func observeValue( - forKeyPath keyPath: String?, - of object: Any?, - change: [NSKeyValueChangeKey: Any]?, - context: UnsafeMutableRawPointer? - ) { - self.didChange() - } - } -} - -private struct AppStorageKeyID: Hashable { - let key: String - let store: UserDefaults -} - -extension DependencyValues { - public var defaultAppStorage: UserDefaults { - get { self[DefaultAppStorageKey.self].value } - set { self[DefaultAppStorageKey.self].value = newValue } - } -} - -private enum DefaultAppStorageKey: DependencyKey { - static var testValue: UncheckedSendable { - UncheckedSendable( - UserDefaults( - suiteName: - "\(NSTemporaryDirectory())co.pointfree.ComposableArchitecture.\(UUID().uuidString)" - )! - ) - } - static var previewValue: UncheckedSendable { - Self.testValue - } - static var liveValue: UncheckedSendable { - UncheckedSendable(UserDefaults.standard) - } -} - -// NB: This is mainly used for tests, where observer notifications can bleed across cases. -private enum SharedAppStorageLocals { - @TaskLocal static var isSetting = false -} - -private protocol Lookup: Sendable { - associatedtype Value: Sendable - func loadValue(from store: UserDefaults, at key: String, default defaultValue: Value?) -> Value? - func saveValue(_ newValue: Value, to store: UserDefaults, at key: String) -} - -private struct CastableLookup: Lookup, Sendable { - func loadValue( - from store: UserDefaults, - at key: String, - default defaultValue: Value? - ) -> Value? { - guard let value = store.object(forKey: key) as? Value - else { - guard !SharedAppStorageLocals.isSetting - else { return defaultValue } - - SharedAppStorageLocals.$isSetting.withValue(true) { - store.setValue(defaultValue, forKey: key) - } - return defaultValue - } - return value - } - - func saveValue(_ newValue: Value, to store: UserDefaults, at key: String) { - SharedAppStorageLocals.$isSetting.withValue(true) { - store.setValue(newValue, forKey: key) - } - } -} - -/// Lookup implementation tuned for URL values. -/// For URLs, dedicated UserDefaults APIs for getting/setting need to be called that convert the URL from/to Data. -/// Calling setValue with a URL causes a NSInvalidArgumentException exception. -private struct URLLookup: Lookup { - typealias Value = URL - - func loadValue(from store: UserDefaults, at key: String, default defaultValue: URL?) -> URL? { - guard let value = store.url(forKey: key) - else { - guard !SharedAppStorageLocals.isSetting - else { return defaultValue } - - SharedAppStorageLocals.$isSetting.withValue(true) { - store.set(defaultValue, forKey: key) - } - return defaultValue - } - return value - } - - func saveValue(_ newValue: URL, to store: UserDefaults, at key: String) { - SharedAppStorageLocals.$isSetting.withValue(true) { - store.set(newValue, forKey: key) - } - } -} - -private struct RawRepresentableLookup: Lookup, - Sendable -where Value.RawValue == Base.Value { - let base: Base - func loadValue( - from store: UserDefaults, at key: String, default defaultValue: Value? - ) -> Value? { - base.loadValue(from: store, at: key, default: defaultValue?.rawValue) - .flatMap(Value.init(rawValue:)) - ?? defaultValue - } - func saveValue(_ newValue: Value, to store: UserDefaults, at key: String) { - base.saveValue(newValue.rawValue, to: store, at: key) - } -} - -private struct OptionalLookup: Lookup { - let base: Base - func loadValue( - from store: UserDefaults, at key: String, default defaultValue: Base.Value?? - ) -> Base.Value?? { - base.loadValue(from: store, at: key, default: defaultValue ?? nil) - } - func saveValue(_ newValue: Base.Value?, to store: UserDefaults, at key: String) { - if let newValue { - base.saveValue(newValue, to: store, at: key) - } else { - store.removeObject(forKey: key) - } - } -} diff --git a/Sources/ComposableArchitecture/SharedState/PersistenceKey/FileStorageKey.swift b/Sources/ComposableArchitecture/SharedState/PersistenceKey/FileStorageKey.swift deleted file mode 100644 index 9b9cc3b19679..000000000000 --- a/Sources/ComposableArchitecture/SharedState/PersistenceKey/FileStorageKey.swift +++ /dev/null @@ -1,354 +0,0 @@ -import Combine -import Dependencies -import Foundation - -extension PersistenceReaderKey { - /// Creates a persistence key that can read and write to a `Codable` value in the file system. - /// - /// - Parameters: - /// - url: The file URL from which to read and write the value. - /// - decoder: The JSONDecoder to use for decoding the value. - /// - encoder: The JSONEncoder to use for encoding the value. - /// - Returns: A file persistence key. - public static func fileStorage( - _ url: URL, - decoder: JSONDecoder = JSONDecoder(), - encoder: JSONEncoder = JSONEncoder() - ) -> Self - where Self == FileStorageKey { - FileStorageKey( - url: url, - decode: { try decoder.decode(Value.self, from: $0) }, - encode: { try encoder.encode($0) } - ) - } - - /// Creates a persistence key that can read and write to a value in the file system. - /// - /// - Parameters: - /// - url: The file URL from which to read and write the value. - /// - decode: The closure to use for decoding the value. - /// - encode: The closure to use for encoding the value. - /// - Returns: A file persistence key. - public static func fileStorage( - _ url: URL, - decode: @escaping @Sendable (Data) throws -> Value, - encode: @escaping @Sendable (Value) throws -> Data - ) -> Self - where Self == FileStorageKey { - FileStorageKey(url: url, decode: decode, encode: encode) - } -} - -/// A type defining a file persistence strategy -/// -/// Use ``PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` to create values of this type. -public final class FileStorageKey: PersistenceKey, Sendable { - private let storage: FileStorage - private let url: URL - private let decode: @Sendable (Data) throws -> Value - private let encode: @Sendable (Value) throws -> Data - fileprivate let state = LockIsolated(State()) - - fileprivate struct State { - var value: Value? - var workItem: DispatchWorkItem? - } - - public var id: AnyHashable { - FileStorageKeyID(url: self.url, storage: self.storage) - } - - fileprivate init( - url: URL, - decode: @escaping @Sendable (Data) throws -> Value, - encode: @escaping @Sendable (Value) throws -> Data - ) { - @Dependency(\.defaultFileStorage) var storage - self.storage = storage - self.url = url - self.decode = decode - self.encode = encode - } - - public func load(initialValue: Value?) -> Value? { - do { - return try decode(self.storage.load(self.url)) - } catch { - return initialValue - } - } - - public func save(_ value: Value) { - self.state.withValue { state in - if state.workItem == nil { - try? self.storage.save(encode(value), self.url) - let workItem = DispatchWorkItem { [weak self] in - guard let self else { return } - self.state.withValue { state in - defer { - state.value = nil - state.workItem = nil - } - guard let value = state.value - else { return } - try? self.storage.save(self.encode(value), self.url) - } - } - state.workItem = workItem - if canListenForResignActive { - self.storage.asyncAfter(.seconds(1), workItem) - } else { - self.storage.async(workItem) - } - } else { - state.value = value - } - } - } - - public func subscribe( - initialValue: Value?, - didSet: @escaping @Sendable (_ newValue: Value?) -> Void - ) -> Shared.Subscription { - let cancellable = LockIsolated(nil) - @Sendable func setUpSources() { - cancellable.withValue { [weak self] in - $0?.cancel() - guard let self else { return } - // NB: Make sure there is a file to create a source for. - if !self.storage.fileExists(self.url) { - try? self.storage.createDirectory(self.url.deletingLastPathComponent(), true) - try? self.storage.save(Data(), self.url) - } - let writeCancellable = self.storage.fileSystemSource(self.url, [.write]) { - // TODO: Improve this by fingerprinting (by adding extra bytes?) the file we write to the - // file system so that we can early out of this closure. - self.state.withValue { state in - guard state.workItem == nil - else { return } - didSet(self.load(initialValue: initialValue)) - } - } - let deleteCancellable = self.storage.fileSystemSource(self.url, [.delete, .rename]) { - self.state.withValue { state in - state.workItem?.cancel() - state.workItem = nil - } - `didSet`(self.load(initialValue: initialValue)) - setUpSources() - } - $0 = AnyCancellable { - writeCancellable.cancel() - deleteCancellable.cancel() - } - } - } - setUpSources() - let willResign: (any NSObjectProtocol)? - if let willResignNotificationName { - willResign = NotificationCenter.default.addObserver( - forName: willResignNotificationName, - object: nil, - queue: nil - ) { [weak self] _ in - guard let self - else { return } - self.performImmediately() - } - } else { - willResign = nil - } - let willTerminate: (any NSObjectProtocol)? - if let willTerminateNotificationName { - willTerminate = NotificationCenter.default.addObserver( - forName: willTerminateNotificationName, - object: nil, - queue: nil - ) { [weak self] _ in - guard let self - else { return } - self.performImmediately() - } - } else { - willTerminate = nil - } - return Shared.Subscription { - cancellable.withValue { $0?.cancel() } - if let willResign { - NotificationCenter.default.removeObserver(willResign) - } - if let willTerminate { - NotificationCenter.default.removeObserver(willTerminate) - } - } - } - - private func performImmediately() { - self.state.withValue { state in - guard let workItem = state.workItem - else { return } - self.storage.async(workItem) - self.storage.async( - DispatchWorkItem { - self.state.withValue { state in - state.workItem?.cancel() - state.workItem = nil - } - } - ) - } - } -} - -private struct FileStorageKeyID: Hashable { - let url: URL - let storage: FileStorage -} - -private enum FileStorageDependencyKey: DependencyKey { - static var liveValue: FileStorage { - .fileSystem - } - static var previewValue: FileStorage { - .inMemory - } - static var testValue: FileStorage { - .inMemory - } -} - -extension DependencyValues { - /// Default file storage used by ``PersistenceReaderKey/fileStorage(_:decoder:encoder:)``. - /// - /// Use this dependency to override the manner in which ``PersistenceReaderKey/fileStorage(_:decoder:encoder:)`` - /// interacts with file storage. For example, while your app is running for UI tests you - /// probably do not want your features writing changes to disk, which would cause that data to - /// bleed over from test to test. - /// - /// So, for that situation you can use the ``FileStorage/inMemory`` file storage so that each - /// run of the app starts with a fresh "file system" that will never interfere with other tests: - /// - /// ```swift - /// @main - /// struct EntryPoint: App { - /// let store = Store(initialState: AppFeature.State()) { - /// AppFeature() - /// } withDependencies: { - /// if ProcessInfo.processInfo.environment["UITesting"] == "true" { - /// $0.defaultFileStorage = .inMemory - /// } - /// } - /// } - /// ``` - public var defaultFileStorage: FileStorage { - get { self[FileStorageDependencyKey.self] } - set { self[FileStorageDependencyKey.self] = newValue } - } -} - -/// A type that encapsulates saving and loading data from disk. -public struct FileStorage: Hashable, Sendable { - let id: AnyHashableSendable - let async: @Sendable (DispatchWorkItem) -> Void - let asyncAfter: @Sendable (DispatchTimeInterval, DispatchWorkItem) -> Void - let createDirectory: @Sendable (URL, Bool) throws -> Void - let fileExists: @Sendable (URL) -> Bool - let fileSystemSource: - @Sendable (URL, DispatchSource.FileSystemEvent, @escaping @Sendable () -> Void) -> - AnyCancellable - let load: @Sendable (URL) throws -> Data - @_spi(Internals) public let save: @Sendable (Data, URL) throws -> Void - - /// File storage that emulates a file system without actually writing anything to disk. - /// - /// This is the version of the ``Dependencies/DependencyValues/defaultFileStorage`` dependency - /// that is used by default when running your app in tests and previews. - public static var inMemory: Self { - inMemory(fileSystem: LockIsolated([:])) - } - - /// File storage that interacts directly with the file system for saving, loading and listening - /// for file changes. - /// - /// This is the version of the ``Dependencies/DependencyValues/defaultFileStorage`` dependency - /// that is used by default when running your app in the simulator or on device. - public static let fileSystem = Self( - id: AnyHashableSendable(DispatchQueue.main), - async: { DispatchQueue.main.async(execute: $0) }, - asyncAfter: { DispatchQueue.main.asyncAfter(deadline: .now() + $0, execute: $1) }, - createDirectory: { - try FileManager.default.createDirectory(at: $0, withIntermediateDirectories: $1) - }, - fileExists: { FileManager.default.fileExists(atPath: $0.path) }, - fileSystemSource: { - let source = DispatchSource.makeFileSystemObjectSource( - fileDescriptor: open($0.path, O_EVTONLY), - eventMask: $1, - queue: .main - ) - source.setEventHandler(handler: $2) - source.resume() - return AnyCancellable { - source.cancel() - close(source.handle) - } - }, - load: { try Data(contentsOf: $0) }, - save: { try $0.write(to: $1) } - ) - - @_spi(Internals) public static func inMemory( - fileSystem: LockIsolated<[URL: Data]>, - scheduler: AnySchedulerOf = .immediate - ) -> Self { - let sourceHandlers = LockIsolated<[URL: Set]>([:]) - return Self( - id: AnyHashableSendable(ObjectIdentifier(fileSystem)), - async: { scheduler.schedule($0.perform) }, - asyncAfter: { scheduler.schedule(after: scheduler.now.advanced(by: .init($0)), $1.perform) }, - createDirectory: { _, _ in }, - fileExists: { fileSystem.keys.contains($0) }, - fileSystemSource: { url, event, handler in - guard event.contains(.write) - else { return AnyCancellable {} } - let handler = Handler(operation: handler) - sourceHandlers.withValue { _ = $0[url, default: []].insert(handler) } - return AnyCancellable { - sourceHandlers.withValue { _ = $0[url]?.remove(handler) } - } - }, - load: { - guard let data = fileSystem[$0] - else { - struct LoadError: Error {} - throw LoadError() - } - return data - }, - save: { data, url in - fileSystem.withValue { $0[url] = data } - sourceHandlers.withValue { $0[url]?.forEach { $0.operation() } } - } - ) - } - - fileprivate struct Handler: Hashable, Sendable { - let id = UUID() - let operation: @Sendable () -> Void - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.id == rhs.id - } - func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - } - } - - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.id == rhs.id - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - } -} diff --git a/Sources/ComposableArchitecture/SharedState/PersistenceKey/InMemoryKey.swift b/Sources/ComposableArchitecture/SharedState/PersistenceKey/InMemoryKey.swift deleted file mode 100644 index c07ec77a02d0..000000000000 --- a/Sources/ComposableArchitecture/SharedState/PersistenceKey/InMemoryKey.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Dependencies -import Foundation - -extension PersistenceReaderKey { - /// Creates a persistence key for sharing data in-memory for the lifetime of an application. - /// - /// For example, one could initialize a key with the date and time at which the application was - /// most recently launched, and access this date from anywhere using the ``Shared`` property - /// wrapper: - /// - /// ```swift - /// @Shared(.inMemory("appLaunchedAt")) var appLaunchedAt = Date() - /// ``` - /// - /// - Parameter key: A string key identifying a value to share in memory. - /// - Returns: An in-memory persistence key. - public static func inMemory(_ key: String) -> Self - where Self == InMemoryKey { - InMemoryKey(key) - } -} - -/// A type defining an in-memory persistence strategy -/// -/// See ``PersistenceReaderKey/inMemory(_:)`` to create values of this type. -public struct InMemoryKey: PersistenceKey, Sendable { - private let key: String - private let store: InMemoryStorage - fileprivate init(_ key: String) { - @Dependency(\.defaultInMemoryStorage) var defaultInMemoryStorage - self.key = key - self.store = defaultInMemoryStorage - } - public var id: AnyHashable { - InMemoryKeyID(key: self.key, store: self.store) - } - public func load(initialValue: Value?) -> Value? { initialValue } - public func save(_ value: Value) {} -} - -public struct InMemoryStorage: Hashable, Sendable { - private let id = UUID() - public init() {} -} - -private struct InMemoryKeyID: Hashable { - let key: String - let store: InMemoryStorage -} - -private enum DefaultInMemoryStorageKey: DependencyKey { - static var liveValue: InMemoryStorage { InMemoryStorage() } - static var testValue: InMemoryStorage { InMemoryStorage() } -} - -extension DependencyValues { - public var defaultInMemoryStorage: InMemoryStorage { - get { self[DefaultInMemoryStorageKey.self] } - set { self[DefaultInMemoryStorageKey.self] = newValue } - } -} diff --git a/Sources/ComposableArchitecture/SharedState/PersistenceKey/PersistenceKeyDefault.swift b/Sources/ComposableArchitecture/SharedState/PersistenceKey/PersistenceKeyDefault.swift deleted file mode 100644 index 64d45aa07679..000000000000 --- a/Sources/ComposableArchitecture/SharedState/PersistenceKey/PersistenceKeyDefault.swift +++ /dev/null @@ -1,54 +0,0 @@ -/// A persistence key that provides a default value to an existing persistence key. -/// -/// Use this persistence key when constructing type-safe keys (see -/// for more info) to provide a default that is used instead of -/// providing one at the call site of using [`@Shared`](). -/// -/// For example, if an `isOn` value is backed by user defaults and it should default to `false` when -/// there is no value in user defaults, then you can define a persistence key like so: -/// -/// ```swift -/// extension PersistenceReaderKey where Self == PersistenceKeyDefault> { -/// static var isOn: Self { -/// PersistenceKeyDefault(.appStorage("isOn"), false) -/// } -/// } -/// ``` -/// -/// And then use it like so: -/// -/// ```swift -/// struct State { -/// @Shared(.isOn) var isOn -/// } -/// ``` -public struct PersistenceKeyDefault: PersistenceReaderKey { - let base: Base - let defaultValue: @Sendable () -> Base.Value - - public init(_ key: Base, _ value: @autoclosure @escaping @Sendable () -> Base.Value) { - self.base = key - self.defaultValue = value - } - - public var id: Base.ID { - self.base.id - } - - public func load(initialValue: Base.Value?) -> Base.Value? { - self.base.load(initialValue: initialValue ?? self.defaultValue()) - } - - public func subscribe( - initialValue: Base.Value?, - didSet: @escaping @Sendable (Base.Value?) -> Void - ) -> Shared.Subscription { - self.base.subscribe(initialValue: initialValue, didSet: didSet) - } -} - -extension PersistenceKeyDefault: PersistenceKey where Base: PersistenceKey { - public func save(_ value: Value) { - self.base.save(value) - } -} diff --git a/Sources/ComposableArchitecture/SharedState/Reference.swift b/Sources/ComposableArchitecture/SharedState/Reference.swift deleted file mode 100644 index c5db9108236e..000000000000 --- a/Sources/ComposableArchitecture/SharedState/Reference.swift +++ /dev/null @@ -1,20 +0,0 @@ -#if canImport(Combine) - import Combine -#endif - -protocol Reference: AnyObject, CustomStringConvertible, Sendable { - associatedtype Value: Sendable - var value: Value { get set } - - func access() - func withMutation(_ mutation: () throws -> T) rethrows -> T - #if canImport(Combine) - var publisher: AnyPublisher { get } - #endif -} - -extension Reference { - var valueType: Any.Type { - Value.self - } -} diff --git a/Sources/ComposableArchitecture/SharedState/References/ValueReference.swift b/Sources/ComposableArchitecture/SharedState/References/ValueReference.swift deleted file mode 100644 index 65fad69c2c60..000000000000 --- a/Sources/ComposableArchitecture/SharedState/References/ValueReference.swift +++ /dev/null @@ -1,452 +0,0 @@ -import Dependencies -import Foundation - -#if canImport(Combine) - import Combine -#endif - -extension Shared { - /// Creates a shared reference to a value using a persistence key. - /// - /// - Parameters: - /// - value: A default value that is used when no value can be returned from the persistence - /// key. - /// - persistenceKey: A persistence key associated with the shared reference. It is responsible - /// for loading and saving the shared reference's value from some external source. - /// - fileID: The fileID. - /// - line: The line. - public init( - wrappedValue value: @autoclosure @Sendable () -> Value, - _ persistenceKey: some PersistenceKey, - fileID: StaticString = #fileID, - line: UInt = #line - ) { - self.init( - reference: { - @Dependency(\.persistentReferences) var references - return references.withValue { - if let reference = $0[persistenceKey.id] { - precondition( - reference.valueType == Value.self, - """ - "\(typeName(Value.self, genericsAbbreviated: false))" does not match existing \ - persistent reference "\(typeName(reference.valueType, genericsAbbreviated: false))" \ - (key: "\(persistenceKey.id)") - """ - ) - return reference - } else { - let reference = ValueReference( - initialValue: value(), - persistenceKey: persistenceKey, - fileID: fileID, - line: line - ) - $0[persistenceKey.id] = reference - return reference - } - } - }(), - keyPath: \Value.self - ) - } - - /// Creates a shared reference to an optional value using a persistence key. - /// - /// - Parameters: - /// - persistenceKey: A persistence key associated with the shared reference. It is responsible - /// for loading and saving the shared reference's value from some external source. - /// - fileID: The fileID. - /// - line: The line. - @_disfavoredOverload - public init( - _ persistenceKey: some PersistenceKey, - fileID: StaticString = #fileID, - line: UInt = #line - ) where Value == Wrapped? { - self.init(wrappedValue: nil, persistenceKey, fileID: fileID, line: line) - } - - /// Creates a shared reference to a value using a persistence key. - /// - /// If the given persistence key cannot load a value, an error is thrown. For a non-throwing - /// version of this initializer, see ``init(wrappedValue:_:fileID:line:)-9kfmy``. - /// - /// - Parameters: - /// - persistenceKey: A persistence key associated with the shared reference. It is responsible - /// for loading and saving the shared reference's value from some external source. - /// - fileID: The fileID. - /// - line: The line. - @_disfavoredOverload - public init( - _ persistenceKey: some PersistenceKey, - fileID: StaticString = #fileID, - line: UInt = #line - ) throws { - try self.init( - throwingValue: { - guard let initialValue = persistenceKey.load(initialValue: nil) - else { throw LoadError() } - return initialValue - }(), - persistenceKey, - fileID: fileID, - line: line - ) - } - - private init( - throwingValue value: @autoclosure @Sendable () throws -> Value, - _ persistenceKey: some PersistenceKey, - fileID: StaticString = #fileID, - line: UInt = #line - ) throws { - try self.init( - reference: { - @Dependency(\.persistentReferences) var references - return try references.withValue { - if let reference = $0[persistenceKey.id] { - precondition( - reference.valueType == Value.self, - """ - "\(typeName(Value.self, genericsAbbreviated: false))" does not match existing \ - persistent reference "\(typeName(reference.valueType, genericsAbbreviated: false))" \ - (key: "\(persistenceKey.id)") - """ - ) - return reference - } else { - let reference = try ValueReference( - initialValue: value(), - persistenceKey: persistenceKey, - fileID: fileID, - line: line - ) - $0[persistenceKey.id] = reference - return reference - } - } - }(), - keyPath: \Value.self - ) - } - - /// Creates a shared reference to a value using a persistence key with a default value. - /// - /// - Parameters: - /// - persistenceKey: A persistence key associated with the shared reference. It is responsible - /// for loading and saving the shared reference's value from some external source. - /// - fileID: The fileID. - /// - line: The line. - public init>( - _ persistenceKey: PersistenceKeyDefault, - fileID: StaticString = #fileID, - line: UInt = #line - ) { - self.init( - wrappedValue: persistenceKey.defaultValue(), - persistenceKey.base, - fileID: fileID, - line: line - ) - } - - /// Creates a shared reference to a value using a persistence key by overriding its default value. - /// - /// - Parameters: - /// - value: A default value that is used when no value can be returned from the persistence - /// key. - /// - persistenceKey: A persistence key associated with the shared reference. It is responsible - /// for loading and saving the shared reference's value from some external source. - /// - fileID: The fileID. - /// - line: The line. - public init>( - wrappedValue value: @autoclosure @Sendable () -> Value, - _ persistenceKey: PersistenceKeyDefault, - fileID: StaticString = #fileID, - line: UInt = #line - ) { - self.init( - wrappedValue: value(), - persistenceKey.base, - fileID: fileID, - line: line - ) - } -} - -extension SharedReader { - /// Creates a shared reference to a read-only value using a persistence key. - /// - /// - Parameters: - /// - value: A default value that is used when no value can be returned from the persistence - /// key. - /// - persistenceKey: A persistence key associated with the shared reference. It is responsible - /// for loading the shared reference's value from some external source. - /// - fileID: The fileID. - /// - line: The line. - public init( - wrappedValue value: @autoclosure @Sendable () -> Value, - _ persistenceKey: some PersistenceReaderKey, - fileID: StaticString = #fileID, - line: UInt = #line - ) { - self.init( - reference: { - @Dependency(\.persistentReferences) var references - return references.withValue { - if let reference = $0[persistenceKey.id] { - precondition( - reference.valueType == Value.self, - """ - Type mismatch at persistence key "\(persistenceKey.id)": \ - \(reference.valueType) != \(Value.self) - """ - ) - return reference - } else { - let reference = ValueReference( - initialValue: value(), - persistenceKey: persistenceKey, - fileID: fileID, - line: line - ) - $0[persistenceKey.id] = reference - return reference - } - } - }(), - keyPath: \Value.self - ) - } - - /// Creates a shared reference to an optional, read-only value using a persistence key. - /// - /// - Parameters: - /// - persistenceKey: A persistence key associated with the shared reference. It is responsible - /// for loading the shared reference's value from some external source. - /// - fileID: The fileID. - /// - line: The line. - @_disfavoredOverload - public init( - _ persistenceKey: some PersistenceReaderKey, - fileID: StaticString = #fileID, - line: UInt = #line - ) where Value == Wrapped? { - self.init(wrappedValue: nil, persistenceKey, fileID: fileID, line: line) - } - - /// Creates a shared reference to a read-only value using a persistence key. - /// - /// If the given persistence key cannot load a value, an error is thrown. For a non-throwing - /// version of this initializer, see ``init(wrappedValue:_:fileID:line:)-7f68o``. - /// - /// - Parameters: - /// - persistenceKey: A persistence key associated with the shared reference. It is responsible - /// for loading the shared reference's value from some external source. - /// - fileID: The fileID. - /// - line: The line. - @_disfavoredOverload - public init( - _ persistenceKey: some PersistenceReaderKey, - fileID: StaticString = #fileID, - line: UInt = #line - ) throws { - try self.init( - throwingValue: { - guard let initialValue = persistenceKey.load(initialValue: nil) - else { throw LoadError() } - return initialValue - }(), - persistenceKey, - fileID: fileID, - line: line - ) - } - - private init( - throwingValue value: @autoclosure @Sendable () throws -> Value, - _ persistenceKey: some PersistenceReaderKey, - fileID: StaticString = #fileID, - line: UInt = #line - ) throws { - try self.init( - reference: { - @Dependency(\.persistentReferences) var references - return try references.withValue { - if let reference = $0[persistenceKey.id] { - precondition( - reference.valueType == Value.self, - """ - Type mismatch at persistence key "\(persistenceKey.id)": \ - \(reference.valueType) != \(Value.self) - """ - ) - return reference - } else { - let reference = ValueReference( - initialValue: try value(), - persistenceKey: persistenceKey, - fileID: fileID, - line: line - ) - $0[persistenceKey.id] = reference - return reference - } - } - }(), - keyPath: \Value.self - ) - } - - /// Creates a shared reference to a read-only value using a persistence key with a default value. - /// - /// - Parameters: - /// - persistenceKey: A persistence key associated with the shared reference. It is responsible - /// for loading the shared reference's value from some external source. - /// - fileID: The fileID. - /// - line: The line. - public init>( - _ persistenceKey: PersistenceKeyDefault, - fileID: StaticString = #fileID, - line: UInt = #line - ) { - self.init( - wrappedValue: persistenceKey.defaultValue(), - persistenceKey.base, - fileID: fileID, - line: line - ) - } - - /// Creates a shared reference to a value using a persistence key by overriding its default value. - /// - /// - Parameters: - /// - value: A default value that is used when no value can be returned from the persistence - /// key. - /// - persistenceKey: A persistence key associated with the shared reference. It is responsible - /// for loading the shared reference's value from some external source. - /// - fileID: The fileID. - /// - line: The line. - public init>( - wrappedValue value: @autoclosure @Sendable () -> Value, - _ persistenceKey: PersistenceKeyDefault, - fileID: StaticString = #fileID, - line: UInt = #line - ) { - self.init( - wrappedValue: value(), - persistenceKey.base, - fileID: fileID, - line: line - ) - } -} - -private struct LoadError: Error {} - -final class ValueReference>: Reference, - @unchecked Sendable -{ - private let lock = NSRecursiveLock() - private let persistenceKey: Persistence? - #if canImport(Combine) - private let subject: CurrentValueRelay - #endif - private var subscription: Shared.Subscription! - private var _value: Value { - willSet { - self.subject.send(newValue) - } - } - private let _$perceptionRegistrar = PerceptionRegistrar( - isPerceptionCheckingEnabled: _isStorePerceptionCheckingEnabled - ) - private let fileID: StaticString - private let line: UInt - var value: Value { - get { - self._$perceptionRegistrar.access(self, keyPath: \.value) - return self.lock.withLock { self._value } - } - set { - self._$perceptionRegistrar.willSet(self, keyPath: \.value) - defer { self._$perceptionRegistrar.didSet(self, keyPath: \.value) } - self.lock.withLock { - self._value = newValue - func open(_ key: some PersistenceKey) { - key.save(self._value as! A) - } - guard let key = self.persistenceKey as? any PersistenceKey - else { return } - open(key) - } - } - } - #if canImport(Combine) - var publisher: AnyPublisher { - self.subject.dropFirst().eraseToAnyPublisher() - } - #endif - init( - initialValue: Value, - persistenceKey: Persistence? = nil, - fileID: StaticString, - line: UInt - ) { - self._value = persistenceKey?.load(initialValue: initialValue) ?? initialValue - self.persistenceKey = persistenceKey - #if canImport(Combine) - self.subject = CurrentValueRelay(initialValue) - #endif - self.fileID = fileID - self.line = line - if let persistenceKey { - self.subscription = persistenceKey.subscribe( - initialValue: initialValue - ) { [weak self] value in - guard let self else { return } - mainActorASAP { - self._$perceptionRegistrar.willSet(self, keyPath: \.value) - defer { self._$perceptionRegistrar.didSet(self, keyPath: \.value) } - self.lock.withLock { - self._value = value ?? initialValue - } - } - } - } - } - func access() { - _$perceptionRegistrar.access(self, keyPath: \.value) - } - func withMutation(_ mutation: () throws -> T) rethrows -> T { - self._$perceptionRegistrar.willSet(self, keyPath: \.value) - defer { self._$perceptionRegistrar.didSet(self, keyPath: \.value) } - return try mutation() - } - var description: String { - "Shared<\(Value.self)>@\(self.fileID):\(self.line)" - } -} - -#if canImport(Observation) - extension ValueReference: Observable {} -#endif - -extension ValueReference: Perceptible {} - -private enum PersistentReferencesKey: DependencyKey { - static var liveValue: LockIsolated<[AnyHashable: any Reference]> { - LockIsolated([:]) - } - static var testValue: LockIsolated<[AnyHashable: any Reference]> { - LockIsolated([:]) - } -} - -extension DependencyValues { - var persistentReferences: LockIsolated<[AnyHashable: any Reference]> { - get { self[PersistentReferencesKey.self] } - set { self[PersistentReferencesKey.self] = newValue } - } -} diff --git a/Sources/ComposableArchitecture/SharedState/Shared.swift b/Sources/ComposableArchitecture/SharedState/Shared.swift deleted file mode 100644 index db5b93955d9d..000000000000 --- a/Sources/ComposableArchitecture/SharedState/Shared.swift +++ /dev/null @@ -1,481 +0,0 @@ -import CustomDump -import Dependencies -import IssueReporting - -#if canImport(Combine) - import Combine -#endif - -/// A property wrapper type that shares a value with multiple parts of an application. -/// -/// See the article for more detailed information on how to use this property -/// wrapper. -@dynamicMemberLookup -@propertyWrapper -public struct Shared: Sendable { - private let reference: any Reference - private let keyPath: _SendableAnyKeyPath - - init(reference: any Reference, keyPath: _SendableAnyKeyPath) { - self.reference = reference - self.keyPath = keyPath - } - - /// Wraps a value in a shared reference. - /// - /// - Parameters: - /// - value: A value to wrap. - /// - fileID: The fileID. - /// - line: The line. - public init(_ value: Value, fileID: StaticString = #fileID, line: UInt = #line) { - self.init( - reference: ValueReference>( - initialValue: value, - fileID: fileID, - line: line - ), - keyPath: \Value.self - ) - } - - /// Creates a shared reference from another shared reference. - /// - /// You don't call this initializer directly. Instead, Swift calls it for you when you use a - /// property-wrapper attribute on a binding closure parameter. - /// - /// - Parameter projectedValue: A shared reference. - public init(projectedValue: Shared) { - self = projectedValue - } - - /// Unwraps a shared reference to an optional value. - /// - /// ```swift - /// @Shared(.currentUser) var currentUser: User? - /// - /// if let sharedCurrentUser = Shared($currentUser) { - /// sharedCurrentUser // Shared - /// } - /// ``` - /// - /// - Parameter base: A shared reference to an optional value. - public init?(_ base: Shared) { - guard let initialValue = base.wrappedValue - else { return nil } - self.init( - reference: base.reference, - // NB: Can get rid of bitcast when this is fixed: - // https://github.com/swiftlang/swift/issues/75531 - keyPath: (base.keyPath as AnyKeyPath) - .appending(path: \Value?.[default: DefaultSubscript(initialValue)])! - .unsafeSendable() - ) - } - - /// Perform an operation on shared state with isolated access to the underlying value. - @MainActor - public func withLock(_ transform: @Sendable (inout Value) throws -> R) rethrows -> R { - try transform(&self._wrappedValue) - } - - /// The underlying value referenced by the shared variable. - /// - /// This property provides primary access to the value's data. However, you don't access - /// `wrappedValue` directly. Instead, you use the property variable created with the ``Shared`` - /// attribute. In the following example, the shared variable `topics` returns the value of - /// `wrappedValue`: - /// - /// ```swift - /// struct State { - /// @Shared var subscriptions: [Subscription] - /// - /// var isSubscribed: Bool { - /// !subscriptions.isEmpty - /// } - /// } - /// ``` - public var wrappedValue: Value { - get { _wrappedValue } - @available(*, noasync, message: "Use '$shared.withLock' instead of mutating directly.") - nonmutating set { _wrappedValue = newValue } - } - - /// A projection of the shared value that returns a shared reference. - /// - /// Use the projected value to pass a shared value down to another feature. This is most - /// commonly done to share a value from one feature to another: - /// - /// ```swift - /// case .nextButtonTapped: - /// state.path.append( - /// PersonalInfoFeature(signUpData: state.$signUpData) - /// ) - /// ``` - /// - /// Further you can use dot-chaining syntax to derive a smaller piece of shared state to hand - /// to another feature: - /// - /// ```swift - /// case .nextButtonTapped: - /// state.path.append( - /// PhoneNumberFeature(phoneNumber: state.$signUpData.phoneNumber) - /// ) - /// ``` - /// - /// See for more details. - public var projectedValue: Self { - get { - reference.access() - return self - } - set { - reference.withMutation { - self = newValue - } - } - } - - #if canImport(Combine) - /// Returns a publisher that emits events when the underlying value changes. - /// - /// Useful when a feature needs to execute logic when a shared reference is updated outside of - /// the feature itself. - /// - /// ```swift - /// case .onAppear: - /// return .run { [currentUser = state.$currentUser] send in - /// for await _ in currentUser.publisher.values { - /// await send(.currentUserUpdated) - /// } - /// } - /// ``` - public var publisher: AnyPublisher { - func open(_ reference: some Reference) -> AnyPublisher { - reference.publisher - .map { $0[keyPath: unsafeDowncast(self.keyPath, to: KeyPath.self)] } - .eraseToAnyPublisher() - } - return open(self.reference) - } - #endif - - /// Returns a shared reference to the resulting value of a given key path. - /// - /// You don't call this subscript directly. Instead, Swift calls it for you when you access a - /// property of the underlying value. In the following example, the property access - /// `$signUpData.topics` returns the value of invoking this subscript with `\SignUpData.topics`: - /// - /// ```swift - /// @Shared var signUpData: SignUpData - /// - /// $signUpData.topics // Shared> - /// ``` - /// - /// - Parameter keyPath: A key path to a specific resulting value. - /// - Returns: A new binding. - public subscript( - dynamicMember keyPath: WritableKeyPath - ) -> Shared { - Shared( - reference: self.reference, - // NB: Can get rid of bitcast when this is fixed: - // https://github.com/swiftlang/swift/issues/75531 - keyPath: (self.keyPath as AnyKeyPath) - .appending(path: keyPath)! - .unsafeSendable() - ) - } - - @_disfavoredOverload - @available( - *, deprecated, message: "Use 'Shared($value.optional)' to unwrap optional shared values" - ) - public subscript( - dynamicMember keyPath: WritableKeyPath - ) -> Shared? { - Shared(self[dynamicMember: keyPath]) - } - - public func assert( - _ updateValueToExpectedResult: (inout Value) throws -> Void, - fileID: StaticString = #fileID, - file filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) rethrows where Value: Equatable { - @Dependency(\.sharedChangeTrackers) var changeTrackers - guard - let changeTracker = - changeTrackers - .first(where: { $0.changes[ObjectIdentifier(self.reference)] != nil }) - else { - reportIssue( - "Expected changes, but none occurred.", - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - return - } - try changeTracker.assert { - guard var snapshot = self.snapshot, snapshot != self.currentValue else { - reportIssue( - "Expected changes, but none occurred.", - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - return - } - try updateValueToExpectedResult(&snapshot) - self.snapshot = snapshot - // TODO: Finesse error more than `expectNoDifference` - expectNoDifference( - self.currentValue, - self.snapshot, - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - self.snapshot = nil - } - } - - fileprivate var _wrappedValue: Value { - get { - @Dependency(\.sharedChangeTracker) var changeTracker - if changeTracker != nil { - return self.snapshot ?? self.currentValue - } else { - return self.currentValue - } - } - nonmutating set { - @Dependency(\.sharedChangeTracker) var changeTracker - if changeTracker != nil { - self.snapshot = newValue - } else { - @Dependency(\.sharedChangeTrackers) var changeTrackers: Set - for changeTracker in changeTrackers { - changeTracker.track(self.reference) - } - self.currentValue = newValue - } - } - } - - private var currentValue: Value { - get { - func open(_ reference: some Reference) -> Value { - reference.value[ - keyPath: unsafeDowncast(self.keyPath, to: KeyPath.self) - ] - } - return open(self.reference) - } - nonmutating set { - func open(_ reference: some Reference) { - reference.value[ - keyPath: unsafeDowncast(self.keyPath, to: WritableKeyPath.self) - ] = newValue - } - return open(self.reference) - } - } - - private var snapshot: Value? { - get { - func open(_ reference: some Reference) -> Value? { - @Dependency(\.sharedChangeTracker) var changeTracker - return changeTracker?[reference]?.snapshot[ - keyPath: unsafeDowncast(self.keyPath, to: WritableKeyPath.self) - ] - } - return open(self.reference) - } - nonmutating set { - func open(_ reference: some Reference) { - @Dependency(\.sharedChangeTracker) var changeTracker - guard let newValue else { - changeTracker?[reference] = nil - return - } - if changeTracker?[reference] == nil { - changeTracker?[reference] = AnyChange(reference) - } - changeTracker?[reference]?.snapshot[ - keyPath: unsafeDowncast(self.keyPath, to: WritableKeyPath.self) - ] = newValue - } - return open(self.reference) - } - } -} - -extension Shared: Equatable where Value: Equatable { - public static func == (lhs: Shared, rhs: Shared) -> Bool { - @Dependency(\.sharedChangeTracker) var changeTracker - if changeTracker != nil, lhs.reference === rhs.reference, lhs.keyPath == rhs.keyPath { - if let lhsReference = lhs.reference as? any Equatable { - func open(_ lhsReference: T) -> Bool { - lhsReference == rhs.reference as? T - } - return open(lhsReference) - } - return lhs.snapshot ?? lhs.currentValue == rhs.currentValue - } else { - return lhs.wrappedValue == rhs.wrappedValue - } - } -} - -extension Shared: Identifiable where Value: Identifiable { - public var id: Value.ID { - self.wrappedValue.id - } -} - -extension Shared: CustomDumpRepresentable { - public var customDumpValue: Any { - self.currentValue - } -} - -extension Shared: _CustomDiffObject { - public var _customDiffValues: (Any, Any) { - (self.snapshot ?? self.currentValue, self.currentValue) - } - - public var _objectIdentifier: ObjectIdentifier { - ObjectIdentifier(self.reference) - } -} - -extension Shared -where - Value: _MutableIdentifiedCollection, - Value.Element: Sendable -{ - /// Allows a `ForEach` view to transform a shared collection into shared elements. - /// - /// ```swift - /// struct State { - /// @Shared(.fileStorage(.todos)) var todos: IdentifiedArrayOf = [] - /// // ... - /// } - /// - /// // ... - /// - /// ForEach(store.$todos.elements) { $todo in - /// NavigationLink( - /// // $todo: Shared - /// // todo: Todo - /// state: Path.State.todo(TodoFeature.State(todo: $todo)) - /// ) { - /// Text(todo.title) - /// } - /// } - /// ``` - /// - /// > Warning: It is not appropriate to use this property outside of SwiftUI's `ForEach` view. If - /// > you need to derive a shared element from a shared collection, use a stable lookup, instead, - /// > like the `$array[id:]` subscript on `IdentifiedArray`. - public var elements: some RandomAccessCollection> { - zip(self.wrappedValue.ids, self.wrappedValue).lazy.map { id, element in - self[id: id, default: DefaultSubscript(element)] - } - } -} - -@available( - *, - unavailable, - message: - "Derive shared elements from a stable subscript, like '$array[id:]' on 'IdentifiedArray', or pass '$array.elements' to a 'ForEach' view." -) -extension Shared: Collection, Sequence -where Value: MutableCollection & RandomAccessCollection, Value.Index: Hashable { - public var startIndex: Value.Index { - assertionFailure("Conformance of 'Shared' to 'Collection' is unavailable.") - return self.wrappedValue.startIndex - } - public var endIndex: Value.Index { - assertionFailure("Conformance of 'Shared' to 'Collection' is unavailable.") - return self.wrappedValue.endIndex - } - public func index(after i: Value.Index) -> Value.Index { - assertionFailure("Conformance of 'Shared' to 'Collection' is unavailable.") - return self.wrappedValue.index(after: i) - } -} - -@available( - *, - unavailable, - message: - "Derive shared elements from a stable subscript, like '$array[id:]' on 'IdentifiedArray', or pass '$array.elements' to a 'ForEach' view." -) -extension Shared: MutableCollection -where Value: MutableCollection & RandomAccessCollection, Value.Index: Hashable { - public subscript(position: Value.Index) -> Shared { - get { - fatalError("Conformance of 'Shared' to 'MutableCollection' is unavailable.") - } - set { - fatalError("Conformance of 'Shared' to 'MutableCollection' is unavailable.") - } - } -} - -@available( - *, - unavailable, - message: - "Derive shared elements from a stable subscript, like '$array[id:]' on 'IdentifiedArray', or pass '$array.elements' to a 'ForEach' view." -) -extension Shared: BidirectionalCollection -where Value: MutableCollection & RandomAccessCollection, Value.Index: Hashable { - public func index(before i: Value.Index) -> Value.Index { - assertionFailure("Conformance of 'Shared' to 'BidirectionalCollection' is unavailable.") - return self.wrappedValue.index(before: i) - } -} - -@available( - *, - unavailable, - message: - "Derive shared elements from a stable subscript, like '$array[id:]' on 'IdentifiedArray', or pass '$array.elements' to a 'ForEach' view." -) -extension Shared: RandomAccessCollection -where Value: MutableCollection & RandomAccessCollection, Value.Index: Hashable { -} - -extension Shared { - public subscript( - dynamicMember keyPath: KeyPath - ) -> SharedReader { - SharedReader( - reference: self.reference, - keyPath: (self.keyPath as AnyKeyPath).appending(path: keyPath)!.unsafeSendable() - ) - } - - /// Constructs a read-only version of the shared value. - public var reader: SharedReader { - SharedReader(reference: self.reference, keyPath: self.keyPath) - } - - @_disfavoredOverload - @available( - *, deprecated, message: "Use 'SharedReader($value.optional)' to unwrap optional shared values" - ) - public subscript( - dynamicMember keyPath: KeyPath - ) -> SharedReader? { - SharedReader(self[dynamicMember: keyPath]) - } -} diff --git a/Sources/ComposableArchitecture/SharedState/SharedChangeTracking.swift b/Sources/ComposableArchitecture/SharedState/SharedChangeTracking.swift deleted file mode 100644 index 1eda500d8366..000000000000 --- a/Sources/ComposableArchitecture/SharedState/SharedChangeTracking.swift +++ /dev/null @@ -1,142 +0,0 @@ -import CustomDump -import Dependencies - -@_spi(Internals) -public func withSharedChangeTracking( - _ apply: (SharedChangeTracker) throws -> T -) rethrows -> T { - let changeTracker = SharedChangeTracker() - return try changeTracker.track { - try apply(changeTracker) - } -} - -@_spi(Internals) -public func withSharedChangeTracking( - _ apply: (SharedChangeTracker) async throws -> T -) async rethrows -> T { - let changeTracker = SharedChangeTracker() - return try await changeTracker.track { - try await apply(changeTracker) - } -} - -protocol Change { - associatedtype Value - var reference: any Reference { get } - var snapshot: Value { get set } -} - -extension Change { - func assertUnchanged() { - if let difference = diff(snapshot, self.reference.value, format: .proportional) { - reportIssue( - """ - Tracked changes to '\(self.reference.description)' but failed to assert: … - - \(difference.indent(by: 2)) - - (Before: −, After: +) - - Call 'Shared<\(Value.self)>.assert' to exhaustively test these changes, or call \ - 'skipChanges' to ignore them. - """ - ) - } - } -} - -struct AnyChange: Change, Sendable { - let reference: any Reference - var snapshot: Value - - init(_ reference: some Reference) { - self.reference = reference - self.snapshot = reference.value - } -} - -@_spi(Internals) -public final class SharedChangeTracker: Sendable { - let changes: LockIsolated<[ObjectIdentifier: any Sendable]> = LockIsolated([:]) - var hasChanges: Bool { !self.changes.isEmpty } - @_spi(Internals) public init() {} - func resetChanges() { self.changes.withValue { $0.removeAll() } } - func assertUnchanged() { - for change in self.changes.values { - if let change = change as? any Change { - change.assertUnchanged() - } - } - self.resetChanges() - } - func track(_ reference: some Reference) { - if !self.changes.keys.contains(ObjectIdentifier(reference)) { - self.changes.withValue { $0[ObjectIdentifier(reference)] = AnyChange(reference) } - } - } - subscript(_ reference: some Reference) -> AnyChange? { - _read { yield self.changes[ObjectIdentifier(reference)] as? AnyChange } - _modify { - var change = self.changes[ObjectIdentifier(reference)] as? AnyChange - yield &change - self.changes.withValue { [change] in $0[ObjectIdentifier(reference)] = change } - } - } - func track(_ operation: () throws -> R) rethrows -> R { - try withDependencies { - $0.sharedChangeTrackers.insert(self) - } operation: { - try operation() - } - } - func track(_ operation: () async throws -> R) async rethrows -> R { - try await withDependencies { - $0.sharedChangeTrackers.insert(self) - } operation: { - try await operation() - } - } - @_spi(Internals) - public func assert(_ operation: () throws -> R) rethrows -> R { - try withDependencies { - $0.sharedChangeTracker = self - } operation: { - try operation() - } - } -} - -extension SharedChangeTracker: Hashable { - @_spi(Internals) - public static func == (lhs: SharedChangeTracker, rhs: SharedChangeTracker) -> Bool { - lhs === rhs - } - @_spi(Internals) - public func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self)) - } -} - -private enum SharedChangeTrackersKey: DependencyKey { - static var liveValue: Set { [] } - static var testValue: Set { [SharedChangeTracker()] } -} - -private enum SharedChangeTrackerKey: DependencyKey { - static var liveValue: SharedChangeTracker? { nil } - static var testValue: SharedChangeTracker? { nil } -} - -extension DependencyValues { - @_spi(Internals) - public var sharedChangeTrackers: Set { - get { self[SharedChangeTrackersKey.self] } - set { self[SharedChangeTrackersKey.self] = newValue } - } - @_spi(Internals) - public var sharedChangeTracker: SharedChangeTracker? { - get { self[SharedChangeTrackerKey.self] } - set { self[SharedChangeTrackerKey.self] = newValue } - } -} diff --git a/Sources/ComposableArchitecture/SharedState/SharedReader.swift b/Sources/ComposableArchitecture/SharedState/SharedReader.swift deleted file mode 100644 index d42597202d4e..000000000000 --- a/Sources/ComposableArchitecture/SharedState/SharedReader.swift +++ /dev/null @@ -1,201 +0,0 @@ -#if canImport(Combine) - import Combine -#endif - -/// A property wrapper type that shares a value with multiple parts of an application. -/// -/// See the article for more detailed information on how to use this property -/// wrapper, in particular . -@dynamicMemberLookup -@propertyWrapper -public struct SharedReader { - fileprivate let reference: any Reference - fileprivate let keyPath: AnyKeyPath - - init(reference: any Reference, keyPath: AnyKeyPath) { - self.reference = reference - self.keyPath = keyPath - } - - init(reference: some Reference) { - self.init(reference: reference, keyPath: \Value.self) - } - - /// Creates a read-only shared reference from another read-only shared reference. - /// - /// You don't call this initializer directly. Instead, Swift calls it for you when you use a - /// property-wrapper attribute on a binding closure parameter. - /// - /// - Parameter projectedValue: A read-only shared reference. - public init(projectedValue: SharedReader) { - self = projectedValue - } - - /// Unwraps a read-only shared reference to an optional value. - /// - /// ```swift - /// @SharedReader(.currentUser) var currentUser: User? - /// - /// if let sharedCurrentUser = SharedReader($currentUser) { - /// sharedCurrentUser // SharedReader - /// } - /// ``` - /// - /// - Parameter base: A read-only shared reference to an optional value. - public init?(_ base: SharedReader) { - guard let initialValue = base.wrappedValue - else { return nil } - self.init( - reference: base.reference, - keyPath: base.keyPath.appending(path: \Value?.[default: DefaultSubscript(initialValue)])! - ) - } - - /// Creates a read-only shared reference from a shared reference. - /// - /// - Parameter base: A shared reference. - public init(_ base: Shared) { - self = base.reader - } - - /// Constructs a read-only shared value that remains constant. - /// - /// This can be useful for providing ``SharedReader`` values to features in previews and tests: - /// - /// ```swift - /// #Preview { - /// FeatureView( - /// store: Store( - /// initialState: Feature.State(count: .constant(42)) - /// ) { - /// Feature() - /// } - /// ) - /// ) - /// ``` - public static func constant(_ value: Value) -> Self { - Shared(value).reader - } - - /// The underlying value referenced by the shared variable. - /// - /// This property provides primary access to the value's data. However, you don't access - /// `wrappedValue` directly. Instead, you use the property variable created with the - /// ``SharedReader`` attribute. In the following example, the shared variable `topics` returns the - /// value of `wrappedValue`: - /// - /// ```swift - /// struct State { - /// @SharedReader var subscriptions: [Subscription] - /// - /// var isSubscribed: Bool { - /// !subscriptions.isEmpty - /// } - /// } - /// ``` - public var wrappedValue: Value { - func open(_ reference: some Reference) -> Value { - reference.value[ - keyPath: unsafeDowncast(self.keyPath, to: KeyPath.self) - ] - } - return open(self.reference) - } - - /// A projection of the read-only shared value that returns a shared reference. - public var projectedValue: Self { - get { - reference.access() - return self - } - set { - reference.withMutation { - self = newValue - } - } - } - - /// Returns a shared reference to the resulting value of a given key path. - public subscript( - dynamicMember keyPath: KeyPath - ) -> SharedReader { - SharedReader(reference: self.reference, keyPath: self.keyPath.appending(path: keyPath)!) - } - - @_disfavoredOverload - @available( - *, deprecated, message: "Use 'SharedReader($value.optional)' to unwrap optional shared values" - ) - public subscript( - dynamicMember keyPath: KeyPath - ) -> SharedReader? { - SharedReader(self[dynamicMember: keyPath]) - } - - #if canImport(Combine) - /// Returns a publisher that emits events when the underlying value changes. - public var publisher: AnyPublisher { - func open(_ reference: R) -> AnyPublisher { - return reference.publisher - .compactMap { $0[keyPath: self.keyPath] as? Value } - .eraseToAnyPublisher() - } - return open(self.reference) - } - #endif -} - -extension SharedReader: @unchecked Sendable where Value: Sendable {} - -extension SharedReader: Equatable where Value: Equatable { - public static func == (lhs: SharedReader, rhs: SharedReader) -> Bool { - lhs.wrappedValue == rhs.wrappedValue - } -} - -extension SharedReader: Hashable where Value: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(self.wrappedValue) - } -} - -extension SharedReader: Identifiable where Value: Identifiable { - public var id: Value.ID { - self.wrappedValue.id - } -} - -extension SharedReader: Encodable where Value: Encodable { - public func encode(to encoder: any Encoder) throws { - do { - var container = encoder.singleValueContainer() - try container.encode(self.wrappedValue) - } catch { - try self.wrappedValue.encode(to: encoder) - } - } -} - -extension SharedReader: CustomDumpRepresentable { - public var customDumpValue: Any { - self.wrappedValue - } -} - -extension SharedReader -where - Value: RandomAccessCollection & MutableCollection, - Value.Index: Hashable & Sendable, - Value.Element: Sendable -{ - /// Derives a collection of read-only shared elements from a read-only shared collection of - /// elements. - /// - /// See the documentation for [`@Shared`]()'s ``Shared/elements`` for more - /// information. - public var elements: some RandomAccessCollection> { - zip(self.wrappedValue.indices, self.wrappedValue).lazy.map { index, element in - self[index, default: DefaultSubscript(element)] - } - } -} diff --git a/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKeyPathKey.swift b/Sources/ComposableArchitecture/Sharing/AppStorageKeyPathKey.swift similarity index 73% rename from Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKeyPathKey.swift rename to Sources/ComposableArchitecture/Sharing/AppStorageKeyPathKey.swift index a4d2e2a748f8..d771ca70aebf 100644 --- a/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKeyPathKey.swift +++ b/Sources/ComposableArchitecture/Sharing/AppStorageKeyPathKey.swift @@ -1,11 +1,11 @@ import Dependencies import Foundation -extension PersistenceReaderKey { +extension SharedReaderKey { /// Creates a persistence key for sharing data in user defaults given a key path. /// /// For example, one could initialize a key with the date and time at which the application was - /// most recently launched, and access this date from anywhere using the ``Shared`` property + /// most recently launched, and access this date from anywhere using the `@Shared` property /// wrapper: /// /// ```swift @@ -14,6 +14,7 @@ extension PersistenceReaderKey { /// /// - Parameter keyPath: A string key identifying a value to share in memory. /// - Returns: A persistence key. + @available(*, deprecated, message: "Use 'appStorage' with a supported data type, instead") public static func appStorage( _ keyPath: _SendableReferenceWritableKeyPath ) -> Self where Self == AppStorageKeyPathKey { @@ -23,7 +24,8 @@ extension PersistenceReaderKey { /// A type defining a user defaults persistence strategy via key path. /// -/// See ``PersistenceReaderKey/appStorage(_:)-69h4r`` to create values of this type. +/// See ``Sharing/SharedReaderKey/appStorage(_:)`` to create values of this type. +@available(*, deprecated, message: "Use an 'AppStorageKey', instead") public struct AppStorageKeyPathKey: Sendable { private let keyPath: _SendableReferenceWritableKeyPath private let store: UncheckedSendable @@ -35,12 +37,13 @@ public struct AppStorageKeyPathKey: Sendable { } } -extension AppStorageKeyPathKey: PersistenceKey, Hashable { +@available(*, deprecated, message: "Use an 'AppStorageKey', instead") +extension AppStorageKeyPathKey: SharedKey, Hashable { public func load(initialValue _: Value?) -> Value? { self.store.wrappedValue[keyPath: self.keyPath] } - public func save(_ newValue: Value) { + public func save(_ newValue: Value, immediately: Bool) { SharedAppStorageLocals.$isSetting.withValue(true) { self.store.wrappedValue[keyPath: self.keyPath] = newValue } @@ -48,15 +51,15 @@ extension AppStorageKeyPathKey: PersistenceKey, Hashable { public func subscribe( initialValue: Value?, - didSet: @escaping @Sendable (_ newValue: Value?) -> Void - ) -> Shared.Subscription { + didSet receiveValue: @escaping @Sendable (_ newValue: Value?) -> Void + ) -> SharedSubscription { let observer = self.store.wrappedValue.observe(self.keyPath, options: .new) { _, change in guard !SharedAppStorageLocals.isSetting else { return } - didSet(change.newValue ?? initialValue) + receiveValue(change.newValue ?? initialValue) } - return Shared.Subscription { + return SharedSubscription { observer.invalidate() } } diff --git a/Sources/ComposableArchitecture/TestStore.swift b/Sources/ComposableArchitecture/TestStore.swift index b0a03d15c193..8e774a4be2eb 100644 --- a/Sources/ComposableArchitecture/TestStore.swift +++ b/Sources/ComposableArchitecture/TestStore.swift @@ -5,6 +5,7 @@ import CustomDump @_spi(Beta) import Dependencies import Foundation import IssueReporting +@_spi(SharedChangeTracking) import Sharing /// A testable runtime for a reducer. /// @@ -542,7 +543,7 @@ public final class TestStore { let sharedChangeTracker = SharedChangeTracker() let reducer = Dependencies.withDependencies { prepareDependencies(&$0) - $0.sharedChangeTrackers.insert(sharedChangeTracker) + sharedChangeTracker.track(&$0) } operation: { TestReducer(Reduce(reducer()), initialState: initialState()) } @@ -753,7 +754,7 @@ public final class TestStore { ) } open(stateType) - self.sharedChangeTracker.resetChanges() + self.sharedChangeTracker.reset() } } @@ -988,14 +989,12 @@ extension TestStore where State: Equatable { let expectedState = self.state let previousState = self.reducer.state let previousStackElementID = self.reducer.dependencies.stackElementID.incrementingCopy() - let task = self.sharedChangeTracker.track { - self.store.send( - .init( - origin: .send(action), fileID: fileID, filePath: filePath, line: line, column: column - ), - originatingFrom: nil - ) - } + let task = self.store.send( + .init( + origin: .send(action), fileID: fileID, filePath: filePath, line: line, column: column + ), + originatingFrom: nil + ) if uncheckedUseMainSerialExecutor { await Task.yield() } else { @@ -1121,7 +1120,7 @@ extension TestStore where State: Equatable { skipUnnecessaryModifyFailure || self.sharedChangeTracker.hasChanges == true if self.exhaustivity != .on { - self.sharedChangeTracker.resetChanges() + self.sharedChangeTracker.reset() } let current = expected @@ -1148,9 +1147,10 @@ extension TestStore where State: Equatable { if let updateStateToExpectedResult { try Dependencies.withDependencies { $0 = self.reducer.dependencies - $0.sharedChangeTracker = self.sharedChangeTracker } operation: { - try updateStateToExpectedResult(&expectedWhenGivenPreviousState) + try self.sharedChangeTracker.assert { + try updateStateToExpectedResult(&expectedWhenGivenPreviousState) + } } } expected = expectedWhenGivenPreviousState @@ -1166,9 +1166,10 @@ extension TestStore where State: Equatable { if let updateStateToExpectedResult { try Dependencies.withDependencies { $0 = self.reducer.dependencies - $0.sharedChangeTracker = self.sharedChangeTracker } operation: { - try updateStateToExpectedResult(&expectedWhenGivenActualState) + try self.sharedChangeTracker.assert { + try updateStateToExpectedResult(&expectedWhenGivenActualState) + } } } expected = expectedWhenGivenActualState @@ -1186,9 +1187,10 @@ extension TestStore where State: Equatable { do { try Dependencies.withDependencies { $0 = self.reducer.dependencies - $0.sharedChangeTracker = self.sharedChangeTracker } operation: { - try updateStateToExpectedResult(&expectedWhenGivenPreviousState) + try self.sharedChangeTracker.assert { + try updateStateToExpectedResult(&expectedWhenGivenPreviousState) + } } } catch { reportIssue( @@ -1269,7 +1271,7 @@ extension TestStore where State: Equatable { column: column ) } - self.sharedChangeTracker.resetChanges() + self.sharedChangeTracker.reset() } } } diff --git a/Tests/ComposableArchitectureTests/AppStorageTests.swift b/Tests/ComposableArchitectureTests/AppStorageTests.swift deleted file mode 100644 index 2bc2ef3ce67a..000000000000 --- a/Tests/ComposableArchitectureTests/AppStorageTests.swift +++ /dev/null @@ -1,364 +0,0 @@ -@_spi(Internals) import ComposableArchitecture -import Perception -import XCTest - -final class AppStorageTests: XCTestCase { - func testBasics() { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("count")) var count = 0 - XCTAssertEqual(count, 0) - XCTAssertEqual(defaults.integer(forKey: "count"), 0) - - count += 1 - XCTAssertEqual(count, 1) - XCTAssertEqual(defaults.integer(forKey: "count"), 1) - } - - func testDefaultsRegistered() { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("count")) var count = 42 - XCTAssertEqual(defaults.integer(forKey: "count"), 42) - - count += 1 - XCTAssertEqual(count, 43) - XCTAssertEqual(defaults.integer(forKey: "count"), 43) - } - - func testDefaultsReadURL() { - @Dependency(\.defaultAppStorage) var defaults - defaults.set(URL(string: "https://pointfree.co"), forKey: "url") - @Shared(.appStorage("url")) var url: URL? - XCTAssertEqual(url, URL(string: "https://pointfree.co")) - } - - func testDefaultsRegistered_URL() { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("url")) var url: URL = URL(string: "https://pointfree.co")! - XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://pointfree.co")!) - - url = URL(string: "https://example.com")! - XCTAssertEqual(url, URL(string: "https://example.com")!) - XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://example.com")!) - } - - func testDefaultsRegistered_Optional_URL() { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("url")) var url: URL? = URL(string: "https://pointfree.co") - XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://pointfree.co")) - - url = URL(string: "https://example.com")! - XCTAssertEqual(url, URL(string: "https://example.com")) - XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://example.com")) - } - - func testDefaultsReadDate() { - let expectedDate = Date() - @Dependency(\.defaultAppStorage) var defaults - defaults.set(expectedDate, forKey: "date") - @Shared(.appStorage("date")) var date: Date? - XCTAssertEqual(date, expectedDate) - } - - func testDefaultsRegistered_Date() { - let expectedDate = Date() - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("date")) var date: Date = expectedDate - XCTAssertEqual(defaults.object(forKey: "date") as? Date, expectedDate) - - let newDate = Date().addingTimeInterval(60) - date = newDate - XCTAssertEqual(date, newDate) - XCTAssertEqual(defaults.object(forKey: "date") as? Date, newDate) - } - - func testDefaultsRegistered_Optional_Date() { - let initialDate: Date? = Date() - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("date")) var date: Date? = initialDate - XCTAssertEqual(defaults.object(forKey: "date") as? Date, initialDate) - - let newDate = Date().addingTimeInterval(60) - date = newDate - XCTAssertEqual(date, newDate) - XCTAssertEqual(defaults.object(forKey: "date") as? Date, newDate) - } - - func testDefaultsRegistered_Optional() { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("data")) var data: Data? - XCTAssertEqual(defaults.data(forKey: "data"), nil) - - data = Data() - XCTAssertEqual(data, Data()) - XCTAssertEqual(defaults.data(forKey: "data"), Data()) - } - - func testDefaultsRegistered_RawRepresentable() { - enum Direction: String, CaseIterable { - case north, south, east, west - } - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("direction")) var direction: Direction = .north - XCTAssertEqual(defaults.string(forKey: "direction"), "north") - - direction = .south - XCTAssertEqual(defaults.string(forKey: "direction"), "south") - } - - func testDefaultsRegistered_Optional_RawRepresentable() { - enum Direction: String, CaseIterable { - case north, south, east, west - } - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("direction")) var direction: Direction? - XCTAssertEqual(defaults.string(forKey: "direction"), nil) - - direction = .south - XCTAssertEqual(defaults.string(forKey: "direction"), "south") - } - - func testDefaultAppStorageOverride() { - let defaults = UserDefaults(suiteName: "tests")! - defaults.removePersistentDomain(forName: "tests") - - withDependencies { - $0.defaultAppStorage = defaults - } operation: { - @Shared(.appStorage("count")) var count = 0 - count += 1 - XCTAssertEqual(defaults.integer(forKey: "count"), 1) - } - - @Dependency(\.defaultAppStorage) var defaultAppStorage - XCTAssertNotEqual(defaultAppStorage, defaults) - XCTAssertEqual(defaultAppStorage.integer(forKey: "count"), 0) - } - - func testObservation_DirectMutation() { - @Shared(.appStorage("count")) var count = 0 - let countDidChange = self.expectation(description: "countDidChange") - withPerceptionTracking { - _ = count - } onChange: { - countDidChange.fulfill() - } - count += 1 - self.wait(for: [countDidChange], timeout: 0) - } - - func testObservation_ExternalMutation() { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("count")) var count = 0 - let didChange = self.expectation(description: "didChange") - - withPerceptionTracking { - _ = count - } onChange: { [count = $count] in - XCTAssertEqual(count.wrappedValue, 0) - didChange.fulfill() - } - - defaults.setValue(42, forKey: "count") - self.wait(for: [didChange], timeout: 0) - XCTAssertEqual(count, 42) - } - - func testChangeUserDefaultsDirectly() { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("count")) var count = 0 - defaults.setValue(count + 42, forKey: "count") - XCTAssertEqual(count, 42) - } - - func testChangeUserDefaultsDirectly_RawRepresentable() { - enum Direction: String, CaseIterable { - case north, south, east, west - } - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("direction")) var direction: Direction = .south - defaults.set("east", forKey: "direction") - XCTAssertEqual(direction, .east) - } - - func testChangeUserDefaultsDirectly_KeyWithPeriod() { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("pointfreeco.count")) var count = 0 - defaults.setValue(count + 42, forKey: "pointfreeco.count") - XCTAssertEqual(count, 42) - } - - func testDeleteUserDefault() { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("count")) var count = 0 - count = 42 - defaults.removeObject(forKey: "count") - XCTAssertEqual(count, 0) - } - - func testKeyPath() { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage(\.count)) var count = 0 - defaults.count += 1 - XCTAssertEqual(count, 1) - } - - func testOptionalInitializers() { - @Shared(.appStorage("count1")) var count1: Int? - XCTAssertEqual(count1, nil) - @Shared(.appStorage("count")) var count2: Int? = nil - XCTAssertEqual(count2, nil) - } - - func testOptionalInitializers_URL() { - @Shared(.appStorage("url1")) var url1: URL? - XCTAssertEqual(url1, nil) - @Shared(.appStorage("url2")) var url2: URL? = nil - XCTAssertEqual(url2, nil) - } - - func testOptionalInitializers_Date() { - @Shared(.appStorage("date1")) var date1: Date? - XCTAssertEqual(date1, nil) - @Shared(.appStorage("date2")) var date2: Date? = nil - XCTAssertEqual(date2, nil) - } - - func testRemoveDuplicates() { - @Dependency(\.defaultAppStorage) var store - @Shared(.appStorage("count")) var count = 0 - - let values = LockIsolated([Int]()) - let cancellable = $count - .publisher - .sink { count in values.withValue { $0.append(count) } } - defer { _ = cancellable } - - count += 1 - XCTAssertEqual(values.value, [1]) - - store.setValue(2, forKey: "other-count") - XCTAssertEqual(values.value, [1]) - } - - func testUpdateStoreFromBackgroundThread() async throws { - @Dependency(\.defaultAppStorage) var store - @Shared(.appStorage("count")) var count = 0 - - let publisherExpectation = expectation(description: "publisher") - let cancellable = $count.publisher.sink { _ in - XCTAssertTrue(Thread.isMainThread) - publisherExpectation.fulfill() - } - defer { _ = cancellable } - - let perceptionExpectation = self.expectation(description: "perception") - withPerceptionTracking { - _ = count - } onChange: { - XCTAssertTrue(Thread.isMainThread) - perceptionExpectation.fulfill() - } - - await withUnsafeContinuation { continuation in - DispatchQueue.global().async { [store = UncheckedSendable(store)] in - XCTAssertFalse(Thread.isMainThread) - store.wrappedValue.setValue(1, forKey: "count") - continuation.resume() - } - } - - await fulfillment(of: [perceptionExpectation, publisherExpectation], timeout: 1) - } - - @MainActor - func testUpdateStoreFromMainThread() async throws { - @Dependency(\.defaultAppStorage) var store - @Shared(.appStorage("count")) var count = 0 - let isInStackFrame = LockIsolated(false) - - let publisherExpectation = expectation(description: "publisher") - let cancellable = $count.publisher.sink { _ in - XCTAssertTrue(Thread.isMainThread) - XCTAssertTrue(isInStackFrame.value) - publisherExpectation.fulfill() - } - defer { _ = cancellable } - - await withUnsafeContinuation { continuation in - XCTAssertTrue(Thread.isMainThread) - isInStackFrame.withValue { $0 = true } - store.setValue(1, forKey: "count") - isInStackFrame.withValue { $0 = false } - continuation.resume() - } - - await fulfillment(of: [publisherExpectation], timeout: 0) - } - - func testWillEnterForegroundFromBackgroundThread() async throws { - @Shared(.appStorage("count")) var count = 0 - - let publisherExpectation = expectation(description: "publisher") - let cancellable = $count.publisher.sink { _ in - XCTAssertTrue(Thread.isMainThread) - publisherExpectation.fulfill() - } - defer { _ = cancellable } - - let perceptionExpectation = self.expectation(description: "perception") - withPerceptionTracking { - _ = count - } onChange: { - XCTAssertTrue(Thread.isMainThread) - perceptionExpectation.fulfill() - } - - await withUnsafeContinuation { continuation in - DispatchQueue.global().async { - XCTAssertFalse(Thread.isMainThread) - NotificationCenter.default.post(name: willEnterForegroundNotificationName!, object: nil) - continuation.resume() - } - } - - await fulfillment(of: [perceptionExpectation, publisherExpectation], timeout: 1) - } - - func testUpdateStoreFromBackgroundThread_SendableKeyPath() async throws { - @Dependency(\.defaultAppStorage) var store - @Shared(.appStorage(\.count)) var count = 0 - - let publisherExpectation = expectation(description: "publisher") - publisherExpectation.expectedFulfillmentCount = 2 - let cancellable = $count.publisher.sink { _ in - XCTAssertTrue(Thread.isMainThread) - publisherExpectation.fulfill() - } - defer { _ = cancellable } - - let perceptionExpectation = self.expectation(description: "perception") - withPerceptionTracking { - _ = count - } onChange: { - XCTAssertTrue(Thread.isMainThread) - perceptionExpectation.fulfill() - } - - await withUnsafeContinuation { continuation in - DispatchQueue.global().async { [store = UncheckedSendable(store)] in - XCTAssertFalse(Thread.isMainThread) - store.wrappedValue.count = 1 - continuation.resume() - } - } - - await fulfillment(of: [perceptionExpectation, publisherExpectation], timeout: 1) - } -} - -extension UserDefaults { - @objc fileprivate dynamic var count: Int { - get { integer(forKey: "count") } - set { set(newValue, forKey: "count") } - } -} diff --git a/Tests/ComposableArchitectureTests/DebugTests.swift b/Tests/ComposableArchitectureTests/DebugTests.swift index 6342d069ccd2..16e8c2bb7ca6 100644 --- a/Tests/ComposableArchitectureTests/DebugTests.swift +++ b/Tests/ComposableArchitectureTests/DebugTests.swift @@ -167,9 +167,9 @@ @Shared var count: Int } - let store = await Store(initialState: State(count: Shared(0))) { + let store = await Store(initialState: State(count: Shared(value: 0))) { Reduce(internal: { state, action in - state.count += action ? 1 : -1 + state.$count.withLock { $0 += action ? 1 : -1 } return .none }) ._printChanges(printer) diff --git a/Tests/ComposableArchitectureTests/FileStorageTests.swift b/Tests/ComposableArchitectureTests/FileStorageTests.swift deleted file mode 100644 index 60daccb7fd1c..000000000000 --- a/Tests/ComposableArchitectureTests/FileStorageTests.swift +++ /dev/null @@ -1,576 +0,0 @@ -@_spi(Internals) import ComposableArchitecture -import Perception -import XCTest - -@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) -final class FileStorageTests: XCTestCase { - func testBasics() throws { - let fileSystem = LockIsolated<[URL: Data]>([:]) - try withDependencies { - $0.defaultFileStorage = .inMemory(fileSystem: fileSystem, scheduler: .immediate) - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - expectNoDifference(fileSystem.value, [.fileURL: Data()]) - users.append(.blob) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - } - } - - func testBasics_CustomDecodeEncodeClosures() { - let fileSystem = LockIsolated<[URL: Data]>([:]) - withDependencies { - $0.defaultFileStorage = .inMemory(fileSystem: fileSystem, scheduler: .immediate) - } operation: { - @Shared(.utf8String) var string = "" - expectNoDifference(fileSystem.value, [.utf8StringURL: Data()]) - string = "hello" - expectNoDifference( - fileSystem.value[.utf8StringURL].map { String(decoding: $0, as: UTF8.self) }, - "hello" - ) - } - } - - func testThrottle() throws { - let fileSystem = LockIsolated<[URL: Data]>([:]) - let testScheduler = DispatchQueue.test - try withDependencies { - $0.defaultFileStorage = .inMemory( - fileSystem: fileSystem, - scheduler: testScheduler.eraseToAnyScheduler() - ) - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - try expectNoDifference(fileSystem.value.users(for: .fileURL), nil) - - users.append(.blob) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - - users.append(.blobJr) - testScheduler.advance(by: .seconds(1) - .milliseconds(1)) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - - users.append(.blobSr) - testScheduler.advance(by: .milliseconds(1)) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr, .blobSr]) - - testScheduler.advance(by: .seconds(1)) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr, .blobSr]) - - testScheduler.advance(by: .seconds(0.5)) - users.append(.blobEsq) - try expectNoDifference( - fileSystem.value.users(for: .fileURL), - [ - .blob, - .blobJr, - .blobSr, - .blobEsq, - ] - ) - } - } - - func testNoThrottling() throws { - let fileSystem = LockIsolated<[URL: Data]>([:]) - let testScheduler = DispatchQueue.test - try withDependencies { - $0.defaultFileStorage = .inMemory( - fileSystem: fileSystem, - scheduler: testScheduler.eraseToAnyScheduler() - ) - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - try expectNoDifference(fileSystem.value.users(for: .fileURL), nil) - - users.append(.blob) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - - testScheduler.advance(by: .seconds(2)) - users.append(.blobJr) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr]) - } - } - - func testWillResign() throws { - guard let willResignNotificationName else { return } - - let fileSystem = LockIsolated<[URL: Data]>([:]) - let testScheduler = DispatchQueue.test - try withDependencies { - $0.defaultFileStorage = .inMemory( - fileSystem: fileSystem, - scheduler: testScheduler.eraseToAnyScheduler() - ) - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - try expectNoDifference(fileSystem.value.users(for: .fileURL), nil) - - users.append(.blob) - users.append(.blobJr) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - - NotificationCenter.default.post(name: willResignNotificationName, object: nil) - testScheduler.advance() - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr]) - } - } - - func testWillTerminate() throws { - guard let willTerminateNotificationName else { return } - - let fileSystem = LockIsolated<[URL: Data]>([:]) - let testScheduler = DispatchQueue.test - try withDependencies { - $0.defaultFileStorage = .inMemory( - fileSystem: fileSystem, - scheduler: testScheduler.eraseToAnyScheduler() - ) - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - try expectNoDifference(fileSystem.value.users(for: .fileURL), nil) - - users.append(.blob) - users.append(.blobJr) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - - NotificationCenter.default.post(name: willTerminateNotificationName, object: nil) - testScheduler.advance() - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr]) - } - } - - func testMultipleFiles() throws { - let fileSystem = LockIsolated<[URL: Data]>([:]) - try withDependencies { - $0.defaultFileStorage = .inMemory(fileSystem: fileSystem) - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - @Shared(.fileStorage(.anotherFileURL)) var otherUsers = [User]() - - users.append(.blob) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - try expectNoDifference(fileSystem.value.users(for: .anotherFileURL), nil) - - otherUsers.append(.blobJr) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - try expectNoDifference(fileSystem.value.users(for: .anotherFileURL), [.blobJr]) - } - } - - func testLivePersistence() async throws { - guard let willResignNotificationName else { return } - try? FileManager.default.removeItem(at: .fileURL) - - try await withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - - await $users.withLock { $0.append(.blob) } - NotificationCenter.default - .post(name: willResignNotificationName, object: nil) - await Task.yield() - - try expectNoDifference( - JSONDecoder().decode([User].self, from: Data(contentsOf: .fileURL)), - [.blob] - ) - } - } - - func testInitialValue() async throws { - try await withMainSerialExecutor { - let fileSystem = try LockIsolated<[URL: Data]>( - [.fileURL: try JSONEncoder().encode([User.blob])] - ) - try await withDependencies { - $0.defaultFileStorage = .inMemory(fileSystem: fileSystem) - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - _ = users - await Task.yield() - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - } - } - } - - func testInitialValue_LivePersistence() async throws { - try await withMainSerialExecutor { - try? FileManager.default.removeItem(at: .fileURL) - try JSONEncoder().encode([User.blob]).write(to: .fileURL) - - try await withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - _ = users - await Task.yield() - try expectNoDifference( - JSONDecoder().decode([User].self, from: Data(contentsOf: .fileURL)), - [.blob] - ) - } - } - } - - func testWriteFile() async throws { - try await withMainSerialExecutor { - try? FileManager.default.removeItem(at: .fileURL) - try JSONEncoder().encode([User.blob]).write(to: .fileURL) - - try await withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - await Task.yield() - expectNoDifference(users, [.blob]) - - try JSONEncoder().encode([User.blobJr]).write(to: .fileURL) - try await Task.sleep(nanoseconds: 10_000_000) - expectNoDifference(users, [.blobJr]) - } - } - } - - func testWriteFileWhileThrottling() throws { - let fileSystem = LockIsolated<[URL: Data]>([:]) - let scheduler = DispatchQueue.test - let fileStorage = FileStorage.inMemory( - fileSystem: fileSystem, - scheduler: scheduler.eraseToAnyScheduler() - ) - - try withDependencies { - $0.defaultFileStorage = fileStorage - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - - users.append(.blob) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - - try fileStorage.save(Data(), .fileURL) - scheduler.run() - expectNoDifference(users, [.blob]) - try expectNoDifference(fileSystem.value.users(for: .fileURL), nil) - } - } - - func testDeleteFile() async throws { - try await withMainSerialExecutor { - try? FileManager.default.removeItem(at: .fileURL) - try JSONEncoder().encode([User.blob]).write(to: .fileURL) - - try await withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - await Task.yield() - expectNoDifference(users, [.blob]) - - try FileManager.default.removeItem(at: .fileURL) - try await Task.sleep(nanoseconds: 1_000_000) - expectNoDifference(users, []) - } - } - } - - func testMoveFile() async throws { - try await withMainSerialExecutor { - try? FileManager.default.removeItem(at: .fileURL) - try? FileManager.default.removeItem(at: .anotherFileURL) - try JSONEncoder().encode([User.blob]).write(to: .fileURL) - - try await withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - await Task.yield() - expectNoDifference(users, [.blob]) - - try FileManager.default.moveItem(at: .fileURL, to: .anotherFileURL) - try await Task.sleep(nanoseconds: 1_000_000) - expectNoDifference(users, []) - - try FileManager.default.removeItem(at: .fileURL) - try FileManager.default.moveItem(at: .anotherFileURL, to: .fileURL) - try await Task.sleep(nanoseconds: 1_000_000) - expectNoDifference(users, [.blob]) - } - } - } - - func testDeleteFile_ThenWriteToFile() async throws { - try await withMainSerialExecutor { - try? FileManager.default.removeItem(at: .fileURL) - try JSONEncoder().encode([User.blob]).write(to: .fileURL) - - try await withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User]() - await Task.yield() - expectNoDifference(users, [.blob]) - - try FileManager.default.removeItem(at: .fileURL) - try await Task.sleep(nanoseconds: 1_000_000) - expectNoDifference(users, []) - - try JSONEncoder().encode([User.blobJr]).write(to: .fileURL) - try await Task.sleep(nanoseconds: 1_000_000) - expectNoDifference(users, [.blobJr]) - } - } - } - - func testMismatchTypes() { - XCTAssertEqual( - FileStorageKey.fileStorage(.fileURL).id, - FileStorageKey.fileStorage(.fileURL).id - ) - XCTAssertNotEqual( - FileStorageKey.fileStorage(.fileURL).id, - FileStorageKey.fileStorage(.anotherFileURL).id - ) - XCTAssertNotEqual( - FileStorageKey.fileStorage(.fileURL).id, - withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - FileStorageKey.fileStorage(.fileURL).id - } - ) - - XCTAssertEqual( - AppStorageKey.appStorage("key").id, - AppStorageKey.appStorage("key").id - ) - XCTAssertNotEqual( - AppStorageKey.appStorage("key").id, - AppStorageKey.appStorage("key2").id - ) - XCTAssertNotEqual( - AppStorageKey.appStorage("key").id, - withDependencies { - $0.defaultAppStorage = UserDefaults(suiteName: "\(NSTemporaryDirectory())test-mismatch")! - } operation: { - AppStorageKey.appStorage("key").id - } - ) - - XCTAssertEqual( - InMemoryKey.inMemory("key").id, - InMemoryKey.inMemory("key").id - ) - XCTAssertNotEqual( - InMemoryKey.inMemory("key").id, - InMemoryKey.inMemory("key2").id - ) - XCTAssertNotEqual( - InMemoryKey.inMemory("key").id, - withDependencies { - $0.defaultInMemoryStorage = InMemoryStorage() - } operation: { - InMemoryKey.inMemory("key").id - } - ) - } - - func testTwoInMemoryFileStorages() { - let shared1 = withDependencies { - $0.defaultFileStorage = .inMemory - } operation: { - @Shared(.fileStorage(.userURL)) var user = User(id: 1, name: "Blob") - return $user - } - let shared2 = withDependencies { - $0.defaultFileStorage = .inMemory - } operation: { - @Shared(.fileStorage(.userURL)) var user = User(id: 1, name: "Blob") - return $user - } - - shared1.wrappedValue.name = "Blob Jr" - XCTAssertEqual(shared1.wrappedValue.name, "Blob Jr") - XCTAssertEqual(shared2.wrappedValue.name, "Blob") - shared2.wrappedValue.name = "Blob Sr" - XCTAssertEqual(shared1.wrappedValue.name, "Blob Jr") - XCTAssertEqual(shared2.wrappedValue.name, "Blob Sr") - } - - func testCancelThrottleWhenFileIsDeleted() async throws { - try await withMainSerialExecutor { - try? FileManager.default.removeItem(at: .fileURL) - - try await withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - @Shared(.fileStorage(.fileURL)) var users = [User.blob] - await Task.yield() - expectNoDifference(users, [.blob]) - - await $users.withLock { $0 = [.blobJr] } // NB: Saved immediately - await $users.withLock { $0 = [.blobSr] } // NB: Throttled for 1 second - try FileManager.default.removeItem(at: .fileURL) - try await Task.sleep(nanoseconds: 1_200_000_000) - expectNoDifference(users, [.blob]) - try XCTAssertEqual(Data(contentsOf: .fileURL), Data()) - } - } - } - - func testWritesFromManyThreads() async { - let fileSystem = LockIsolated<[URL: Data]>([:]) - let fileStorage = FileStorage.inMemory( - fileSystem: fileSystem, - scheduler: DispatchQueue.main.eraseToAnyScheduler() - ) - - await withDependencies { - $0.defaultFileStorage = fileStorage - } operation: { - @Shared(.fileStorage(.fileURL)) var count = 0 - let max = 10_000 - await withTaskGroup(of: Void.self) { group in - for index in (1...max) { - group.addTask { [count = $count] in - try? await Task.sleep(for: .milliseconds(Int.random(in: 200...3_000))) - await count.withLock { $0 += index } - } - } - } - - XCTAssertEqual(count, max * (max + 1) / 2) - } - } - - @MainActor - func testUpdateFileSystemFromBackgroundThread() async throws { - await withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - try? FileManager.default.removeItem(at: .fileURL) - - @Shared(.fileStorage(.fileURL)) var count = 0 - - let publisherExpectation = expectation(description: "publisher") - let cancellable = $count.publisher.sink { _ in - XCTAssertTrue(Thread.isMainThread) - publisherExpectation.fulfill() - } - defer { _ = cancellable } - - await withUnsafeContinuation { continuation in - DispatchQueue.global().async { - XCTAssertFalse(Thread.isMainThread) - try! Data("1".utf8).write(to: .fileURL) - continuation.resume() - } - } - - await fulfillment(of: [publisherExpectation], timeout: 1) - } - } - - @MainActor - func testMultipleMutations() async throws { - try? FileManager.default.removeItem( - at: URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("counts.json") - ) - - try await withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - @Shared(.counts) var counts - for m in 1...1000 { - for n in 1...10 { - $counts.withLock { - $0[n, default: 0] += 1 - } - } - expectNoDifference( - Dictionary((1...10).map { n in (n, m) }, uniquingKeysWith: { $1 }), - counts - ) - try await Task.sleep(for: .seconds(0.001)) - } - } - } - - func testMultipleMutationsFromMultipleThreads() async throws { - try? FileManager.default.removeItem( - at: URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("counts.json") - ) - - await withDependencies { - $0.defaultFileStorage = .fileSystem - } operation: { - @Shared(.counts) var counts - - await withTaskGroup(of: Void.self) { group in - for _ in 1...1000 { - group.addTask { [$counts] in - for _ in 1...10 { - await $counts.withLock { $0[0, default: 0] += 1 } - try? await Task.sleep(for: .seconds(0.2)) - } - } - } - } - - XCTAssertEqual(counts[0], 10_000) - } - } -} - -extension PersistenceReaderKey -where Self == FileStorageKey { - fileprivate static var utf8String: Self { - .fileStorage( - .utf8StringURL, - decode: { data in String(decoding: data, as: UTF8.self) }, - encode: { string in Data(string.utf8) } - ) - } -} - -extension URL { - fileprivate static let fileURL = Self(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("file.json") - fileprivate static let userURL = Self(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("user.json") - fileprivate static let anotherFileURL = Self(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("another-file.json") - fileprivate static let utf8StringURL = Self(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("utf8-string.json") -} - -private struct User: Codable, Equatable, Identifiable { - let id: Int - var name: String - static let blob = User(id: 1, name: "Blob") - static let blobJr = User(id: 2, name: "Blob Jr.") - static let blobSr = User(id: 3, name: "Blob Sr.") - static let blobEsq = User(id: 4, name: "Blob Esq.") -} - -extension [URL: Data] { - fileprivate func users(for url: URL) throws -> [User]? { - guard - let data = self[url], - !data.isEmpty - else { return nil } - return try JSONDecoder().decode([User].self, from: data) - } -} - -extension PersistenceKey where Self == PersistenceKeyDefault> { - fileprivate static var counts: Self { - Self( - .fileStorage( - URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("counts.json") - ), - [:] - ) - } -} diff --git a/Tests/ComposableArchitectureTests/SharedAppStorageTests.swift b/Tests/ComposableArchitectureTests/SharedAppStorageTests.swift deleted file mode 100644 index 7d5ee766a951..000000000000 --- a/Tests/ComposableArchitectureTests/SharedAppStorageTests.swift +++ /dev/null @@ -1,124 +0,0 @@ -import ComposableArchitecture -import XCTest - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -final class SharedAppStorageTests: XCTestCase { - func testBasics() async { - let store = await TestStore(initialState: Feature.State()) { - Feature() - } - - await store.send(.incrementButtonTapped) { - $0.count = 1 - } - } - - func testSubscription() async throws { - let store = await TestStore(initialState: Feature.State()) { - Feature() - } - - await store.send(.incrementButtonTapped) { - $0.count = 1 - } - @Dependency(\.defaultAppStorage) var userDefaults - userDefaults.setValue(42, forKey: "count") - await Task.yield() - await store.send(.incrementButtonTapped) { - $0.count = 43 - } - } - - func testSiblings() async { - let store = await TestStore(initialState: ParentFeature.State()) { - ParentFeature() - } - - await store.send(.child1(.incrementButtonTapped)) { - $0.child1.count = 1 - XCTAssertEqual($0.child2.count, 1) - } - await store.send(.child2(.incrementButtonTapped)) { - $0.child2.count = 2 - XCTAssertEqual($0.child1.count, 2) - } - await store.send(.child1(.incrementButtonTapped)) { - $0.child2.count = 3 - XCTAssertEqual($0.child1.count, 3) - } - await store.send(.child2(.incrementButtonTapped)) { - $0.child1.count = 4 - XCTAssertEqual($0.child2.count, 4) - } - } - - @MainActor - func testSiblings_Failure() async { - let store = TestStore(initialState: ParentFeature.State()) { - ParentFeature() - } - - XCTExpectFailure { - $0.compactDescription == """ - failed - State was not expected to change, but a change occurred: … - -   ParentFeature.State( -   _child1: Feature.State( - − _count: #1 0 - + _count: #1 1 -   ), -   _child2: Feature.State( - − _count: #1 Int(↩︎) - + _count: #1 Int(↩︎) -   ) -   ) - - (Expected: −, Actual: +) - """ - } - await store.send(.child1(.incrementButtonTapped)) - } -} - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -@Reducer -private struct ParentFeature { - @ObservableState - struct State: Equatable { - var child1 = Feature.State() - var child2 = Feature.State() - } - enum Action { - case child1(Feature.Action) - case child2(Feature.Action) - } - var body: some ReducerOf { - Scope(state: \.child1, action: \.child1) { - Feature() - } - Scope(state: \.child2, action: \.child2) { - Feature() - } - } -} - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -@Reducer -private struct Feature { - @ObservableState - struct State: Equatable { - @Shared(.appStorage("count")) var count = 0 - } - enum Action { - case incrementButtonTapped - } - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .incrementButtonTapped: - state.count += 1 - return .none - } - } - } -} diff --git a/Tests/ComposableArchitectureTests/SharedInMemoryTests.swift b/Tests/ComposableArchitectureTests/SharedInMemoryTests.swift deleted file mode 100644 index 58cd15c8d4a9..000000000000 --- a/Tests/ComposableArchitectureTests/SharedInMemoryTests.swift +++ /dev/null @@ -1,127 +0,0 @@ -import ComposableArchitecture -import XCTest - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -final class SharedInMemoryTests: XCTestCase { - func testBasics() async { - let store = await TestStore(initialState: Feature.State()) { - Feature() - } - - await store.send(.incrementButtonTapped) { - $0.count = 1 - } - } - - func testSiblings() async { - let store = await TestStore(initialState: ParentFeature.State()) { - ParentFeature() - } - - await store.send(.child1(.incrementButtonTapped)) { - $0.child1.count = 1 - XCTAssertEqual($0.child2.count, 1) - XCTAssertEqual($0.child3.count, 0) - } - await store.send(.child2(.incrementButtonTapped)) { - $0.child2.count = 2 - XCTAssertEqual($0.child1.count, 2) - XCTAssertEqual($0.child3.count, 0) - } - await store.send(.child1(.incrementButtonTapped)) { - $0.child2.count = 3 - XCTAssertEqual($0.child1.count, 3) - XCTAssertEqual($0.child3.count, 0) - } - await store.send(.child2(.incrementButtonTapped)) { - $0.child1.count = 4 - XCTAssertEqual($0.child2.count, 4) - XCTAssertEqual($0.child3.count, 0) - } - await store.send(.child3(.incrementButtonTapped)) { - $0.child3.count = 1 - XCTAssertEqual($0.child1.count, 4) - XCTAssertEqual($0.child2.count, 4) - } - } - - @MainActor - func testSiblings_Failure() async { - let store = TestStore(initialState: ParentFeature.State()) { - ParentFeature() - } - - XCTExpectFailure { - $0.compactDescription == """ - failed - State was not expected to change, but a change occurred: … - -   ParentFeature.State( -   _child1: Feature.State( - − _count: #1 0 - + _count: #1 1 -   ), -   _child2: Feature.State( - − _count: #1 Int(↩︎) - + _count: #1 Int(↩︎) -   ), -   _child3: Feature.State(…) -   ) - - (Expected: −, Actual: +) - """ - } - await store.send(.child1(.incrementButtonTapped)) - } -} - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -@Reducer -private struct ParentFeature { - @ObservableState - struct State: Equatable { - var child1 = Feature.State() - var child2 = Feature.State() - var child3 = withDependencies { - $0.defaultInMemoryStorage = .init() - } operation: { - Feature.State() - } - } - enum Action { - case child1(Feature.Action) - case child2(Feature.Action) - case child3(Feature.Action) - } - var body: some ReducerOf { - Scope(state: \.child1, action: \.child1) { - Feature() - } - Scope(state: \.child2, action: \.child2) { - Feature() - } - Scope(state: \.child3, action: \.child3) { - Feature() - } - } -} - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -@Reducer -private struct Feature { - @ObservableState - struct State: Equatable { - @Shared(.inMemory("count")) var count = 0 - } - enum Action { - case incrementButtonTapped - } - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .incrementButtonTapped: - state.count += 1 - return .none - } - } - } -} diff --git a/Tests/ComposableArchitectureTests/SharedReaderTests.swift b/Tests/ComposableArchitectureTests/SharedReaderTests.swift deleted file mode 100644 index 5bd47418d71b..000000000000 --- a/Tests/ComposableArchitectureTests/SharedReaderTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Combine -import ComposableArchitecture -import XCTest - -final class SharedReaderTests: XCTestCase { - func testSharedReader() { - @Shared var count: Int - _count = Shared(0) - let countReader = $count.reader - - count += 1 - XCTAssertEqual(count, 1) - XCTAssertEqual(countReader.wrappedValue, 1) - } -} diff --git a/Tests/ComposableArchitectureTests/SharedTests.swift b/Tests/ComposableArchitectureTests/SharedTests.swift deleted file mode 100644 index 7aa8ba5c48b2..000000000000 --- a/Tests/ComposableArchitectureTests/SharedTests.swift +++ /dev/null @@ -1,1339 +0,0 @@ -import Combine -@_spi(Internals) import ComposableArchitecture -import CustomDump -import XCTest - -final class SharedTests: XCTestCase { - @MainActor - func testSharing() async { - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } - await store.send(.sharedIncrement) { - $0.sharedCount = 1 - } - await store.send(.incrementStats) { - $0.profile.stats.count = 1 - $0.stats.count = 1 - } - XCTAssertEqual(store.state.profile.stats.count, 1) - } - - @MainActor - func testSharingWithDelegateAction() async { - XCTTODO( - """ - Ideally this test would pass but is a known, but also expected, issue with shared state and - the test store. The fix is to have the test store not eagerly process actions from effects, - but unfortunately that would be a breaking change in 1.0. - """) - - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } - await store.send(.incrementSharedInDelegate) - await store.receive(\.delegate.didIncrement) { - $0.count = 1 - $0.stats.count = 1 - } - } - - @MainActor - func testSharingWithDelegateAction_EagerActionProcessing() async { - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } - await store.send(.incrementSharedInDelegate) { - $0.stats.count = 1 - } - await store.receive(\.delegate.didIncrement) { - $0.count = 1 - } - } - - @MainActor - func testSharing_Failure() async { - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } - XCTExpectFailure { - $0.compactDescription == """ - failed - A state change does not match expectation: … - -   SharedFeature.State( -   _count: 0, -   _profile: #1 Profile(…), - − _sharedCount: #1 2, - + _sharedCount: #1 1, -   _stats: #1 Stats(count: 0), -   _isOn: #1 false -   ) - - (Expected: −, Actual: +) - """ - } - await store.send(.sharedIncrement) { - $0.sharedCount = 2 - } - XCTAssertEqual(store.state.sharedCount, 1) - } - - @MainActor - func testSharing_NonExhaustive() async { - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } - store.exhaustivity = .off(showSkippedAssertions: true) - - await store.send(.sharedIncrement) - XCTAssertEqual(store.state.sharedCount, 1) - - XCTExpectFailure { - $0.compactDescription == """ - failed - A state change does not match expectation: … - -   SharedFeature.State( -   _count: 0, -   _profile: #1 Profile(…), - − _sharedCount: #1 3, - + _sharedCount: #1 2, -   _stats: #1 Stats(count: 0), -   _isOn: #1 false -   ) - - (Expected: −, Actual: +) - """ - } - await store.send(.sharedIncrement) { - $0.sharedCount = 3 - } - XCTAssertEqual(store.state.sharedCount, 2) - } - - @MainActor - func testMultiSharing() async { - @Shared(Stats()) var stats - - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: $stats)), - sharedCount: Shared(0), - stats: $stats - ) - ) { - SharedFeature() - } - await store.send(.incrementStats) { - $0.profile.stats.count = 2 - $0.stats.count = 2 - } - XCTAssertEqual(stats.count, 2) - } - - func testIncrementalMutation() async { - let store = await TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } - await store.send(.sharedIncrement) { - $0.sharedCount += 1 - } - } - - @MainActor - func testIncrementalMutation_Failure() async { - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } - XCTExpectFailure { - $0.compactDescription == """ - failed - A state change does not match expectation: … - -   SharedFeature.State( -   _count: 0, -   _profile: #1 Profile(…), - − _sharedCount: #1 2, - + _sharedCount: #1 1, -   _stats: #1 Stats(count: 0), -   _isOn: #1 false -   ) - - (Expected: −, Actual: +) - """ - } - await store.send(.sharedIncrement) { - $0.sharedCount += 2 - } - } - - func testEffect() async { - let store = await TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } - await store.send(.request) - await store.receive(\.sharedIncrement) { - $0.sharedCount = 1 - } - } - - @MainActor - func testEffect_Failure() async { - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } - XCTExpectFailure { - $0.compactDescription == """ - failed - State was not expected to change, but a change occurred: … - -   SharedFeature.State( -   _count: 0, -   _profile: #1 Profile(…), - − _sharedCount: #1 0, - + _sharedCount: #1 1, -   _stats: #1 Stats(count: 0), -   _isOn: #1 false -   ) - - (Expected: −, Actual: +) - """ - } - await store.send(.request) - await store.receive(\.sharedIncrement) - } - - func testMutationOfSharedStateInLongLivingEffect() async { - let store = await TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } withDependencies: { - $0.mainQueue = .immediate - } - await store.send(.longLivingEffect).finish() - await store.assert { - $0.sharedCount = 1 - } - } - - @MainActor - func testMutationOfSharedStateInLongLivingEffect_NoAssertion() async { - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } withDependencies: { - $0.mainQueue = .immediate - } - XCTExpectFailure { - $0.compactDescription == """ - failed - Test store finished before asserting against changes to shared state: … - -   SharedFeature.State( -   _count: 0, -   _profile: #1 Profile(…), - − _sharedCount: #1 0, - + _sharedCount: #1 1, -   _stats: #1 Stats(count: 0), -   _isOn: #1 false -   ) - - (Expected: −, Actual: +) - - Invoke "TestStore.assert" at the end of this test to assert against changes to shared state. - """ - } - await store.send(.longLivingEffect) - } - - @MainActor - func testMutationOfSharedStateInLongLivingEffect_IncorrectAssertion() async { - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()) - ) - ) { - SharedFeature() - } withDependencies: { - $0.mainQueue = .immediate - } - XCTExpectFailure { - $0.compactDescription == """ - failed - A state change does not match expectation: … - -   SharedFeature.State( -   _count: 0, -   _profile: #1 Profile(…), - − _sharedCount: #1 2, - + _sharedCount: #1 1, -   _stats: #1 Stats(count: 0), -   _isOn: #1 false -   ) - - (Expected: −, Actual: +) - """ - } - await store.send(.longLivingEffect) - store.assert { - $0.sharedCount = 2 - } - } - - func testComplexSharedEffect_ReducerMutation() async { - struct Feature: Reducer { - struct State: Equatable { - @Shared var count: Int - } - enum Action { - case startTimer - case stopTimer - case timerTick - } - @Dependency(\.mainQueue) var queue - enum CancelID { case timer } - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .startTimer: - return .run { send in - for await _ in self.queue.timer(interval: .seconds(1)) { - await send(.timerTick) - } - } - .cancellable(id: CancelID.timer) - case .stopTimer: - return .cancel(id: CancelID.timer) - case .timerTick: - state.count += 1 - return .none - } - } - } - } - let mainQueue = DispatchQueue.test - let store = await TestStore(initialState: Feature.State(count: Shared(0))) { - Feature() - } withDependencies: { - $0.mainQueue = mainQueue.eraseToAnyScheduler() - } - await store.send(.startTimer) - await mainQueue.advance(by: .seconds(1)) - await store.receive(.timerTick) { - $0.count = 1 - } - await store.send(.stopTimer) - await mainQueue.advance(by: .seconds(1)) - } - - func testComplexSharedEffect_EffectMutation() async { - struct Feature: Reducer { - struct State: Equatable { - @Shared var count: Int - } - enum Action { - case startTimer - case stopTimer - case timerTick - } - @Dependency(\.mainQueue) var queue - enum CancelID { case timer } - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .startTimer: - return .run { [count = state.$count] send in - for await _ in self.queue.timer(interval: .seconds(1)) { - await count.withLock { $0 += 1 } - await send(.timerTick) - } - } - .cancellable(id: CancelID.timer) - case .stopTimer: - return .merge( - .cancel(id: CancelID.timer), - .run { [count = state.$count] _ in - Task { - try await self.queue.sleep(for: .seconds(1)) - await count.withLock { $0 = 42 } - } - } - ) - case .timerTick: - return .none - } - } - } - } - let mainQueue = DispatchQueue.test - let store = await TestStore(initialState: Feature.State(count: Shared(0))) { - Feature() - } withDependencies: { - $0.mainQueue = mainQueue.eraseToAnyScheduler() - } - await store.send(.startTimer) - await mainQueue.advance(by: .seconds(1)) - await store.receive(.timerTick) { - $0.count = 1 - } - await store.send(.stopTimer) - await mainQueue.advance(by: .seconds(1)) - await store.assert { - $0.count = 42 - } - } - - @MainActor - func testDump() { - @Shared(Profile(stats: Shared(Stats()))) var profile: Profile - XCTAssertEqual( - String(customDumping: profile), - """ - Profile( - _stats: #1 Stats(count: 0) - ) - """ - ) - - let count = $profile.stats.count - XCTAssertEqual( - String(customDumping: count), - """ - #1 0 - """ - ) - } - - @MainActor - func testSimpleFeatureFailure() async { - let store = TestStore(initialState: SimpleFeature.State(count: Shared(0))) { - SimpleFeature() - } - - XCTExpectFailure { - $0.compactDescription == """ - failed - State was not expected to change, but a change occurred: … - -   SimpleFeature.State( - − _count: #1 0 - + _count: #1 1 -   ) - - (Expected: −, Actual: +) - """ - } - - await store.send(.incrementInReducer) - } - - func testObservation() { - @Shared var count: Int - _count = Shared(0) - let countDidChange = self.expectation(description: "countDidChange") - withPerceptionTracking { - _ = count - } onChange: { - countDidChange.fulfill() - } - count += 1 - self.wait(for: [countDidChange], timeout: 0) - } - - func testObservation_projected() { - @Shared var count: Int - _count = Shared(0) - let countDidChange = self.expectation(description: "countDidChange") - withPerceptionTracking { - _ = $count - } onChange: { - countDidChange.fulfill() - } - $count = Shared(1) - self.wait(for: [countDidChange], timeout: 0) - } - - @available(*, deprecated) - @MainActor - func testObservation_Object() { - @Shared var object: SharedObject - _object = Shared(SharedObject()) - let countDidChange = self.expectation(description: "countDidChange") - withPerceptionTracking { - _ = object.count - } onChange: { - countDidChange.fulfill() - } - object.count += 1 - self.wait(for: [countDidChange], timeout: 0) - } - - @MainActor - func testAssertSharedStateWithNoChanges() { - let store = TestStore(initialState: SimpleFeature.State(count: Shared(0))) { - SimpleFeature() - } - XCTExpectFailure { - $0.compactDescription == """ - failed - Expected changes, but none occurred. - """ - } - store.state.$count.assert { - $0 = 0 - } - } - - @MainActor - func testPublisher() { - var cancellables: Set = [] - defer { _ = cancellables } - - let sharedCount = Shared(0) - var counts = [Int]() - sharedCount.publisher.sink { _ in - } receiveValue: { count in - XCTAssertEqual(sharedCount.wrappedValue, count - 1) - counts.append(count) - } - .store(in: &cancellables) - - sharedCount.wrappedValue += 1 - XCTAssertEqual(counts, [1]) - sharedCount.wrappedValue += 1 - XCTAssertEqual(counts, [1, 2]) - } - - @MainActor - func testPublisher_MultipleSubscribers() { - var cancellables: Set = [] - defer { _ = cancellables } - - let sharedCount = Shared(0) - var counts = [Int]() - sharedCount.publisher.sink { _ in - } receiveValue: { count in - counts.append(count) - } - .store(in: &cancellables) - sharedCount.publisher.sink { _ in - } receiveValue: { count in - counts.append(count) - } - .store(in: &cancellables) - - sharedCount.wrappedValue += 1 - XCTAssertEqual(counts, [1, 1]) - sharedCount.wrappedValue += 1 - XCTAssertEqual(counts, [1, 1, 2, 2]) - } - - @MainActor - func testPublisher_MutateInSink() { - var cancellables: Set = [] - defer { _ = cancellables } - - let sharedCount = Shared(0) - var counts = [Int]() - sharedCount.publisher.sink { _ in - } receiveValue: { count in - counts.append(count) - if count == 1 { - sharedCount.wrappedValue = 2 - } - } - .store(in: &cancellables) - - sharedCount.wrappedValue += 1 - XCTAssertEqual(counts, [1, 2]) - } - - @MainActor - func testPublisher_Persistence_MutateInSink() { - var cancellables: Set = [] - defer { _ = cancellables } - - @Shared(.appStorage("count")) var count = 0 - var counts = [Int]() - $count.publisher.sink { _ in - } receiveValue: { newCount in - counts.append(newCount) - if newCount == 1 { - count = 2 - } - } - .store(in: &cancellables) - - count += 1 - XCTAssertEqual(counts, [1, 2]) - @Dependency(\.defaultAppStorage) var userDefaults - // TODO: Should we runtime warn on re-entrant mutations? - XCTAssertEqual(count, 1) - XCTAssertEqual(userDefaults.integer(forKey: "count"), 1) - } - - @MainActor - func testPublisher_Persistence_ExternalChange() async throws { - @Dependency(\.defaultAppStorage) var defaults - @Shared(.appStorage("count")) var count = 0 - XCTAssertEqual(count, 0) - - var cancellables: Set = [] - defer { _ = cancellables } - - var counts = [Int]() - $count.publisher.sink { _ in - } receiveValue: { newCount in - counts.append(newCount) - if newCount == 1 { count = 2 } - } - .store(in: &cancellables) - - try await Task.sleep(nanoseconds: 1_000_000) - defaults.set(1, forKey: "count") - try await Task.sleep(nanoseconds: 10_000_000) - XCTAssertEqual(counts, [1, 2]) - XCTAssertEqual(defaults.integer(forKey: "count"), 2) - } - - @MainActor - func testMultiplePublisherSubscriptions() async { - let runCount = 10 - for _ in 1...runCount { - let store = TestStore(initialState: ListFeature.State()) { - ListFeature() - } withDependencies: { - $0.uuid = .incrementing - } - await store.send(.children(.element(id: 0, action: .onAppear))) - await store.send(.children(.element(id: 1, action: .onAppear))) - await store.send(.children(.element(id: 2, action: .onAppear))) - await store.send(.children(.element(id: 3, action: .onAppear))) - await store.send(.incrementValue) { - $0.value = 1 - } - await store.receive(\.children[id: 0].response) { - $0.children[id: 0]?.text = "1" - } - await store.receive(\.children[id: 1].response) { - $0.children[id: 1]?.text = "1" - } - await store.receive(\.children[id: 2].response) { - $0.children[id: 2]?.text = "1" - } - await store.receive(\.children[id: 3].response) { - $0.children[id: 3]?.text = "1" - } - } - } - - @MainActor - func testEarlySharedStateMutation() async { - let store = TestStore(initialState: EarlySharedStateMutation.State(count: Shared(0))) { - EarlySharedStateMutation() - } - - XCTTODO( - """ - This currently fails because the effect returned from '.action' synchronously sends the - '.response' action, which then mutates the shared state. Because the TestStore processes - actions immediately the shared state mutation must be asserted in `store.send` rather than - store.receive. - - We should update the TestStore so that effects suspend until one does 'store.receive'. That - would fix this test. - """ - ) - await store.send(.action) - await store.receive(.response) { - $0.count = 42 - } - } - - #if canImport(UIKit) - @MainActor - func testObserveWithPrintChanges() async { - let store = TestStore(initialState: SimpleFeature.State(count: Shared(0))) { - SimpleFeature()._printChanges() - } - - var observations: [Int] = [] - observe { - observations.append(store.state.count) - } - - XCTAssertEqual(observations, [0]) - await store.send(.incrementInReducer) { - $0.count += 1 - } - XCTAssertEqual(observations, [0, 1]) - } - #endif - - func testSharedDefaults_UseDefault() { - @Shared(.isOn) var isOn - XCTAssertEqual(isOn, false) - } - - func testSharedDefaults_OverrideDefault() { - @Shared(.isOn) var isOn = true - XCTAssertEqual(isOn, true) - } - - func testSharedDefaults_MultipleWithDifferentDefaults() { - @Shared(.isOn) var isOn1 - @Shared(.isOn) var isOn2 = true - @Shared(.appStorage("isOn")) var isOn3 = true - - XCTAssertEqual(isOn1, false) - XCTAssertEqual(isOn2, false) - XCTAssertEqual(isOn3, false) - - isOn2 = true - XCTAssertEqual(isOn1, true) - XCTAssertEqual(isOn2, true) - XCTAssertEqual(isOn3, true) - - isOn1 = false - XCTAssertEqual(isOn1, false) - XCTAssertEqual(isOn2, false) - XCTAssertEqual(isOn3, false) - - isOn3 = true - XCTAssertEqual(isOn1, true) - XCTAssertEqual(isOn2, true) - XCTAssertEqual(isOn3, true) - } - - func testSharedDefaults_Used() { - let didAccess = LockIsolated(false) - let logDefault: @Sendable () -> Bool = { - didAccess.setValue(true) - return true - } - @Shared(.isActive(default: logDefault)) var isActive - XCTAssertEqual(isActive, true) - XCTAssertEqual(didAccess.value, true) - } - - func testSharedDefaults_Unused() { - let didAccess = LockIsolated(false) - let logDefault: @Sendable () -> Bool = { - didAccess.setValue(true) - return true - } - @Shared(.isActive(default: logDefault)) var isActive = false - XCTAssertEqual(isActive, false) - XCTAssertEqual(didAccess.value, false) - } - - func testSharedInitialValueUnused() { - let accessedIsOn1 = LockIsolated(false) - let accessedIsOn2 = LockIsolated(false) - @Shared(.isOn) var isOn1 = { - accessedIsOn1.setValue(true) - return false - }() - @Shared(.isOn) var isOn2 = { - accessedIsOn2.setValue(true) - return true - }() - XCTAssertEqual(isOn1, false) - XCTAssertEqual(isOn2, false) - XCTAssertEqual(accessedIsOn1.value, true) - XCTAssertEqual(accessedIsOn2.value, false) - } - - func testSharedOverrideDefault() { - let accessedActive1 = LockIsolated(false) - let accessedDefault = LockIsolated(false) - let logDefault: @Sendable () -> Bool = { - accessedDefault.setValue(true) - return true - } - @Shared(.isActive(default: logDefault)) var isActive1 = { - accessedActive1.setValue(true) - return false - }() - @Shared(.isActive(default: logDefault)) var isActive2 - - XCTAssertEqual(isActive1, false) - XCTAssertEqual(isActive2, false) - XCTAssertEqual(accessedActive1.value, true) - } - - func testSharedReaderInitialValueUnused() { - let accessedIsOn1 = LockIsolated(false) - let accessedIsOn2 = LockIsolated(false) - @SharedReader(.isOn) var isOn1 = { - accessedIsOn1.setValue(true) - return false - }() - @SharedReader(.isOn) var isOn2 = { - accessedIsOn2.setValue(true) - return true - }() - XCTAssertEqual(isOn1, false) - XCTAssertEqual(isOn2, false) - XCTAssertEqual(accessedIsOn1.value, true) - XCTAssertEqual(accessedIsOn2.value, false) - } - - func testSharedReaderOverrideDefault() { - let accessedActive1 = LockIsolated(false) - let accessedDefault = LockIsolated(false) - let logDefault: @Sendable () -> Bool = { - accessedDefault.setValue(true) - return true - } - @SharedReader(.isActive(default: logDefault)) var isActive1 = { - accessedActive1.setValue(true) - return false - }() - @SharedReader(.isActive(default: logDefault)) var isActive2 - - XCTAssertEqual(isActive1, false) - XCTAssertEqual(isActive2, false) - XCTAssertEqual(accessedActive1.value, true) - } - - func testSharedThrowingInitialValueUnused() throws { - try XCTAssertThrowsError(Shared(.noDefaultIsOn)) - } - - func testSharedReaderThrowingInitialValueUnused() throws { - try XCTAssertThrowsError(SharedReader(.noDefaultIsOn)) - } - - func testSharedReaderDefaults_MultipleWithDifferentDefaults() { - @Shared(.appStorage("isOn")) var isOn = false - @SharedReader(.isOn) var isOn1 - @SharedReader(.isOn) var isOn2 = true - @SharedReader(.appStorage("isOn")) var isOn3 = true - - XCTAssertEqual(isOn1, false) - XCTAssertEqual(isOn2, false) - XCTAssertEqual(isOn3, false) - - isOn = true - XCTAssertEqual(isOn1, true) - XCTAssertEqual(isOn2, true) - XCTAssertEqual(isOn3, true) - } - - @MainActor - func testPrivateSharedState() async { - let isOn = Shared(false) - let store = TestStore( - initialState: SharedFeature.State( - profile: Shared(Profile(stats: Shared(Stats()))), - sharedCount: Shared(0), - stats: Shared(Stats()), - isOn: isOn - ) - ) { - SharedFeature() - } - - await store.send(.toggleIsOn) { - _ = $0 - isOn.wrappedValue = true - } - await store.send(.toggleIsOn) { - _ = $0 - isOn.wrappedValue = false - } - } - - func testEquatability_DifferentReference() { - let count = Shared(0) - @Shared(.appStorage("count")) var appStorageCount = 0 - @Shared( - .fileStorage( - URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("count.json") - ) - ) - var fileStorageCount = 0 - @Shared(.inMemory("count")) var inMemoryCount = 0 - - XCTAssertEqual(count, $appStorageCount) - XCTAssertEqual($appStorageCount, $fileStorageCount) - XCTAssertEqual($fileStorageCount, $inMemoryCount) - XCTAssertEqual($inMemoryCount, count) - } - - func testEquatable_DifferentKeyPath() { - struct Settings { - var isOn = false - var hasSeen = false - } - @Shared(.inMemory("settings")) var settings = Settings() - XCTAssertEqual($settings.isOn, $settings.hasSeen) - withSharedChangeTracking { tracker in - settings.isOn.toggle() - XCTAssertNotEqual(settings.isOn, settings.hasSeen) - XCTAssertNotEqual($settings.isOn, $settings.hasSeen) - XCTAssertNotEqual($settings.hasSeen, $settings.isOn) - tracker.assert { - XCTAssertEqual(settings.isOn, settings.hasSeen) - XCTAssertEqual($settings.isOn, $settings.hasSeen) - XCTAssertEqual($settings.hasSeen, $settings.isOn) - settings.hasSeen.toggle() - XCTAssertNotEqual(settings.isOn, settings.hasSeen) - XCTAssertNotEqual($settings.isOn, $settings.hasSeen) - XCTAssertNotEqual($settings.hasSeen, $settings.isOn) - } - } - } - - func testSelfEqualityInAnAssertion() { - let count = Shared(0) - withSharedChangeTracking { tracker in - count.wrappedValue += 1 - tracker.assert { - XCTAssertNotEqual(count, count) - XCTAssertEqual(count.wrappedValue, count.wrappedValue) - } - XCTAssertEqual(count, count) - XCTAssertEqual(count.wrappedValue, count.wrappedValue) - } - XCTAssertEqual(count, count) - XCTAssertEqual(count.wrappedValue, count.wrappedValue) - } - - func testBasicAssertion() { - let count = Shared(0) - withSharedChangeTracking { tracker in - count.wrappedValue += 1 - tracker.assert { - count.wrappedValue += 1 - XCTAssertEqual(count, count) - XCTAssertEqual(count.wrappedValue, count.wrappedValue) - } - XCTAssertEqual(count, count) - XCTAssertEqual(count.wrappedValue, count.wrappedValue) - } - XCTAssertEqual(count, count) - XCTAssertEqual(count.wrappedValue, count.wrappedValue) - } - - func testDefaultVersusValueInExternalStorage() async { - @Dependency(\.defaultAppStorage) var userDefaults - userDefaults.set(true, forKey: "optionalValueWithDefault") - - @Shared(.optionalValueWithDefault) var optionalValueWithDefault - - XCTAssertNotNil(optionalValueWithDefault) - - await $optionalValueWithDefault.withLock { $0 = nil } - - XCTAssertNil(optionalValueWithDefault) - } - - func testElements() { - struct User: Equatable, Identifiable { - let id: Int - var name = "" - } - let sharedCollection = Shared([User(id: 1), User(id: 2)] as IdentifiedArrayOf) - let elements = sharedCollection.elements - let first = elements.first! - let second = elements.last! - - first.wrappedValue.name = "Blob" - second.wrappedValue.name = "Blob Jr" - expectNoDifference(first.wrappedValue, User(id: 1, name: "Blob")) - expectNoDifference(second.wrappedValue, User(id: 2, name: "Blob Jr")) - expectNoDifference( - sharedCollection.wrappedValue, - [ - User(id: 1, name: "Blob"), - User(id: 2, name: "Blob Jr"), - ] - ) - - sharedCollection.wrappedValue.swapAt(0, 1) - expectNoDifference(first.wrappedValue, User(id: 1, name: "Blob")) - expectNoDifference(second.wrappedValue, User(id: 2, name: "Blob Jr")) - expectNoDifference( - sharedCollection.wrappedValue, - [ - User(id: 2, name: "Blob Jr"), - User(id: 1, name: "Blob"), - ] - ) - - first.wrappedValue.name += ", M.D." - second.wrappedValue.name += ", Esq." - expectNoDifference(first.wrappedValue, User(id: 1, name: "Blob, M.D.")) - expectNoDifference(second.wrappedValue, User(id: 2, name: "Blob Jr, Esq.")) - expectNoDifference( - sharedCollection.wrappedValue, - [ - User(id: 2, name: "Blob Jr, Esq."), - User(id: 1, name: "Blob, M.D."), - ] - ) - } - - @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) - func testConcurrentPublisherAccess() async { - let sharedCount = Shared(0) - await withTaskGroup(of: Void.self) { group in - for _ in 0..<1_000 { - group.addTask { - for await _ in sharedCount.publisher.values.prefix(0) {} - } - } - } - } - - func testReEntrantSharedSubscriptionDependencyResolution() async throws { - for _ in 1...10 { - try await withDependencies { - $0 = DependencyValues() - } operation: { - @Shared(.appStorage("count")) var count = 0 - - struct Client: TestDependencyKey { - init() { - @Dependency(\.defaultAppStorage) var userDefaults - userDefaults.set(42, forKey: "count") - } - static var testValue: Self { Self() } - } - - withEscapedDependencies { dependencies in - DispatchQueue.global().async { - dependencies.yield { - XCTAssertEqual({ Thread.isMainThread }(), false) - @Dependency(Client.self) var client - _ = client - } - } - DispatchQueue.main.async { [sharedCount = $count] in - dependencies.yield { - XCTAssertEqual({ Thread.isMainThread }(), true) - _ = sharedCount.wrappedValue - } - } - } - - try await Task.sleep(nanoseconds: 1_000_000_000) - XCTAssertEqual(count, 42) - } - } - } - - func testPersistenceKeySubscription() async throws { - let persistenceKey: AppStorageKey = .appStorage("shared") - let changes = LockIsolated<[Int?]>([]) - var subscription: Optional = persistenceKey.subscribe(initialValue: nil) { value in - changes.withValue { $0.append(value) } - } - @Dependency(\.defaultAppStorage) var userDefaults - userDefaults.set(1, forKey: "shared") - userDefaults.set(42, forKey: "shared") - subscription?.cancel() - userDefaults.set(123, forKey: "shared") - subscription = nil - XCTAssertEqual([1, 42], changes.value) - XCTAssertEqual(123, persistenceKey.load(initialValue: nil)) - } -} - -@globalActor actor GA: GlobalActor { - static let shared = GA() -} - -@Reducer -private struct SharedFeature { - @ObservableState - struct State: Equatable { - var count = 0 - @Shared var profile: Profile - @Shared var sharedCount: Int - @Shared var stats: Stats - @Shared fileprivate var isOn: Bool - init( - count: Int = 0, - profile: Shared, - sharedCount: Shared, - stats: Shared, - isOn: Shared = Shared(false) - ) { - self.count = count - self._profile = profile - self._sharedCount = sharedCount - self._stats = stats - self._isOn = isOn - } - } - enum Action { - case delegate(Delegate) - case increment - case incrementStats - case incrementSharedInDelegate - case longLivingEffect - case noop - case request - case sharedIncrement - case toggleIsOn - @CasePathable - enum Delegate { - case didIncrement - } - } - @Dependency(\.mainQueue) var mainQueue - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .delegate(.didIncrement): - state.count += 1 - state.stats.count += 1 - return .none - case .increment: - state.count += 1 - return .none - case .incrementStats: - state.profile.stats.count += 1 - state.stats.count += 1 - return .none - case .incrementSharedInDelegate: - return .send(.delegate(.didIncrement)) - case .longLivingEffect: - return .run { [sharedCount = state.$sharedCount] _ in - try await self.mainQueue.sleep(for: .seconds(1)) - await sharedCount.withLock { $0 += 1 } - } - case .noop: - return .none - case .request: - return .run { send in - await send(.sharedIncrement) - } - case .sharedIncrement: - state.sharedCount += 1 - return .none - case .toggleIsOn: - state.isOn.toggle() - return .none - } - } - } -} - -private struct Stats: Codable, Equatable { - var count = 0 -} -private struct Profile: Equatable { - @Shared var stats: Stats -} -@Reducer -private struct SimpleFeature { - struct State: Equatable { - @Shared var count: Int - } - enum Action { - case incrementInEffect - case incrementInReducer - } - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .incrementInEffect: - return .run { [count = state.$count] _ in - await count.withLock { $0 += 1 } - } - case .incrementInReducer: - state.count += 1 - return .none - } - } - } -} - -@Perceptible -class SharedObject: @unchecked Sendable { - var count = 0 -} - -@Reducer -private struct RowFeature { - @ObservableState - struct State: Equatable, Identifiable { - let id: Int - var text: String - @Shared var value: Int - } - - enum Action: Equatable { - case onAppear - case response(Int) - } - - var body: some ReducerOf { - Reduce { state, action in - switch action { - case let .response(newValue): - state.text = "\(newValue)" - return .none - - case .onAppear: - return .publisher { - state.$value.publisher - .map(Action.response) - .prefix(1) - } - } - } - } -} - -@Reducer -private struct ListFeature { - @ObservableState - struct State: Equatable { - @Shared var value: Int - var children: IdentifiedArrayOf - - init(value: Int = 0) { - @Dependency(\.uuid) var uuid - self._value = Shared(value) - self.children = [ - .init(id: 0, text: "0", value: _value), - .init(id: 1, text: "0", value: _value), - .init(id: 2, text: "0", value: _value), - .init(id: 3, text: "0", value: _value), - ] - } - } - - enum Action: Equatable { - case children(IdentifiedActionOf) - case incrementValue - } - - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .children: - return .none - - case .incrementValue: - state.value += 1 - return .none - } - } - .forEach(\.children, action: \.children) { RowFeature() } - } -} - -@Reducer -private struct EarlySharedStateMutation { - @ObservableState - struct State: Equatable { - @Shared var count: Int - } - enum Action { - case action - case response - } - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .action: - return .send(.response) - case .response: - state.count = 42 - return .none - } - } - } -} - -extension PersistenceReaderKey where Self == PersistenceKeyDefault> { - static var isOn: Self { - PersistenceKeyDefault(.appStorage("isOn"), false) - } - - static func isActive(default keyDefault: @escaping @Sendable () -> Bool) -> Self { - PersistenceKeyDefault(.appStorage("isActive"), keyDefault()) - } -} - -// NB: This is a compile-time test to verify that optional shared state with defaults compiles. -struct StateWithOptionalSharedAndDefault { - @Shared(.optionalValueWithDefault) var optionalValueWithDefault -} -extension PersistenceKey where Self == PersistenceKeyDefault> { - fileprivate static var optionalValueWithDefault: Self { - return PersistenceKeyDefault(.appStorage("optionalValueWithDefault"), nil) - } -} - -extension PersistenceReaderKey where Self == AppStorageKey { - static var noDefaultIsOn: Self { - appStorage("noDefaultIsOn") - } -}