From 99d4eb039772268b2c36993a6e17091003a3d1f7 Mon Sep 17 00:00:00 2001 From: dankinsoid <30962149+dankinsoid@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:22:17 +0300 Subject: [PATCH] 0.24.0 --- .github/FUNDING.yml | 6 + .../UserInterfaceState.xcuserstate | Bin 37351 -> 40057 bytes .../SpeechClient/Client.swift | 20 +- .../SpeechRecognition/SpeechClient/Live.swift | 4 +- .../SpeechRecognition/SpeechRecognition.swift | 6 +- .../SyncUps/SyncUps.xcodeproj/project.pbxproj | 51 +-- .../xcshareddata/swiftpm/Package.resolved | 165 +--------- .../UserInterfaceState.xcuserstate | Bin 19506 -> 73819 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../xcschemes/xcschememanagement.plist | 25 +- Examples/SyncUps/SyncUps/App.swift | 41 ++- Examples/SyncUps/SyncUps/AppFeature.swift | 190 ++++++------ .../SyncUps/Dependencies/DataManager.swift | 15 +- .../SyncUps/Dependencies/OpenSettings.swift | 21 +- .../Dependencies/SpeechRecognizer.swift | 42 ++- Examples/SyncUps/SyncUps/Meeting.swift | 3 +- Examples/SyncUps/SyncUps/Models.swift | 8 +- Examples/SyncUps/SyncUps/RecordMeeting.swift | 42 +-- Examples/SyncUps/SyncUps/SyncUpDetail.swift | 4 +- Examples/SyncUps/SyncUps/SyncUpForm.swift | 105 +++---- Examples/SyncUps/SyncUps/SyncUpsList.swift | 292 +++++++++--------- .../SyncUpsTests/AppFeatureTests.swift | 2 +- .../SyncUpsTests/RecordMeetingTests.swift | 2 +- .../SyncUpsTests/SyncUpDetailTests.swift | 2 +- .../SyncUpsTests/SyncUpFormTests.swift | 2 +- .../SyncUpsTests/SyncUpsListTests.swift | 2 +- README.md | 2 +- Sources/VDStore/Action.swift | 1 + .../VDStore/Dependencies/TasksStorage.swift | 6 +- Sources/VDStore/Dependencies/UUID.swift | 116 +++++++ Sources/VDStore/Store.swift | 45 ++- Sources/VDStore/StoreDependencies.swift | 41 ++- Sources/VDStore/StoreExtensions/ForEach.swift | 18 ++ Sources/VDStore/StoreExtensions/Iflet.swift | 48 --- .../VDStore/StoreExtensions/OnChange.swift | 25 ++ Sources/VDStore/StoreExtensions/Or.swift | 12 + .../StoreExtensions/WithAnimaiton.swift | 12 + Sources/VDStore/Utils/Binding++.swift | 26 ++ Sources/VDStore/Utils/Ref.swift | 71 ----- Sources/VDStore/Utils/StoreBox.swift | 89 +++++- Sources/VDStore/Utils/StorePublisher.swift | 31 -- Sources/VDStore/Utils/StoreUpdates.swift | 70 +++++ Sources/VDStore/ViewStore.swift | 1 + Sources/VDStoreMacros/ActionsMacro.swift | 1 + Sources/VDStoreMacros/Extensions.swift | 10 + Tests/VDStoreTests/VDStoreTests.swift | 62 ++++ 46 files changed, 942 insertions(+), 801 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 Examples/SyncUps/SyncUps.xcodeproj/xcuserdata/danil.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 Sources/VDStore/Dependencies/UUID.swift create mode 100644 Sources/VDStore/StoreExtensions/ForEach.swift delete mode 100644 Sources/VDStore/StoreExtensions/Iflet.swift create mode 100644 Sources/VDStore/StoreExtensions/OnChange.swift create mode 100644 Sources/VDStore/StoreExtensions/Or.swift create mode 100644 Sources/VDStore/StoreExtensions/WithAnimaiton.swift create mode 100644 Sources/VDStore/Utils/Binding++.swift delete mode 100644 Sources/VDStore/Utils/Ref.swift delete mode 100644 Sources/VDStore/Utils/StorePublisher.swift create mode 100644 Sources/VDStore/Utils/StoreUpdates.swift diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5e96f94 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,6 @@ +# These are supported funding model platforms + +github: dankinsoid +open_collective: voidilov-daniil +ko_fi: dankinsoid +custom: ["https://paypal.me/voidilovuae"] diff --git a/Examples/SpeechRecognition/SpeechRecognition.xcodeproj/project.xcworkspace/xcuserdata/danil.xcuserdatad/UserInterfaceState.xcuserstate b/Examples/SpeechRecognition/SpeechRecognition.xcodeproj/project.xcworkspace/xcuserdata/danil.xcuserdatad/UserInterfaceState.xcuserstate index f0b7d4c3776b2034f0ac37fab89da11e75e70863..19fbcb17cc3e189ec962200e5735d0b00f36e8cb 100644 GIT binary patch delta 19825 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 delta 18472 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 diff --git a/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Client.swift b/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Client.swift index 22178ad..b0b9179 100644 --- a/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Client.swift +++ b/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Client.swift @@ -21,7 +21,7 @@ struct SpeechClient { extension SpeechClient { - static var previewValue: Self { + static let previewValue: Self = { let isRecording = ActorIsolated(false) return Self( @@ -63,7 +63,7 @@ extension SpeechClient { } } ) - } + }() } final actor ActorIsolated { @@ -81,14 +81,10 @@ final actor ActorIsolated { extension StoreDIValues { - var speechClient: SpeechClient { - get { - self[\.speechClient] ?? valueFor( - live: .liveValue, - test: SpeechClient(), - preview: .previewValue - ) - } - set { self[\.speechClient] = newValue } - } + @StoreDIValue + var speechClient: SpeechClient = valueFor( + live: .liveValue, + test: SpeechClient(), + preview: .previewValue + ) } diff --git a/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Live.swift b/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Live.swift index 92d65d2..14dc5e7 100644 --- a/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Live.swift +++ b/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Live.swift @@ -3,7 +3,7 @@ import Speech extension SpeechClient { - static var liveValue: Self { + static let liveValue: Self = { let speech = Speech() return Self( finishTask: { @@ -20,7 +20,7 @@ extension SpeechClient { await speech.startTask(request: request) } ) - } + }() } private actor Speech { diff --git a/Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift b/Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift index 5fe7a67..0bd170f 100644 --- a/Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift +++ b/Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift @@ -120,7 +120,7 @@ struct SpeechRecognitionView: View { .padding() .animation(.linear, value: state.transcribedText) .alert( - "Error", + state.alert ?? "", isPresented: Binding { state.alert != nil } set: { newValue in @@ -129,7 +129,9 @@ struct SpeechRecognitionView: View { } } ) { - Text(state.alert ?? "") + Button("OK") { + state.alert = nil + } } } } diff --git a/Examples/SyncUps/SyncUps.xcodeproj/project.pbxproj b/Examples/SyncUps/SyncUps.xcodeproj/project.pbxproj index c98b747..b0f6dad 100644 --- a/Examples/SyncUps/SyncUps.xcodeproj/project.pbxproj +++ b/Examples/SyncUps/SyncUps.xcodeproj/project.pbxproj @@ -7,14 +7,14 @@ objects = { /* Begin PBXBuildFile section */ - CA14FF052B361C7400104A70 /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA14FF042B361C7400104A70 /* SwiftUINavigation */; }; + 831768012BA07E2F00F199F0 /* VDStore in Frameworks */ = {isa = PBXBuildFile; productRef = 831768002BA07E2F00F199F0 /* VDStore */; }; + 8317680B2BA09AC600F199F0 /* VDFlow in Frameworks */ = {isa = PBXBuildFile; productRef = 8317680A2BA09AC600F199F0 /* VDFlow */; }; DC7CE4E729E9E6E4006B6263 /* ding.wav in Resources */ = {isa = PBXBuildFile; fileRef = DC7CE4E629E9E6E4006B6263 /* ding.wav */; }; DC808D7329E9C3AC0072B4A9 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC808D7229E9C3AC0072B4A9 /* App.swift */; }; DC808D7729E9C3AD0072B4A9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC808D7629E9C3AD0072B4A9 /* Assets.xcassets */; }; DC808D8429E9C3AD0072B4A9 /* SyncUpFormTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC808D8329E9C3AD0072B4A9 /* SyncUpFormTests.swift */; }; DC808D8E29E9C3AD0072B4A9 /* SyncUpsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC808D8D29E9C3AD0072B4A9 /* SyncUpsUITests.swift */; }; DC808DA029E9C4340072B4A9 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC808D9F29E9C4340072B4A9 /* DataManager.swift */; }; - DC808DA329E9C4490072B4A9 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = DC808DA229E9C4490072B4A9 /* ComposableArchitecture */; }; DC808DA529E9C4C70072B4A9 /* OpenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC808DA429E9C4C70072B4A9 /* OpenSettings.swift */; }; DC808DA729E9C4D60072B4A9 /* SpeechRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC808DA629E9C4D60072B4A9 /* SpeechRecognizer.swift */; }; DC808DA929E9C5090072B4A9 /* SyncUpForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC808DA829E9C5090072B4A9 /* SyncUpForm.swift */; }; @@ -23,7 +23,6 @@ DC808DAF29E9C53E0072B4A9 /* RecordMeeting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC808DAE29E9C53E0072B4A9 /* RecordMeeting.swift */; }; DC808DB129E9C54A0072B4A9 /* SyncUpDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC808DB029E9C54A0072B4A9 /* SyncUpDetail.swift */; }; DC808DB329E9C5540072B4A9 /* SyncUpsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC808DB229E9C5540072B4A9 /* SyncUpsList.swift */; }; - DC808DB629E9C58F0072B4A9 /* Tagged in Frameworks */ = {isa = PBXBuildFile; productRef = DC808DB529E9C58F0072B4A9 /* Tagged */; }; DC80B1E929EDE5D1001CC0CC /* AppFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC80B1E829EDE5D1001CC0CC /* AppFeature.swift */; }; DC80B1EB29EE12A7001CC0CC /* AppFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC80B1EA29EE12A7001CC0CC /* AppFeatureTests.swift */; }; DC8C2B0C29EB084900C65286 /* RecordMeetingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8C2B0B29EB084900C65286 /* RecordMeetingTests.swift */; }; @@ -58,7 +57,7 @@ DC808D8329E9C3AD0072B4A9 /* SyncUpFormTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncUpFormTests.swift; sourceTree = ""; }; DC808D8929E9C3AD0072B4A9 /* SyncUpsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SyncUpsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DC808D8D29E9C3AD0072B4A9 /* SyncUpsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncUpsUITests.swift; sourceTree = ""; }; - DC808D9C29E9C3C10072B4A9 /* swift-composable-architecture */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "swift-composable-architecture"; path = ../..; sourceTree = ""; }; + DC808D9C29E9C3C10072B4A9 /* VDStore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = VDStore; path = ../..; sourceTree = ""; }; DC808D9F29E9C4340072B4A9 /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; }; DC808DA429E9C4C70072B4A9 /* OpenSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSettings.swift; sourceTree = ""; }; DC808DA629E9C4D60072B4A9 /* SpeechRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechRecognizer.swift; sourceTree = ""; }; @@ -80,9 +79,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DC808DB629E9C58F0072B4A9 /* Tagged in Frameworks */, - DC808DA329E9C4490072B4A9 /* ComposableArchitecture in Frameworks */, - CA14FF052B361C7400104A70 /* SwiftUINavigation in Frameworks */, + 8317680B2BA09AC600F199F0 /* VDFlow in Frameworks */, + 831768012BA07E2F00F199F0 /* VDStore in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -114,7 +112,7 @@ DC808D6629E9C3AC0072B4A9 = { isa = PBXGroup; children = ( - DC808D9C29E9C3C10072B4A9 /* swift-composable-architecture */, + DC808D9C29E9C3C10072B4A9 /* VDStore */, DC43064F2AE8CF7C005AFB1E /* SyncUps.xctestplan */, DC808DA129E9C4490072B4A9 /* Frameworks */, DC808D7029E9C3AC0072B4A9 /* Products */, @@ -206,9 +204,8 @@ ); name = SyncUps; packageProductDependencies = ( - DC808DA229E9C4490072B4A9 /* ComposableArchitecture */, - DC808DB529E9C58F0072B4A9 /* Tagged */, - CA14FF042B361C7400104A70 /* SwiftUINavigation */, + 831768002BA07E2F00F199F0 /* VDStore */, + 8317680A2BA09AC600F199F0 /* VDFlow */, ); productName = SyncUps; productReference = DC808D6F29E9C3AC0072B4A9 /* SyncUps.app */; @@ -283,8 +280,7 @@ ); mainGroup = DC808D6629E9C3AC0072B4A9; packageReferences = ( - DC808DB429E9C58F0072B4A9 /* XCRemoteSwiftPackageReference "swift-tagged" */, - CA14FF032B361C7400104A70 /* XCRemoteSwiftPackageReference "swiftui-navigation" */, + 831768092BA09AC600F199F0 /* XCRemoteSwiftPackageReference "VDFlow" */, ); productRefGroup = DC808D7029E9C3AC0072B4A9 /* Products */; projectDirPath = ""; @@ -668,38 +664,25 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - CA14FF032B361C7400104A70 /* XCRemoteSwiftPackageReference "swiftui-navigation" */ = { + 831768092BA09AC600F199F0 /* XCRemoteSwiftPackageReference "VDFlow" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pointfreeco/swiftui-navigation.git"; + repositoryURL = "https://github.com/dankinsoid/VDFlow"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.0; - }; - }; - DC808DB429E9C58F0072B4A9 /* XCRemoteSwiftPackageReference "swift-tagged" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pointfreeco/swift-tagged.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.10.0; + minimumVersion = 4.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - CA14FF042B361C7400104A70 /* SwiftUINavigation */ = { - isa = XCSwiftPackageProductDependency; - package = CA14FF032B361C7400104A70 /* XCRemoteSwiftPackageReference "swiftui-navigation" */; - productName = SwiftUINavigation; - }; - DC808DA229E9C4490072B4A9 /* ComposableArchitecture */ = { + 831768002BA07E2F00F199F0 /* VDStore */ = { isa = XCSwiftPackageProductDependency; - productName = ComposableArchitecture; + productName = VDStore; }; - DC808DB529E9C58F0072B4A9 /* Tagged */ = { + 8317680A2BA09AC600F199F0 /* VDFlow */ = { isa = XCSwiftPackageProductDependency; - package = DC808DB429E9C58F0072B4A9 /* XCRemoteSwiftPackageReference "swift-tagged" */; - productName = Tagged; + package = 831768092BA09AC600F199F0 /* XCRemoteSwiftPackageReference "VDFlow" */; + productName = VDFlow; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Examples/SyncUps/SyncUps.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/SyncUps/SyncUps.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index dd995dc..1cb231f 100644 --- a/Examples/SyncUps/SyncUps.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/SyncUps/SyncUps.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,174 +1,21 @@ { "pins" : [ - { - "identity" : "combine-schedulers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/combine-schedulers", - "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", - "state" : { - "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", - "version" : "1.3.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", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "e593aba2c6222daad7c4f2732a431eed2c09bb07", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-clocks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-clocks", - "state" : { - "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", - "version" : "1.0.2" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-custom-dump", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", - "state" : { - "revision" : "3ce83179e5f0c83ad54c305779c6b438e82aaf1d", - "version" : "1.2.1" - } - }, - { - "identity" : "swift-dependencies", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", - "state" : { - "revision" : "d3a5af3038a09add4d7682f66555d6212058a3c0", - "version" : "1.2.2" - } - }, - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-plugin", - "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/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" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-macro-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-macro-testing", - "state" : { - "revision" : "90e38eec4bf661ec0da1bbfd3ec507d0f0c05310", - "version" : "0.3.0" - } - }, - { - "identity" : "swift-perception", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-perception", - "state" : { - "revision" : "a5bb578d963fcdbffe4fd56c92b2e222f5b02c8a", - "version" : "1.1.2" - } - }, - { - "identity" : "swift-snapshot-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing", - "state" : { - "revision" : "5b0c434778f2c1a4c9b5ebdb8682b28e84dd69bd", - "version" : "1.15.4" - } - }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax", "state" : { - "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", - "version" : "510.0.1" - } - }, - { - "identity" : "swift-tagged", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-tagged.git", - "state" : { - "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", - "version" : "0.10.0" - } - }, - { - "identity" : "swiftui-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swiftui-navigation.git", - "state" : { - "revision" : "d9e72f3083c08375794afa216fb2f89c0114f303", - "version" : "1.2.1" + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" } }, { - "identity" : "xctest-dynamic-overlay", + "identity" : "vdflow", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "location" : "https://github.com/dankinsoid/VDFlow", "state" : { - "revision" : "b13b1d1a8e787a5ffc71ac19dcaf52183ab27ba2", - "version" : "1.1.1" + "revision" : "243bae56ccb137e48d94034fa995ae7f42e8e095", + "version" : "4.6.0" } } ], diff --git a/Examples/SyncUps/SyncUps.xcodeproj/project.xcworkspace/xcuserdata/danil.xcuserdatad/UserInterfaceState.xcuserstate b/Examples/SyncUps/SyncUps.xcodeproj/project.xcworkspace/xcuserdata/danil.xcuserdatad/UserInterfaceState.xcuserstate index 701d220c60005b3c4c8dae8afb78dbf00d0c4b4c..c2d52a547c633fcb035c48c839ffc7f7584cad53 100644 GIT binary patch literal 73819 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

U%;5DLz}sG(QD{Q^c>oV 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 D1{W4b delta 10118 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 + + diff --git a/Examples/SyncUps/SyncUps.xcodeproj/xcuserdata/danil.xcuserdatad/xcschemes/xcschememanagement.plist b/Examples/SyncUps/SyncUps.xcodeproj/xcuserdata/danil.xcuserdatad/xcschemes/xcschememanagement.plist index ab11eed..e8e63a6 100644 --- a/Examples/SyncUps/SyncUps.xcodeproj/xcuserdata/danil.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Examples/SyncUps/SyncUps.xcodeproj/xcuserdata/danil.xcuserdatad/xcschemes/xcschememanagement.plist @@ -14,21 +14,42 @@ isShown orderHint - 5 + 3 Tagged (Playground) 2.xcscheme + + isShown + + orderHint + 5 + + Tagged (Playground) 3.xcscheme isShown orderHint 6 + Tagged (Playground) 4.xcscheme + + isShown + + orderHint + 7 + + Tagged (Playground) 5.xcscheme + + isShown + + orderHint + 8 + Tagged (Playground).xcscheme isShown orderHint - 3 + 4 diff --git a/Examples/SyncUps/SyncUps/App.swift b/Examples/SyncUps/SyncUps/App.swift index b71f1d9..72d9f7c 100644 --- a/Examples/SyncUps/SyncUps/App.swift +++ b/Examples/SyncUps/SyncUps/App.swift @@ -1,29 +1,28 @@ -import ComposableArchitecture +import VDStore import SwiftUI @main struct SyncUpsApp: App { - let store = Store(initialState: AppFeature.State()) { - AppFeature() - ._printChanges() - } withDependencies: { - if ProcessInfo.processInfo.environment["UITesting"] == "true" { - $0.dataManager = .mock() + + let store = Store(AppFeature()).transformDI { + if ProcessInfo.processInfo.environment["UITesting"] == "true" { + $0.dataManager = .mock() + } } - } + .saveOnChange - var body: some Scene { - WindowGroup { - // NB: This conditional is here only to facilitate UI testing so that we can mock out certain - // dependencies for the duration of the test (e.g. the data manager). We do not really - // recommend performing UI tests in general, but we do want to demonstrate how it can be - // done. - if _XCTIsTesting { - // NB: Don't run application when testing so that it doesn't interfere with tests. - EmptyView() - } else { - AppView(store: store) - } + var body: some Scene { + WindowGroup { + // NB: This conditional is here only to facilitate UI testing so that we can mock out certain + // dependencies for the duration of the test (e.g. the data manager). We do not really + // recommend performing UI tests in general, but we do want to demonstrate how it can be + // done. + if _XCTIsTesting { + // NB: Don't run application when testing so that it doesn't interfere with tests. + EmptyView() + } else { + AppView(store: store) + } + } } - } } diff --git a/Examples/SyncUps/SyncUps/AppFeature.swift b/Examples/SyncUps/SyncUps/AppFeature.swift index ce8ac10..1d58b1e 100644 --- a/Examples/SyncUps/SyncUps/AppFeature.swift +++ b/Examples/SyncUps/SyncUps/AppFeature.swift @@ -1,126 +1,110 @@ -import ComposableArchitecture +import VDStore import SwiftUI - -@Reducer -struct AppFeature { - @Reducer(state: .equatable) - enum Path { - case detail(SyncUpDetail) - case meeting(Meeting, syncUp: SyncUp) - case record(RecordMeeting) - } - - @ObservableState - struct State: Equatable { - var path = StackState() - var syncUpsList = SyncUpsList.State() - } - - enum Action { - case path(StackActionOf) - case syncUpsList(SyncUpsList.Action) - } - - @Dependency(\.continuousClock) var clock - @Dependency(\.date.now) var now - @Dependency(\.dataManager.save) var saveData - @Dependency(\.uuid) var uuid - - private enum CancelID { - case saveDebounce - } - - var body: some ReducerOf { - Scope(state: \.syncUpsList, action: \.syncUpsList) { - SyncUpsList() +import VDFlow + +struct AppFeature: Equatable { + + var path = Path(.detail) + var syncUpsList = SyncUpsList() + + @Steps + struct Path: Equatable { + var detail: SyncUpDetail? + var meeting = MeetingSyncUp() + var record: RecordMeeting? + + struct MeetingSyncUp: Equatable { + var meeting: Meeting? + var syncUp: SyncUp? + } } - Reduce { state, action in - switch action { - case let .path(.element(id, .detail(.delegate(delegateAction)))): - guard case let .some(.detail(detailState)) = state.path[id: id] - else { return .none } - - switch delegateAction { - case .deleteSyncUp: - state.syncUpsList.syncUps.remove(id: detailState.syncUp.id) - return .none - - case let .syncUpUpdated(syncUp): - state.syncUpsList.syncUps[id: syncUp.id] = syncUp - return .none +} - case .startMeeting: - state.path.append(.record(RecordMeeting.State(syncUp: detailState.syncUp))) - return .none +@Actions +extension Store { + + func setPathDetail(id: String, detail: SyncUpDetail) { + switch detail { + case let .delegate(delegateAction): + guard case let .some(.detail(detailState)) = state.path[id: id] else { return .none } + switch delegateAction { + case .deleteSyncUp: + state.syncUpsList.syncUps.remove(id: detailState.syncUp.id) + return .none + + case let .syncUpUpdated(syncUp): + state.syncUpsList.syncUps[id: syncUp.id] = syncUp + return .none + + case .startMeeting: + state.path.append(.record(RecordMeeting.State(syncUp: detailState.syncUp))) + return .none + } } - - case let .path(.element(_, .record(.delegate(delegateAction)))): + } + + func setPathRecord(id: String, record: RecordMeeting) { switch delegateAction { case let .save(transcript: transcript): - guard let id = state.path.ids.dropLast().last - else { - XCTFail( + guard let id = state.path.ids.dropLast().last + else { + XCTFail( """ Record meeting is the only element in the stack. A detail feature should precede it. """ + ) + return .none + } + + state.path[id: id]?.detail?.syncUp.meetings.insert( + Meeting( + id: Meeting.ID(self.uuid()), + date: self.now, + transcript: transcript + ), + at: 0 ) + guard let syncUp = state.path[id: id]?.detail?.syncUp + else { return .none } + state.syncUpsList.syncUps[id: syncUp.id] = syncUp return .none - } - - state.path[id: id]?.detail?.syncUp.meetings.insert( - Meeting( - id: Meeting.ID(self.uuid()), - date: self.now, - transcript: transcript - ), - at: 0 - ) - guard let syncUp = state.path[id: id]?.detail?.syncUp - else { return .none } - state.syncUpsList.syncUps[id: syncUp.id] = syncUp - return .none } - - case .path: - return .none - - case .syncUpsList: - return .none - } } - .forEach(\.path, action: \.path) - Reduce { state, action in - return .run { [syncUps = state.syncUpsList.syncUps] _ in - try await withTaskCancellation(id: CancelID.saveDebounce, cancelInFlight: true) { - try await self.clock.sleep(for: .seconds(1)) - try await self.saveData(JSONEncoder().encode(syncUps), .syncUps) + var saveOnChange: Self { + onChange(of: \.syncUpsList.syncUps) { _, syncUps, _ in + Task { + try await debounceSave(syncUps: syncUps) + } } - } catch: { _, _ in - } } - } + + func debounceSave(syncUps: [SyncUp]) async throws { + cancel(Self.debounceSave) + try await di.clock.sleep(for: .seconds(1)) + try await di.dataManager.save(JSONEncoder().encode(syncUps), .syncUps) + } } struct AppView: View { - @Bindable var store: StoreOf - - var body: some View { - NavigationStack(path: $store.scope(state: \.path, action: \.path)) { - SyncUpsListView( - store: store.scope(state: \.syncUpsList, action: \.syncUpsList) - ) - } destination: { store in - switch store.case { - case let .detail(store): - SyncUpDetailView(store: store) - case let .meeting(meeting, syncUp): - MeetingView(meeting: meeting, syncUp: syncUp) - case let .record(store): - RecordMeetingView(store: store) - } + + @ViewStore var state: AppFeature + + var body: some View { + NavigationStack(path: $state.binding.path.navigationPath) { + SyncUpsListView(store: $state.syncUpsList) + .navigationDestination($state.path.binding, for: \.$detail) { + SyncUpDetailView(store: $state.syncUpsList) + } + .navigationDestination($state.path.binding, \.$meeting) { + MeetingView(meeting: meeting, syncUp: syncUp) + } + .navigationDestination($state.path.binding, for: \.$record) { + RecordMeetingView(store: $state.syncUpsList) + } + } + .stepEnvironment($state.binding.path) } - } } extension URL { diff --git a/Examples/SyncUps/SyncUps/Dependencies/DataManager.swift b/Examples/SyncUps/SyncUps/Dependencies/DataManager.swift index 68ea0b0..3b0b692 100644 --- a/Examples/SyncUps/SyncUps/Dependencies/DataManager.swift +++ b/Examples/SyncUps/SyncUps/Dependencies/DataManager.swift @@ -1,13 +1,12 @@ -import ComposableArchitecture +import VDStore import Foundation -@DependencyClient struct DataManager: Sendable { var load: @Sendable (_ from: URL) throws -> Data var save: @Sendable (Data, _ to: URL) async throws -> Void } -extension DataManager: DependencyKey { +extension DataManager { static let liveValue = Self( load: { url in try Data(contentsOf: url) }, save: { data, url in try data.write(to: url) } @@ -16,14 +15,14 @@ extension DataManager: DependencyKey { static let testValue = Self() } -extension DependencyValues { - var dataManager: DataManager { - get { self[DataManager.self] } - set { self[DataManager.self] = newValue } - } +extension StoreDIValues { + + @StoreDIValue + var dataManager: DataManager = valueFor(live: .liveValue, test: .testValue) } extension DataManager { + static func mock(initialData: Data? = nil) -> Self { let data = LockIsolated(initialData) return Self( diff --git a/Examples/SyncUps/SyncUps/Dependencies/OpenSettings.swift b/Examples/SyncUps/SyncUps/Dependencies/OpenSettings.swift index 835fe9b..c9c8073 100644 --- a/Examples/SyncUps/SyncUps/Dependencies/OpenSettings.swift +++ b/Examples/SyncUps/SyncUps/Dependencies/OpenSettings.swift @@ -1,19 +1,16 @@ -import Dependencies +import VDStore import UIKit -extension DependencyValues { +extension StoreDIValues { + var openSettings: @Sendable () async -> Void { - get { self[OpenSettingsKey.self] } - set { self[OpenSettingsKey.self] = newValue } + get { self[\.openSettings] ?? Self.openSettings } + set { self[\.openSettings] = newValue } } - private enum OpenSettingsKey: DependencyKey { - typealias Value = @Sendable () async -> Void - - static let liveValue: @Sendable () async -> Void = { - await MainActor.run { - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) - } + private static let openSettings: @Sendable () async -> Void = { + await MainActor.run { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + } } - } } diff --git a/Examples/SyncUps/SyncUps/Dependencies/SpeechRecognizer.swift b/Examples/SyncUps/SyncUps/Dependencies/SpeechRecognizer.swift index f7fe525..036f3e8 100644 --- a/Examples/SyncUps/SyncUps/Dependencies/SpeechRecognizer.swift +++ b/Examples/SyncUps/SyncUps/Dependencies/SpeechRecognizer.swift @@ -1,20 +1,18 @@ -import ComposableArchitecture +import VDStore @preconcurrency import Speech -@DependencyClient struct SpeechClient { + var authorizationStatus: @Sendable () -> SFSpeechRecognizerAuthorizationStatus = { .denied } - var requestAuthorization: @Sendable () async -> SFSpeechRecognizerAuthorizationStatus = { - .denied - } + var requestAuthorization: @Sendable () async -> SFSpeechRecognizerAuthorizationStatus = { .denied } var startTask: @Sendable (_ request: SFSpeechAudioBufferRecognitionRequest) async -> AsyncThrowingStream< SpeechRecognitionResult, Error - > = { _ in .finished() } + > = { _ in AsyncThrowingStream { nil } } } -extension SpeechClient: DependencyKey { - static var liveValue: SpeechClient { +extension SpeechClient { + static let liveValue: SpeechClient = { let speech = Speech() return SpeechClient( authorizationStatus: { SFSpeechRecognizer.authorizationStatus() }, @@ -29,9 +27,9 @@ extension SpeechClient: DependencyKey { await speech.startTask(request: request) } ) - } + }() - static var previewValue: SpeechClient { + static let previewValue: SpeechClient = { let isRecording = ActorIsolated(false) return Self( authorizationStatus: { .authorized }, @@ -70,7 +68,7 @@ extension SpeechClient: DependencyKey { } } ) - } + }() static let testValue = SpeechClient() @@ -103,11 +101,23 @@ extension SpeechClient: DependencyKey { } } -extension DependencyValues { - var speechClient: SpeechClient { - get { self[SpeechClient.self] } - set { self[SpeechClient.self] = newValue } - } +final actor ActorIsolated { + + var value: T + + init(_ value: T) { + self.value = value + } + + func `set`(_ value: T) { + self.value = value + } +} + +extension StoreDIValues { + + @StoreDIValue + var speechClient: SpeechClient = valueFor(live: .liveValue, test: .testValue, preview: .previewValue) } struct SpeechRecognitionResult: Equatable { diff --git a/Examples/SyncUps/SyncUps/Meeting.swift b/Examples/SyncUps/SyncUps/Meeting.swift index caacc60..dd3528f 100644 --- a/Examples/SyncUps/SyncUps/Meeting.swift +++ b/Examples/SyncUps/SyncUps/Meeting.swift @@ -1,7 +1,8 @@ -import ComposableArchitecture +import VDStore import SwiftUI struct MeetingView: View { + let meeting: Meeting let syncUp: SyncUp diff --git a/Examples/SyncUps/SyncUps/Models.swift b/Examples/SyncUps/SyncUps/Models.swift index ee3e724..b7f7ded 100644 --- a/Examples/SyncUps/SyncUps/Models.swift +++ b/Examples/SyncUps/SyncUps/Models.swift @@ -1,17 +1,17 @@ -import IdentifiedCollections import SwiftUI import Tagged struct SyncUp: Equatable, Identifiable, Codable { + let id: Tagged - var attendees: IdentifiedArrayOf = [] + var attendees: [Attendee] = [] var duration: Duration = .seconds(60 * 5) - var meetings: IdentifiedArrayOf = [] + var meetings: [Meeting] = [] var theme: Theme = .bubblegum var title = "" var durationPerAttendee: Duration { - self.duration / self.attendees.count + duration / attendees.count } } diff --git a/Examples/SyncUps/SyncUps/RecordMeeting.swift b/Examples/SyncUps/SyncUps/RecordMeeting.swift index d0c4401..8ef6b54 100644 --- a/Examples/SyncUps/SyncUps/RecordMeeting.swift +++ b/Examples/SyncUps/SyncUps/RecordMeeting.swift @@ -1,46 +1,30 @@ -import ComposableArchitecture +import VDStore import Speech import SwiftUI -@Reducer -struct RecordMeeting { - @ObservableState - struct State: Equatable { - @Presents var alert: AlertState? +struct RecordMeeting: Equatable { + + var alert: Alert? var secondsElapsed = 0 var speakerIndex = 0 var syncUp: SyncUp var transcript = "" - + var durationRemaining: Duration { - self.syncUp.duration - .seconds(self.secondsElapsed) + self.syncUp.duration - .seconds(self.secondsElapsed) } - } - - enum Action { - case alert(PresentationAction) - case delegate(Delegate) - case endMeetingButtonTapped - case nextButtonTapped - case onTask - case timerTick - case speechFailure - case speechResult(SpeechRecognitionResult) - - @CasePathable + enum Alert { - case confirmDiscard - case confirmSave + case confirmDiscard + case confirmSave } - @CasePathable + enum Delegate { - case save(transcript: String) + case save(transcript: String) } - } +} - @Dependency(\.continuousClock) var clock - @Dependency(\.dismiss) var dismiss - @Dependency(\.speechClient) var speechClient +extension Store { var body: some ReducerOf { Reduce { state, action in diff --git a/Examples/SyncUps/SyncUps/SyncUpDetail.swift b/Examples/SyncUps/SyncUps/SyncUpDetail.swift index 2307386..773bb80 100644 --- a/Examples/SyncUps/SyncUps/SyncUpDetail.swift +++ b/Examples/SyncUps/SyncUps/SyncUpDetail.swift @@ -1,8 +1,8 @@ -import ComposableArchitecture +import VDStore import SwiftUI @Reducer -struct SyncUpDetail { +struct SyncUpDetail: Equatable { @Reducer(state: .equatable) enum Destination { case alert(AlertState) diff --git a/Examples/SyncUps/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUps/SyncUpForm.swift index 8a09b0a..596e614 100644 --- a/Examples/SyncUps/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUps/SyncUpForm.swift @@ -1,106 +1,103 @@ -import ComposableArchitecture +import VDStore import SwiftUI import SwiftUINavigation -@Reducer -struct SyncUpForm { - @ObservableState - struct State: Equatable, Sendable { +struct SyncUpForm: Equatable { + var focus: Field? = .title var syncUp: SyncUp - init(focus: Field? = .title, syncUp: SyncUp) { - self.focus = focus - self.syncUp = syncUp - if self.syncUp.attendees.isEmpty { - @Dependency(\.uuid) var uuid - self.syncUp.attendees.append(Attendee(id: Attendee.ID(uuid()))) - } + init( + focus: Field? = .title, + syncUp: SyncUp + ) { + self.focus = focus + self.syncUp = syncUp +// if self.syncUp.attendees.isEmpty { +// @Dependency(\.uuid) var uuid +// self.syncUp.attendees.append(Attendee(id: Attendee.ID(uuid()))) +// } } enum Field: Hashable { - case attendee(Attendee.ID) - case title + case attendee(Attendee.ID) + case title } - } - - enum Action: BindableAction, Equatable, Sendable { - case addAttendeeButtonTapped - case binding(BindingAction) - case deleteAttendees(atOffsets: IndexSet) - } +} - @Dependency(\.uuid) var uuid +@Actions +extension Store { - var body: some ReducerOf { - BindingReducer() - Reduce { state, action in - switch action { - case .addAttendeeButtonTapped: - let attendee = Attendee(id: Attendee.ID(self.uuid())) + func addAttendeeButtonTapped() { + let attendee = Attendee(id: Attendee.ID(di.uuid())) state.syncUp.attendees.append(attendee) state.focus = .attendee(attendee.id) - return .none - - case .binding: - return .none + } - case let .deleteAttendees(atOffsets: indices): + func deleteAttendees(atOffsets indices: IndexSet) { state.syncUp.attendees.remove(atOffsets: indices) if state.syncUp.attendees.isEmpty { - state.syncUp.attendees.append(Attendee(id: Attendee.ID(self.uuid()))) + state.syncUp.attendees.append(Attendee(id: Attendee.ID(di.uuid()))) } - guard let firstIndex = indices.first - else { return .none } + guard let firstIndex = indices.first else { return } let index = min(firstIndex, state.syncUp.attendees.count - 1) state.focus = .attendee(state.syncUp.attendees[index].id) - return .none - } } - } } struct SyncUpFormView: View { - @Bindable var store: StoreOf - @FocusState var focus: SyncUpForm.State.Field? + + @ViewStore var state: SyncUpForm + @FocusState var focus: SyncUpForm.Field? + + init(state: SyncUpForm, focus: SyncUpForm.Field? = nil) { + self.state = state + self.focus = focus + } + + init(store: Store, focus: SyncUpForm.Field? = nil) { + _state = ViewStore(store: store) + self.focus = focus + } var body: some View { Form { Section { - TextField("Title", text: $store.syncUp.title) - .focused($focus, equals: .title) + TextField("Title", text: $state.binding.syncUp.title) + .focused($focus, equals: .title) HStack { - Slider(value: $store.syncUp.duration.minutes, in: 5...30, step: 1) { + Slider(value: $state.binding.syncUp.duration.minutes, in: 5...30, step: 1) { Text("Length") } Spacer() - Text(store.syncUp.duration.formatted(.units())) + Text(state.syncUp.duration.formatted(.units())) } - ThemePicker(selection: $store.syncUp.theme) + ThemePicker(selection: $state.binding.syncUp.theme) } header: { Text("Sync-up Info") } Section { - ForEach($store.syncUp.attendees) { $attendee in - TextField("Name", text: $attendee.name) + ForEach(state.syncUp.attendees) { attendee in + TextField("Name", text: attendee.name) .focused($focus, equals: .attendee(attendee.id)) } .onDelete { indices in - store.send(.deleteAttendees(atOffsets: indices)) + $state.deleteAttendees(atOffsets: indices) } Button("New attendee") { - store.send(.addAttendeeButtonTapped) + $state.addAttendeeButtonTapped() } } header: { Text("Attendees") } } - .bind($store.focus, to: $focus) +// .bind($state.binding.focus, to: $focus) } } struct ThemePicker: View { + @Binding var selection: Theme var body: some View { @@ -129,10 +126,6 @@ extension Duration { #Preview { NavigationStack { - SyncUpFormView( - store: Store(initialState: SyncUpForm.State(syncUp: .mock)) { - SyncUpForm() - } - ) + SyncUpFormView(state: SyncUpForm(syncUp: .mock)) } } diff --git a/Examples/SyncUps/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUps/SyncUpsList.swift index af755dc..06069ef 100644 --- a/Examples/SyncUps/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUps/SyncUpsList.swift @@ -1,161 +1,167 @@ -import ComposableArchitecture +import VDStore import SwiftUI - -@Reducer -struct SyncUpsList { - @Reducer(state: .equatable) - enum Destination { - case add(SyncUpForm) - case alert(AlertState) - - @CasePathable - enum Alert { - case confirmLoadMockData +import VDFlow + +struct SyncUpsList: Equatable { + + var destination = Destination() + var syncUps: [SyncUp] + + init( + destination: Destination.Steps? = nil, + syncUps: () throws -> [SyncUp] = { [] } + ) { + self.destination = Destination(destination) + do { + self.syncUps = try syncUps() + } catch is DecodingError { + self.destination.selected = .confirmLoadMockData + } catch { + self.syncUps = [] + } } - } - @ObservableState - struct State: Equatable { - @Presents var destination: Destination.State? - var syncUps: IdentifiedArrayOf = [] + @Steps + struct Destination: Equatable { - init(destination: Destination.State? = nil) { - self.destination = destination - - do { - @Dependency(\.dataManager.load) var load - self.syncUps = try JSONDecoder().decode(IdentifiedArray.self, from: load(.syncUps)) - } catch is DecodingError { - self.destination = .alert(.dataFailedToLoad) - } catch { - } + var add = SyncUpForm(syncUp: SyncUp(id: .init())) + var confirmLoadMockData } - } - - enum Action { - case addSyncUpButtonTapped - case confirmAddSyncUpButtonTapped - case destination(PresentationAction) - case dismissAddSyncUpButtonTapped - case onDelete(IndexSet) - } +} - @Dependency(\.continuousClock) var clock - @Dependency(\.uuid) var uuid +@Actions +extension Store { - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .addSyncUpButtonTapped: - state.destination = .add(SyncUpForm.State(syncUp: SyncUp(id: SyncUp.ID(self.uuid())))) - return .none + func addSyncUpButtonTapped() { + state.destination.add = SyncUpForm(syncUp: SyncUp(id: SyncUp.ID(di.uuid()))) + } - case .confirmAddSyncUpButtonTapped: - guard case let .some(.add(editState)) = state.destination - else { return .none } - var syncUp = editState.syncUp + func confirmAddSyncUpButtonTapped() { + var syncUp = state.destination.add.syncUp syncUp.attendees.removeAll { attendee in - attendee.name.allSatisfy(\.isWhitespace) + attendee.name.allSatisfy(\.isWhitespace) } if syncUp.attendees.isEmpty { - syncUp.attendees.append( - editState.syncUp.attendees.first - ?? Attendee(id: Attendee.ID(self.uuid())) - ) + syncUp.attendees.append( + state.destination.add.syncUp.attendees.first + ?? Attendee(id: Attendee.ID(di.uuid())) + ) } state.syncUps.append(syncUp) - state.destination = nil - return .none + state.destination.selected = nil + } - case .destination(.presented(.alert(.confirmLoadMockData))): + func destinationPresented() { + state.destination.confirmLoadMockData.select() state.syncUps = [ - .mock, - .designMock, - .engineeringMock, + .mock, + .designMock, + .engineeringMock, ] - return .none - - case .destination: - return .none + } - case .dismissAddSyncUpButtonTapped: - state.destination = nil - return .none + func dismissAddSyncUpButtonTapped() { + state.destination.selected = nil + } - case let .onDelete(indexSet): + func onDelete(indexSet: IndexSet) { state.syncUps.remove(atOffsets: indexSet) - return .none - } } - .ifLet(\.$destination, action: \.destination) - } } struct SyncUpsListView: View { - @Bindable var store: StoreOf - - var body: some View { - List { - ForEach(store.syncUps) { syncUp in - NavigationLink( - state: AppFeature.Path.State.detail(SyncUpDetail.State(syncUp: syncUp)) - ) { - CardView(syncUp: syncUp) - } - .listRowBackground(syncUp.theme.mainColor) - } - .onDelete { indexSet in - store.send(.onDelete(indexSet)) - } + + @ViewStore var state: SyncUpsList + @StateStep var feature: AppFeature.Path + + init(state: SyncUpsList) { + self.state = state } - .toolbar { - Button { - store.send(.addSyncUpButtonTapped) - } label: { - Image(systemName: "plus") - } + + init(store: Store) { + self._state = ViewStore(store: store) } - .navigationTitle("Daily Sync-ups") - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) - .sheet(item: $store.scope(state: \.destination?.add, action: \.destination.add)) { store in - NavigationStack { - SyncUpFormView(store: store) - .navigationTitle("New sync-up") - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Dismiss") { - self.store.send(.dismissAddSyncUpButtonTapped) - } + + var body: some View { + List { + ForEach(state.syncUps) { syncUp in + NavigationLink(value: AppFeature.Path.Steps.detail) { + CardView(syncUp: syncUp) + } +// Button { +// feature.detail = SyncUpDetail.State(syncUp: syncUp) +// } label: { +// CardView(syncUp: syncUp) +// } + .listRowBackground(syncUp.theme.mainColor) } - ToolbarItem(placement: .confirmationAction) { - Button("Add") { - self.store.send(.confirmAddSyncUpButtonTapped) - } + .onDelete { + $state.onDelete(indexSet: $0) } - } - } + } + .toolbar { + Button { + $state.addSyncUpButtonTapped() + } label: { + Image(systemName: "plus") + } + } + .navigationTitle("Daily Sync-ups") + .syncUpsListAlert($state) + .sheet( + isPresented: $state.binding.destination.isSelected(.add) + ) { + NavigationStack { + SyncUpFormView(store: $state.destination.add) + .navigationTitle("New sync-up") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Dismiss") { + $state.dismissAddSyncUpButtonTapped() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Add") { + $state.dismissAddSyncUpButtonTapped() + } + } + } + } + } } - } } -extension AlertState where Action == SyncUpsList.Destination.Alert { - static let dataFailedToLoad = Self { - TextState("Data failed to load") - } actions: { - ButtonState(action: .send(.confirmLoadMockData, animation: .default)) { - TextState("Yes") - } - ButtonState(role: .cancel) { - TextState("No") - } - } message: { - TextState( +extension View { + + @MainActor + func syncUpsListAlert( + _ store: Store + ) -> some View { + self.alert( + "Data failed to load", + isPresented: Binding { + store.state.destination.selected == .confirmLoadMockData + } set: { + if $0 { + store.destinationPresented() + } + } + ) { + Button("Yes") { + store.withAnimation { + store.destinationPresented() + } + } + Button("No", role: .cancel) {} + } message: { + Text( """ Unfortunately your past data failed to load. Would you like to load some mock data to play \ around with? """ - ) - } + ) + } + } } struct CardView: View { @@ -193,30 +199,26 @@ extension LabelStyle where Self == TrailingIconLabelStyle { } #Preview { - SyncUpsListView( - store: Store(initialState: SyncUpsList.State()) { - SyncUpsList() - } withDependencies: { - $0.dataManager.load = { @Sendable _ in - try JSONEncoder().encode([ - SyncUp.mock, - .designMock, - .engineeringMock, - ]) - } - } - ) + SyncUpsListView( + store: Store( + SyncUpsList {[ + SyncUp.mock, + .designMock, + .engineeringMock, + ]} + ) + ) } #Preview("Load data failure") { - SyncUpsListView( - store: Store(initialState: SyncUpsList.State()) { - SyncUpsList() - } withDependencies: { - $0.dataManager = .mock(initialData: Data("!@#$% bad data ^&*()".utf8)) - } - ) - .previewDisplayName("Load data failure") + SyncUpsListView( + store: Store( + SyncUpsList { + try JSONDecoder().decode([SyncUp].self, from: Data("!@#$% bad data ^&*()".utf8)) + } + ) + ) + .previewDisplayName("Load data failure") } #Preview("Card") { diff --git a/Examples/SyncUps/SyncUpsTests/AppFeatureTests.swift b/Examples/SyncUps/SyncUpsTests/AppFeatureTests.swift index cf07cfb..d2dafc1 100644 --- a/Examples/SyncUps/SyncUpsTests/AppFeatureTests.swift +++ b/Examples/SyncUps/SyncUpsTests/AppFeatureTests.swift @@ -1,4 +1,4 @@ -import ComposableArchitecture +import VDStore import XCTest @testable import SyncUps diff --git a/Examples/SyncUps/SyncUpsTests/RecordMeetingTests.swift b/Examples/SyncUps/SyncUpsTests/RecordMeetingTests.swift index 2b63f42..274172f 100644 --- a/Examples/SyncUps/SyncUpsTests/RecordMeetingTests.swift +++ b/Examples/SyncUps/SyncUpsTests/RecordMeetingTests.swift @@ -1,4 +1,4 @@ -import ComposableArchitecture +import VDStore import XCTest @testable import SyncUps diff --git a/Examples/SyncUps/SyncUpsTests/SyncUpDetailTests.swift b/Examples/SyncUps/SyncUpsTests/SyncUpDetailTests.swift index a237d02..6390fb8 100644 --- a/Examples/SyncUps/SyncUpsTests/SyncUpDetailTests.swift +++ b/Examples/SyncUps/SyncUpsTests/SyncUpDetailTests.swift @@ -1,4 +1,4 @@ -import ComposableArchitecture +import VDStore import XCTest @testable import SyncUps diff --git a/Examples/SyncUps/SyncUpsTests/SyncUpFormTests.swift b/Examples/SyncUps/SyncUpsTests/SyncUpFormTests.swift index 66dc787..70cff3d 100644 --- a/Examples/SyncUps/SyncUpsTests/SyncUpFormTests.swift +++ b/Examples/SyncUps/SyncUpsTests/SyncUpFormTests.swift @@ -1,4 +1,4 @@ -import ComposableArchitecture +import VDStore import XCTest @testable import SyncUps diff --git a/Examples/SyncUps/SyncUpsTests/SyncUpsListTests.swift b/Examples/SyncUps/SyncUpsTests/SyncUpsListTests.swift index 1cdf676..0cb087a 100644 --- a/Examples/SyncUps/SyncUpsTests/SyncUpsListTests.swift +++ b/Examples/SyncUps/SyncUpsTests/SyncUpsListTests.swift @@ -1,4 +1,4 @@ -import ComposableArchitecture +import VDStore import XCTest @testable import SyncUps diff --git a/README.md b/README.md index 891fe15..92822b5 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ import PackageDescription let package = Package( name: "SomeProject", dependencies: [ - .package(url: "https://github.com/dankinsoid/VDStore.git", from: "0.23.0") + .package(url: "https://github.com/dankinsoid/VDStore.git", from: "0.24.0") ], targets: [ .target(name: "SomeProject", dependencies: ["VDStore"]) diff --git a/Sources/VDStore/Action.swift b/Sources/VDStore/Action.swift index b84b062..29e3b44 100644 --- a/Sources/VDStore/Action.swift +++ b/Sources/VDStore/Action.swift @@ -122,6 +122,7 @@ public extension Store.Action { } } +@MainActor public extension Store { /// Executes the given action through middlwares. diff --git a/Sources/VDStore/Dependencies/TasksStorage.swift b/Sources/VDStore/Dependencies/TasksStorage.swift index 5e46a77..eae2080 100644 --- a/Sources/VDStore/Dependencies/TasksStorage.swift +++ b/Sources/VDStore/Dependencies/TasksStorage.swift @@ -94,10 +94,10 @@ public extension Store { public extension Task { /// Store the task in the storage by it cancellation id. - @MainActor + @MainActor @discardableResult - func store(in store: TasksStorage, id: AnyHashable) -> Task { - store.add(for: id, self) + func store(in storage: TasksStorage, id: AnyHashable) -> Task { + storage.add(for: id, self) return self } } diff --git a/Sources/VDStore/Dependencies/UUID.swift b/Sources/VDStore/Dependencies/UUID.swift new file mode 100644 index 0000000..bc71568 --- /dev/null +++ b/Sources/VDStore/Dependencies/UUID.swift @@ -0,0 +1,116 @@ +import Foundation + +extension StoreDIValues { + + /// A dependency that generates UUIDs. + /// + /// Introduce controllable UUID generation to your features by using the ``di`` property + /// with a key path to this property. The wrapped value is an instance of + /// ``UUIDGenerator``, which can be called with a closure to create UUIDs. (It can be called + /// directly because it defines ``UUIDGenerator/callAsFunction()``, which is called when you + /// invoke the instance as you would invoke a function.) + /// + /// For example, you could introduce controllable UUID generation to an observable object model + /// that creates to-dos with unique identifiers: + /// + /// ```swift + /// extension Store { + /// + /// func addButtonTapped() { + /// state.todos.append(Todo(id: di.uuid())) + /// } + /// } + /// ``` + /// + /// By default, a "live" generator is supplied, which returns a random UUID when called by + /// invoking `UUID.init` under the hood. When used in tests, an "unimplemented" generator that + /// additionally reports test failures if invoked, unless explicitly overridden. + /// + /// To test a feature that depends on UUID generation, you can override its generator using + /// ``di(_:_:)-4uz6m`` to override the underlying ``UUIDGenerator``: + /// + /// * ``UUIDGenerator/incrementing`` for reproducible UUIDs that count up from + /// `00000000-0000-0000-0000-000000000000`. + /// + /// * ``UUIDGenerator/constant(_:)`` for a generator that always returns the given UUID. + /// + /// For example, you could test the to-do-creating model by supplying an + /// ``UUIDGenerator/incrementing`` generator as a dependency: + /// + /// ```swift + /// func testFeature() { + /// let model = store.di(\.uuid, .incrementing) + /// + /// model.addButtonTapped() + /// XCTAssertEqual( + /// model.state.todos, + /// [Todo(id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!)] + /// ) + /// } + /// ``` + public var uuid: UUIDGenerator { + get { self[\.uuid] ?? UUIDGenerator { UUID() } } + set { self[\.uuid] = newValue } + } +} + +/// A dependency that generates a UUID. +/// +/// See ``StoreDIValues/uuid`` for more information. +public struct UUIDGenerator: Sendable { + private let generate: @Sendable () -> UUID + + /// A generator that returns a constant UUID. + /// + /// - Parameter uuid: A UUID to return. + /// - Returns: A generator that always returns the given UUID. + public static func constant(_ uuid: UUID) -> Self { + Self { uuid } + } + + /// A generator that generates UUIDs in incrementing order. + /// + /// For example: + /// + /// ```swift + /// let generate = UUIDGenerator.incrementing + /// generate() // UUID(00000000-0000-0000-0000-000000000000) + /// generate() // UUID(00000000-0000-0000-0000-000000000001) + /// generate() // UUID(00000000-0000-0000-0000-000000000002) + /// ``` + public static var incrementing: Self { + let generator = IncrementingUUIDGenerator() + return Self { generator() } + } + + /// Initializes a UUID generator that generates a UUID from a closure. + /// + /// - Parameter generate: A closure that returns the current date when called. + public init(_ generate: @escaping @Sendable () -> UUID) { + self.generate = generate + } + + public func callAsFunction() -> UUID { + self.generate() + } +} + +extension UUID { + public init(_ intValue: Int) { + self.init(uuidString: "00000000-0000-0000-0000-\(String(format: "%012x", intValue))")! + } +} + +private final class IncrementingUUIDGenerator: @unchecked Sendable { + private let lock = NSLock() + private var sequence = 0 + + func callAsFunction() -> UUID { + self.lock.lock() + defer { + self.sequence += 1 + self.lock.unlock() + } + return UUID(self.sequence) + } +} diff --git a/Sources/VDStore/Store.swift b/Sources/VDStore/Store.swift index 317b725..9cc96b7 100644 --- a/Sources/VDStore/Store.swift +++ b/Sources/VDStore/Store.swift @@ -83,10 +83,10 @@ import Foundation /// ### Thread safety /// /// The `Store` class is isolated to main thread by @MainActor attribute. -@MainActor @propertyWrapper @dynamicMemberLookup -public struct Store { +@MainActor +public struct Store: Sendable { /// The state of the store. public var state: State { @@ -95,7 +95,7 @@ public struct Store { } /// Injected dependencies. - public var di: StoreDIValues { + public nonisolated var di: StoreDIValues { diModifier(StoreDIValues().with(store: self)) } @@ -111,20 +111,31 @@ public struct Store { StorePublisher(upstream: box.eraseToAnyPublisher()) } + /// An async sequence that emits when state changes. + /// + /// This sequence supports dynamic member lookup so that you can pluck out a specific field in the state: + /// + /// ```swift + /// for await state in store.async.alert { ... } + /// ``` + public nonisolated var async: StoreAsyncSequence { + StoreAsyncSequence(upstream: box.eraseToAnyPublisher()) + } + /// The publisher that emits before the state is going to be changed. Required by `SwiftUI`. - nonisolated var willSet: AnyPublisher { + nonisolated var willSet: AnyPublisher { box.willSet.eraseToAnyPublisher() } private let box: StoreBox - private let diModifier: (StoreDIValues) -> StoreDIValues + private let diModifier: @Sendable (StoreDIValues) -> StoreDIValues public var wrappedValue: State { get { state } nonmutating set { state = newValue } } - public var projectedValue: Store { + public nonisolated var projectedValue: Store { get { self } set { self = newValue } } @@ -139,9 +150,9 @@ public struct Store { self.init(box: StoreBox(state)) } - nonisolated init( + nonisolated init( box: StoreBox, - di: @escaping (StoreDIValues) -> StoreDIValues = { $0 } + di: @escaping @Sendable (StoreDIValues) -> StoreDIValues = { $0 } ) { self.box = box diModifier = di @@ -179,7 +190,7 @@ public struct Store { /// - get: A closure that gets the child state from the parent state. /// - set: A closure that modifies the parent state from the child state. /// - Returns: A new store with its state transformed. - public func scope( + public nonisolated func scope( get getter: @escaping (State) -> ChildState, set setter: @escaping (inout State, ChildState) -> Void ) -> Store { @@ -216,7 +227,7 @@ public struct Store { /// - Parameters: /// - keyPath: A writable key path from `State` to `ChildState`. /// - Returns: A new store with its state transformed. - public func scope(_ keyPath: WritableKeyPath) -> Store { + public nonisolated func scope(_ keyPath: WritableKeyPath) -> Store { scope { $0[keyPath: keyPath] } set: { @@ -251,7 +262,7 @@ public struct Store { /// - Parameters: /// - keyPath: A writable key path from `State` to `ChildState`. /// - Returns: A new store with its state transformed. - public subscript( + public nonisolated subscript( dynamicMember keyPath: WritableKeyPath ) -> Store { scope(keyPath) @@ -262,7 +273,7 @@ public struct Store { /// - keyPath: A key path to the value in the store's dependencies. /// - value: The value to inject. /// - Returns: A new store with the injected value. - public func di( + public nonisolated func di( _ keyPath: WritableKeyPath, _ value: DIValue ) -> Store { @@ -275,7 +286,7 @@ public struct Store { /// - Parameters: /// - transform: A closure that transforms the store's dependencies. /// - Returns: A new store with the transformed dependencies. - public func transformDI( + public nonisolated func transformDI( _ transform: @escaping (StoreDIValues) -> StoreDIValues ) -> Store { Store(box: box) { [diModifier] in @@ -287,7 +298,7 @@ public struct Store { /// - Parameters: /// - transform: A closure that transforms the store's dependencies. /// - Returns: A new store with the transformed dependencies. - public func transformDI( + public nonisolated func transformDI( _ transform: @escaping (inout StoreDIValues) -> Void ) -> Store { transformDI { @@ -298,7 +309,7 @@ public struct Store { } /// Suspends the store from updating the UI until the block returns. - public func update(_ update: () throws -> T) rethrows -> T { + public func update(_ update: @MainActor () throws -> T) rethrows -> T { box.startUpdate() defer { box.endUpdate() } let result = try update() @@ -308,11 +319,11 @@ public struct Store { public extension Store where State: MutableCollection { - subscript(_ index: State.Index) -> Store { + nonisolated subscript(_ index: State.Index) -> Store { scope(index) } - func scope(_ index: State.Index) -> Store { + nonisolated func scope(_ index: State.Index) -> Store { scope { $0[index] } set: { diff --git a/Sources/VDStore/StoreDependencies.swift b/Sources/VDStore/StoreDependencies.swift index 330b8fe..e726ab9 100644 --- a/Sources/VDStore/StoreDependencies.swift +++ b/Sources/VDStore/StoreDependencies.swift @@ -81,5 +81,42 @@ public func valueFor( #endif } -private let _XCTIsTesting: Bool = ProcessInfo.processInfo.environment.keys.contains("XCTestBundlePath") -private let _isPreview: Bool = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" +public let _isPreview: Bool = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + +#if !os(WASI) +public let _XCTIsTesting: Bool = { + ProcessInfo.processInfo.environment.keys.contains("XCTestBundlePath") + || ProcessInfo.processInfo.environment.keys.contains("XCTestConfigurationFilePath") + || ProcessInfo.processInfo.environment.keys.contains("XCTestSessionIdentifier") + || (ProcessInfo.processInfo.arguments.first + .flatMap(URL.init(fileURLWithPath:)) + .map { $0.lastPathComponent == "xctest" || $0.pathExtension == "xctest" } + ?? false) + || XCTCurrentTestCase != nil +}() +#else +public let _XCTIsTesting = false +#endif + +#if canImport(ObjectiveC) +private var XCTCurrentTestCase: AnyObject? { + guard + let XCTestObservationCenter = NSClassFromString("XCTestObservationCenter"), + let XCTestObservationCenter = XCTestObservationCenter as Any as? NSObjectProtocol, + let shared = XCTestObservationCenter.perform(Selector(("sharedTestObservationCenter")))? + .takeUnretainedValue(), + let observers = shared.perform(Selector(("observers")))? + .takeUnretainedValue() as? [AnyObject], + let observer = + observers + .first(where: { NSStringFromClass(type(of: $0)) == "XCTestMisuseObserver" }), + let currentTestCase = observer.perform(Selector(("currentTestCase")))? + .takeUnretainedValue() + else { return nil } + return currentTestCase +} +#else +private var XCTCurrentTestCase: AnyObject? { + nil +} +#endif diff --git a/Sources/VDStore/StoreExtensions/ForEach.swift b/Sources/VDStore/StoreExtensions/ForEach.swift new file mode 100644 index 0000000..4be4095 --- /dev/null +++ b/Sources/VDStore/StoreExtensions/ForEach.swift @@ -0,0 +1,18 @@ +import Foundation + +public extension Store where State: MutableCollection { + + @MainActor + func forEach(_ operation: (Store) throws -> Void) rethrows { + for index in state.indices { + try operation(self[index]) + } + } + + @MainActor + func forEach(_ operation: (Store) async throws -> Void) async rethrows { + for index in state.indices { + try await operation(self[index]) + } + } +} diff --git a/Sources/VDStore/StoreExtensions/Iflet.swift b/Sources/VDStore/StoreExtensions/Iflet.swift deleted file mode 100644 index f5fad10..0000000 --- a/Sources/VDStore/StoreExtensions/Iflet.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -public extension Store { - - func or(_ defaultValue: @escaping @autoclosure () -> T) -> Store where T? == State { - scope { - $0 ?? defaultValue() - } set: { - $0 = $1 - } - } - - func onChange( - of keyPath: WritableKeyPath, - removeDuplicates isDuplicate: @escaping (V, V) -> Bool, - _ operation: @MainActor @escaping (_ oldValue: V, _ newValue: V, inout State) -> Void - ) -> Store { - scope { - $0 - } set: { - let oldValue = $0[keyPath: keyPath] - $0 = $1 - operation(oldValue, $1[keyPath: keyPath], &$0) - } - } - - func onChange( - of keyPath: WritableKeyPath, - _ operation: @MainActor @escaping (_ oldValue: V, _ newValue: V, inout State) -> Void - ) -> Store { - onChange(of: keyPath, removeDuplicates: ==, operation) - } -} - -public extension Store where State: MutableCollection { - - func forEach(_ operation: @MainActor (Store) throws -> Void) rethrows { - for index in state.indices { - try operation(self[index]) - } - } - - func forEach(_ operation: @MainActor (Store) async throws -> Void) async rethrows { - for index in state.indices { - try await operation(self[index]) - } - } -} diff --git a/Sources/VDStore/StoreExtensions/OnChange.swift b/Sources/VDStore/StoreExtensions/OnChange.swift new file mode 100644 index 0000000..372eb0d --- /dev/null +++ b/Sources/VDStore/StoreExtensions/OnChange.swift @@ -0,0 +1,25 @@ +import Foundation + +public extension Store { + + func onChange( + of keyPath: WritableKeyPath, + removeDuplicates isDuplicate: @escaping (V, V) -> Bool, + _ operation: @escaping (_ oldValue: V, _ newValue: V, inout State) -> Void + ) -> Store { + scope { + $0 + } set: { + let oldValue = $0[keyPath: keyPath] + $0 = $1 + operation(oldValue, $1[keyPath: keyPath], &$0) + } + } + + func onChange( + of keyPath: WritableKeyPath, + _ operation: @escaping (_ oldValue: V, _ newValue: V, inout State) -> Void + ) -> Store { + onChange(of: keyPath, removeDuplicates: ==, operation) + } +} diff --git a/Sources/VDStore/StoreExtensions/Or.swift b/Sources/VDStore/StoreExtensions/Or.swift new file mode 100644 index 0000000..f7c729b --- /dev/null +++ b/Sources/VDStore/StoreExtensions/Or.swift @@ -0,0 +1,12 @@ +import Foundation + +public extension Store { + + func or(_ defaultValue: @escaping @autoclosure () -> T) -> Store where T? == State { + scope { + $0 ?? defaultValue() + } set: { + $0 = $1 + } + } +} diff --git a/Sources/VDStore/StoreExtensions/WithAnimaiton.swift b/Sources/VDStore/StoreExtensions/WithAnimaiton.swift new file mode 100644 index 0000000..023fd85 --- /dev/null +++ b/Sources/VDStore/StoreExtensions/WithAnimaiton.swift @@ -0,0 +1,12 @@ +import SwiftUI + +extension Store { + + @MainActor + /// Suspends the store from updating the UI until the block returns. + public func withAnimation(_ animation: Animation? = .default, _ update: @MainActor () throws -> T) rethrows -> T { + try SwiftUI.withAnimation(animation) { + try self.update(update) + } + } +} diff --git a/Sources/VDStore/Utils/Binding++.swift b/Sources/VDStore/Utils/Binding++.swift new file mode 100644 index 0000000..a91364c --- /dev/null +++ b/Sources/VDStore/Utils/Binding++.swift @@ -0,0 +1,26 @@ +import SwiftUI + +extension Binding { + + public func `didSet`(_ action: @escaping (Value, Value) -> Void) -> Binding { + Binding( + get: { wrappedValue }, + set: { newValue in + let oldValue = wrappedValue + wrappedValue = newValue + action(oldValue, newValue) + } + ) + } + + public func `willSet`(_ action: @escaping (Value, Value) -> Void) -> Binding { + Binding( + get: { wrappedValue }, + set: { newValue in + let oldValue = wrappedValue + action(oldValue, newValue) + wrappedValue = newValue + } + ) + } +} diff --git a/Sources/VDStore/Utils/Ref.swift b/Sources/VDStore/Utils/Ref.swift deleted file mode 100644 index fdb5b55..0000000 --- a/Sources/VDStore/Utils/Ref.swift +++ /dev/null @@ -1,71 +0,0 @@ -import Foundation - -/// The property wrapper for the reference to the state. -@dynamicMemberLookup -@propertyWrapper -public struct Ref { - - private let getter: () -> State - private let setter: (State) -> Void - - public var wrappedValue: State { - get { getter() } - nonmutating set { setter(newValue) } - } - - public var projectedValue: Ref { - get { self } - set { self = newValue } - } - - public init(get: @escaping () -> State, set: @escaping (State) -> Void) { - getter = get - setter = set - } - - /// Returns the reference to the substate. - public func scope( - get childGet: @escaping (State) -> ChildState, - set childSet: @escaping (inout State, ChildState) -> Void - ) -> Ref { - Ref( - get: { childGet(getter()) }, - set: { - var state = getter() - childSet(&state, $0) - setter(state) - } - ) - } - - /// Returns the reference to the substate. - public func scope(_ keyPath: WritableKeyPath) -> Ref { - scope( - get: { $0[keyPath: keyPath] }, - set: { $0[keyPath: keyPath] = $1 } - ) - } - - public subscript(dynamicMember keyPath: WritableKeyPath) -> Ref { - scope(keyPath) - } -} - -#if canImport(SwiftUI) -import SwiftUI - -public extension Ref { - - init(_ binding: Binding) { - self.init( - get: { binding.wrappedValue }, - set: { binding.wrappedValue = $0 } - ) - } - - /// Returns the SwiftUI binding to the state. - var binding: Binding { - Binding(get: getter, set: setter) - } -} -#endif diff --git a/Sources/VDStore/Utils/StoreBox.swift b/Sources/VDStore/Utils/StoreBox.swift index 39984a1..9903dfa 100644 --- a/Sources/VDStore/Utils/StoreBox.swift +++ b/Sources/VDStore/Utils/StoreBox.swift @@ -54,22 +54,28 @@ private final class StoreRootBox: Publisher { typealias Output = State typealias Failure = Never + private var _state: State var state: State { - willSet { + get { + _$observationRegistrar.access(box: self) + return _state + } + set { if updatesCounter == 0 { if suspendAllSyncStoreUpdates { if asyncUpdatesCounter == 0 { suspendSyncUpdates() } } else { - willSetSubject.send() + sendWillSet() } } - } - didSet { - if updatesCounter == 0, asyncUpdatesCounter == 0 { - didSetSubject.send() - } + + _state = newValue + + if updatesCounter == 0, asyncUpdatesCounter == 0 { + sendDidSet() + } } } @@ -81,21 +87,27 @@ private final class StoreRootBox: Publisher { private var asyncUpdatesCounter = 0 private let willSetSubject = PassthroughSubject() private let didSetSubject = PassthroughSubject() + private let _$observationRegistrar: ObservationRegistrarProtocol init(_ state: State) { - self.state = state + _state = state + if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { + _$observationRegistrar = ObservationRegistrar() + } else { + _$observationRegistrar = MockObservationRegistrar() + } } func receive(subscriber: S) where S: Subscriber, Never == S.Failure, Output == S.Input { didSetSubject - .compactMap { [weak self] in self?.state } - .prepend(state) + .compactMap { [weak self] in self?._state } + .prepend(_state) .receive(subscriber: subscriber) } func startUpdate() { if updatesCounter == 0, asyncUpdatesCounter == 0 { - willSetSubject.send() + sendWillSet() } updatesCounter &+= 1 } @@ -103,10 +115,10 @@ private final class StoreRootBox: Publisher { func endUpdate() { updatesCounter &-= 1 guard updatesCounter == 0 else { return } - didSetSubject.send() + sendDidSet() if asyncUpdatesCounter > 0 { - willSetSubject.send() + sendWillSet() } } @@ -119,7 +131,7 @@ private final class StoreRootBox: Publisher { private func startAsyncUpdate() { if asyncUpdatesCounter == 0 { - willSetSubject.send() + sendWillSet() } asyncUpdatesCounter &+= 1 } @@ -127,7 +139,54 @@ private final class StoreRootBox: Publisher { private func endAsyncUpdate() { asyncUpdatesCounter &-= 1 if asyncUpdatesCounter == 0 { - didSetSubject.send() + sendDidSet() } } + + private func sendWillSet() { + willSetSubject.send() + _$observationRegistrar.willSet(box: self) + } + + private func sendDidSet() { + didSetSubject.send() + _$observationRegistrar.didSet(box: self) + } +} + +private protocol ObservationRegistrarProtocol { + func access(box: StoreRootBox) + func willSet(box: StoreRootBox) + func didSet(box: StoreRootBox) + func withMutation(box: StoreRootBox, _ mutation: () throws -> T) rethrows -> T +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension StoreRootBox: Observable { +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension ObservationRegistrar: ObservationRegistrarProtocol { + fileprivate func access(box: StoreRootBox) { + access(box, keyPath: \.state) + } + + fileprivate func willSet(box: StoreRootBox) { + willSet(box, keyPath: \.state) + } + fileprivate func didSet(box: StoreRootBox) { + didSet(box, keyPath: \.state) + } + fileprivate func withMutation(box: StoreRootBox, _ mutation: () throws -> T) rethrows -> T { + try withMutation(of: box, keyPath: \.state, mutation) + } +} + +private struct MockObservationRegistrar: ObservationRegistrarProtocol { + func access(box: StoreRootBox){} + func willSet(box: StoreRootBox) {} + func didSet(box: StoreRootBox) {} + func withMutation(box: StoreRootBox, _ mutation: () throws -> T) rethrows -> T { + try mutation() + } } diff --git a/Sources/VDStore/Utils/StorePublisher.swift b/Sources/VDStore/Utils/StorePublisher.swift deleted file mode 100644 index f6d602c..0000000 --- a/Sources/VDStore/Utils/StorePublisher.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Combine -import Foundation - -/// A publisher of store state. -@dynamicMemberLookup -public struct StorePublisher: Publisher { - - public typealias Output = State - public typealias Failure = Never - - let upstream: AnyPublisher - - public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { - upstream.receive(subscriber: subscriber) - } - - /// Returns the resulting publisher of a given key path. - public subscript( - dynamicMember keyPath: KeyPath - ) -> StorePublisher { - StorePublisher(upstream: upstream.map(keyPath).removeDuplicates().eraseToAnyPublisher()) - } - - /// Returns the resulting publisher of a given key path. - @_disfavoredOverload - public subscript( - dynamicMember keyPath: KeyPath - ) -> StorePublisher { - StorePublisher(upstream: upstream.map(keyPath).eraseToAnyPublisher()) - } -} diff --git a/Sources/VDStore/Utils/StoreUpdates.swift b/Sources/VDStore/Utils/StoreUpdates.swift new file mode 100644 index 0000000..ed25c92 --- /dev/null +++ b/Sources/VDStore/Utils/StoreUpdates.swift @@ -0,0 +1,70 @@ +import Combine +import Foundation + +/// An async sequence and publisher of store state. +@dynamicMemberLookup +public struct StorePublisher: Publisher { + + public typealias Output = State + public typealias Failure = Never + + let upstream: AnyPublisher + + public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { + upstream.receive(subscriber: subscriber) + } + + /// Returns the resulting sequence of a given key path. + public subscript( + dynamicMember keyPath: KeyPath + ) -> StorePublisher { + StorePublisher(upstream: upstream.map(keyPath).removeDuplicates().eraseToAnyPublisher()) + } + + /// Returns the resulting sequence of a given key path. + @_disfavoredOverload + public subscript( + dynamicMember keyPath: KeyPath + ) -> StorePublisher { + StorePublisher(upstream: upstream.map(keyPath).eraseToAnyPublisher()) + } +} + +/// An async sequence and publisher of store state. +@dynamicMemberLookup +public struct StoreAsyncSequence: AsyncSequence { + + public typealias AsyncIterator = AsyncStream.AsyncIterator + public typealias Element = State + + let upstream: AnyPublisher + + /// Returns the resulting sequence of a given key path. + public subscript( + dynamicMember keyPath: KeyPath + ) -> StoreAsyncSequence { + StoreAsyncSequence(upstream: upstream.map(keyPath).removeDuplicates().eraseToAnyPublisher()) + } + + /// Returns the resulting sequence of a given key path. + @_disfavoredOverload + public subscript( + dynamicMember keyPath: KeyPath + ) -> StoreAsyncSequence { + StoreAsyncSequence(upstream: upstream.map(keyPath).eraseToAnyPublisher()) + } + + public func makeAsyncIterator() -> AsyncStream.AsyncIterator { + AsyncStream { continuation in + let cancellable = upstream.sink { _ in + continuation.finish() + } receiveValue: { + continuation.yield($0) + } + continuation.onTermination = { _ in + cancellable.cancel() + } + } + .makeAsyncIterator() + } +} diff --git a/Sources/VDStore/ViewStore.swift b/Sources/VDStore/ViewStore.swift index 4d1d256..c8af06e 100644 --- a/Sources/VDStore/ViewStore.swift +++ b/Sources/VDStore/ViewStore.swift @@ -95,6 +95,7 @@ extension EnvironmentValues { } } +@MainActor public extension Store { /// SwiftUI binding to store's state. diff --git a/Sources/VDStoreMacros/ActionsMacro.swift b/Sources/VDStoreMacros/ActionsMacro.swift index 5293cce..52bfb85 100644 --- a/Sources/VDStoreMacros/ActionsMacro.swift +++ b/Sources/VDStoreMacros/ActionsMacro.swift @@ -129,6 +129,7 @@ private func expansion( var executeDecl = funcDecl executeDecl.remove(attribute: "Action") executeDecl.remove(attribute: "_disfavoredOverload") +// executeDecl.add(attribute: "MainActor") // executeDecl.modifiers.remove(at: privateIndex) var parameterList = executeDecl.signature.parameterClause.parameters.map { FunctionParameterSyntax( diff --git a/Sources/VDStoreMacros/Extensions.swift b/Sources/VDStoreMacros/Extensions.swift index c0640c0..0e33d55 100644 --- a/Sources/VDStoreMacros/Extensions.swift +++ b/Sources/VDStoreMacros/Extensions.swift @@ -21,6 +21,16 @@ extension FunctionDeclSyntax { attributes.remove(at: i) } } + + mutating func add(attribute: String) { + if attributes.contains(where: { $0.as(AttributeSyntax.self)?.attributeName.description == attribute }) { + return + } + attributes.insert( + .attribute(AttributeSyntax("\(raw: attribute)")), + at: attributes.startIndex + ) + } } extension MacroExpansionContext { diff --git a/Tests/VDStoreTests/VDStoreTests.swift b/Tests/VDStoreTests/VDStoreTests.swift index 38a8692..d9fb24a 100644 --- a/Tests/VDStoreTests/VDStoreTests.swift +++ b/Tests/VDStoreTests/VDStoreTests.swift @@ -90,6 +90,20 @@ final class VDStoreTests: XCTestCase { XCTAssertEqual(count, 2) } + /// Test that the publisher property of a Store sends updates when the state changes. + func testAsyncSequenceUpdates() async { + let initialCounter = Counter(counter: 0) + let store = Store(initialCounter) + Task { + store.add() + } + for await newState in store.async { + if newState.counter == 1 { + break + } + } + } + #if swift(>=5.9) /// Test that the publisher property of a Store sends updates when the state changes. func testPublisherUpdates() async { @@ -222,9 +236,57 @@ final class VDStoreTests: XCTestCase { await fulfillment(of: [expectation], timeout: 0.1) XCTAssertEqual(updatesCount, 2) } + +// @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +// func testObservation() async { +// let store = Store(Counter()).counter +// let expectation = expectation(description: "Counter") +//// store.state += 1 +// withObservationTracking { +// store.state += 1 +// } onChange: { +// expectation.fulfill() +// } +//// withContinousObservation(of: store.state) { state in +//// print("onChange") +//// expectation.fulfill() +//// } +//// store.state += 1 +// await fulfillment(of: [expectation], timeout: 0.1) +// } #endif } +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +func withContinousObservation(of value: @escaping @autoclosure () -> T, execute: @escaping (T) -> Void) { + withObservationTracking { + execute(value()) + } onChange: { + DispatchQueue.main.async { + withContinousObservation(of: value(), execute: execute) + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +func observationTrackingStream( + of value: @escaping @autoclosure () -> T +) -> AsyncStream { + AsyncStream { continuation in + @Sendable func observe() { + let result = withObservationTracking { + value() + } onChange: { + DispatchQueue.main.async { + observe() + } + } + continuation.yield(result) + } + observe() + } +} + struct Counter: Equatable { var counter = 0