From 195284b94b799b326729640453f547f08892293a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 30 Jul 2023 14:58:40 -0700 Subject: [PATCH] The Composable Architecture 1.0 (#2318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs * wip * wip * wip * wip * wip * wip * wip * Fix invalid states count for 3 optionals and typos (#2094) * wip * wip * more dismisseffect docs * fixed some references * navigation doc corrections * more nav docs * fix cancellation tests in release mode * wrap some tests in #if DEBUG since they are testing expected failures * update UUIDs in tests to use shorter initializer * fixed a todo * wip * fix merge errors * wip * fix * wip * wip * fixing a bunch of todos * get rid of rawvalue in StackElementID * more todos * NavLinkStore docs * fix swift 5.6 stuff * fix some standups tests * fix * clean up * docs fix * fixes * wip * 5.6 fix * wip * wip * dont parallelize tests * updated demo readmes * wip * Use ObservedObject instead of StateObject for alert/dialog modifiers. * integration tests for bad dismissal behavior * check for runtime warnings in every integration test * wip * wip * wip * fix * wip * wip * wip * wip * wip * wip * Drop a bunch of Hashables. * some nav bug fixes * wip * wip * wip * fix * fix * wip * wip * Simplify recording test. * add concurrent async test * fix * wip * Refact how detail dismisses itself. * fix * 5.6 fix * wip * wip * wip * wip * Add TestStore.assert. * Revert "Add TestStore.assert." This reverts commit a892cccc66781fa78af9b284d57e1c218b7b9878. * add Ukrainian Readme.md (#2121) * Add TestStore.assert. (#2123) * Add TestStore.assert. * wip * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis * Update Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md Co-authored-by: Stephen Celis * fix tests --------- Co-authored-by: Stephen Celis * Run swift-format * push for store.finish and presentation * wip * move docs around * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Add case subscripts * wip * wip * wip * 5.7-only * wip * wip * wip * wip * fix * revert store.finish task cancellation * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * add test for presentation scope * wip * wip * wip * wip * wip * cleanup * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Rename ReducerProtocol.swift to Reducer.swift (#2206) * Hard-deprecate old SwitchStore initializers/overloads * wip * wip * Resolve CaseStudies crash (#2258) * wip * wip * wip * wip * wip * wip * wip * wip * Bump timeout for CI * wip * Remove old deprecations * Simplify test store * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * images for tutorials * wip * wip * Remove deprecated alert APIs * Bump dependencies * wip --------- Co-authored-by: Brandon Williams Co-authored-by: 유재호 Co-authored-by: Dmytro Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Co-authored-by: mbrandonw --- .../xcshareddata/swiftpm/Package.resolved | 36 +- .../CaseStudies.xcodeproj/project.pbxproj | 12 - .../SwiftUICaseStudies/00-Core.swift | 10 - .../SwiftUICaseStudies/00-RootView.swift | 20 - .../01-GettingStarted-Animations.swift | 95 +- .../01-GettingStarted-SharedState.swift | 62 +- .../02-Effects-WebSocket.swift | 158 +-- .../03-NavigationStack.swift | 6 +- ...erOrderReducers-ElmLikeSubscriptions.swift | 159 --- .../04-HigherOrderReducers-Lifecycle.swift | 180 --- .../DownloadComponent.swift | 122 +- .../ReusableComponents-Download.swift | 4 +- ...gherOrderReducers-ReusableFavoriting.swift | 12 +- .../01-GettingStarted-AnimationsTests.swift | 2 +- .../01-GettingStarted-SharedStateTests.swift | 4 +- ...4-HigherOrderReducers-LifecycleTests.swift | 47 - ...rderReducers-ReusableFavoritingTests.swift | 2 +- ...ducers-ReusableOfflineDownloadsTests.swift | 10 +- .../Integration.xcodeproj/project.pbxproj | 2 +- .../Integration/NavigationStackTestCase.swift | 2 +- .../Integration/PresentationTestCase.swift | 2 +- .../SpeechRecognition/SpeechRecognition.swift | 2 +- .../Standups.xcodeproj/project.pbxproj | 2 +- Examples/TicTacToe/tic-tac-toe/Package.swift | 2 +- .../Sources/GameCore/GameCore.swift | 2 +- .../Sources/LoginCore/LoginCore.swift | 4 +- .../Sources/TwoFactorCore/TwoFactorCore.swift | 18 +- Examples/Todos/Todos/Todos.swift | 4 +- Package.resolved | 40 +- Package.swift | 16 +- .../Articles/GettingStarted.md | 4 +- .../Articles/MigratingToTheReducerProtocol.md | 710 ----------- .../Articles/Performance.md | 4 +- .../Articles/StackBasedNavigation.md | 6 +- .../Articles/SwiftConcurrency.md | 8 +- .../Documentation.docc/Articles/Testing.md | 32 +- .../Articles/TreeBasedNavigation.md | 6 +- .../ComposableArchitecture.md | 11 +- .../Extensions/AnyReducerDeprecations.md | 7 - .../Deprecations/EffectDeprecations.md | 52 - .../Deprecations/ReduceDeprecations.md | 9 - .../Deprecations/ReducerDeprecations.md | 21 - .../Deprecations/StoreDeprecations.md | 21 - .../Deprecations/SwiftUIDeprecations.md | 19 - .../Deprecations/TestStoreDeprecations.md | 39 +- .../Deprecations/ViewStoreDeprecations.md | 28 - .../Documentation.docc/Extensions/Effect.md | 38 +- .../Extensions/EffectCancel.md | 7 - .../Extensions/EffectCancelIds.md | 7 - .../Extensions/EffectCancellable.md | 7 - .../Extensions/EffectRun.md | 2 +- .../Extensions/EffectSend.md | 4 +- .../Documentation.docc/Extensions/Reduce.md | 6 +- .../{ReducerProtocol.md => Reducer.md} | 8 +- .../Extensions/ReducerBuilder.md | 1 - ...erProtocolForEach.md => ReducerForEach.md} | 0 ...ducerProtocolIfLet.md => ReducerlIfLet.md} | 0 .../Documentation.docc/Extensions/Store.md | 7 +- .../Extensions/StoreScope.md | 2 - .../Extensions/SwitchStore.md | 1 - .../Extensions/TestStore.md | 18 +- .../Extensions/TestStoreDependencies.md | 8 + .../Extensions/TestStoreExhaustivity.md | 2 + .../Extensions/ViewStore.md | 19 +- .../Extensions/WithTaskCancellation.md | 7 - .../Extensions/WithViewStore.md | 4 +- .../Extensions/WithViewStoreInit.md | 15 +- .../Resources/01-homepage.png | Bin 41633 -> 14195 bytes .../Resources/02-homepage.png | Bin 0 -> 14055 bytes .../01-01-YourFirstFeature.tutorial | 4 +- .../01-02-AddingSideEffects.tutorial | 14 +- .../01-03-TestingYourFeature.tutorial | 4 +- .../02-02-MultipleDestinations.tutorial | 2 +- .../02-04-NavigationStacks.tutorial | 2 +- .../MeetComposableArchitecture.tutorial | 7 +- Sources/ComposableArchitecture/Effect.swift | 220 +--- .../Effects/Animation.swift | 2 +- .../Effects/Cancellation.swift | 17 +- .../Effects/EffectActions.swift | 2 +- .../Effects/Publisher.swift | 471 +------ .../Effects/Publisher/Debouncing.swift | 74 -- .../Effects/Publisher/Deferring.swift | 41 - .../Effects/Publisher/Throttling.swift | 95 -- .../Effects/Publisher/Timer.swift | 136 -- .../Effects/TaskResult.swift | 6 +- .../Internal/Create.swift | 30 +- .../Internal/Deprecations.swift | 1088 +--------------- Sources/ComposableArchitecture/Reducer.swift | 39 +- .../Reducer/AnyReducer/AnyReducer.swift | 931 -------------- .../AnyReducer/AnyReducerBinding.swift | 48 - .../AnyReducer/AnyReducerCompatibility.swift | 70 -- .../Reducer/AnyReducer/AnyReducerDebug.swift | 210 ---- .../AnyReducer/AnyReducerSignpost.swift | 61 - .../Reducer/ReducerBuilder.swift | 4 +- .../Reducer/Reducers/BindingReducer.swift | 4 +- .../Reducer/Reducers/OnChange.swift | 2 +- .../Reducers/PresentationReducer.swift | 9 +- .../Reducer/Reducers/Scope.swift | 3 +- .../Reducer/Reducers/SignpostReducer.swift | 2 +- .../Reducer/Reducers/StackReducer.swift | 6 +- Sources/ComposableArchitecture/Store.swift | 20 +- .../SwiftUI/Alert.swift | 79 -- .../SwiftUI/Binding.swift | 77 +- .../SwiftUI/ConfirmationDialog.swift | 94 -- .../SwiftUI/SwitchStore.swift | 1113 ----------------- .../SwiftUI/WithViewStore.swift | 110 +- .../ComposableArchitecture/TestStore.swift | 807 +++--------- .../ComposableArchitecture/ViewStore.swift | 76 -- .../Effects.swift | 3 +- .../DeprecatedTests.swift | 34 - .../EffectCancellationTests.swift | 69 +- .../EffectDebounceTests.swift | 87 -- .../EffectDeferredTests.swift | 84 -- .../EffectFailureTests.swift | 24 - .../EffectOperationTests.swift | 28 +- .../EffectPublisherTests.swift | 25 - .../EffectTaskTests.swift | 125 -- .../EffectTests.swift | 194 +-- .../EffectThrottleTests.swift | 218 ---- .../ReducerTests.swift | 4 +- .../Reducers/ForEachReducerTests.swift | 2 +- .../Reducers/IfCaseLetReducerTests.swift | 2 +- .../Reducers/PresentationReducerTests.swift | 24 +- .../Reducers/StackReducerTests.swift | 60 +- .../RuntimeWarningTests.swift | 8 +- .../StoreTests.swift | 30 +- .../TestStoreNonExhaustiveTests.swift | 8 +- .../TestStoreTests.swift | 4 +- .../TimerTests.swift | 131 -- .../ViewStoreTests.swift | 12 +- 130 files changed, 859 insertions(+), 8474 deletions(-) delete mode 100644 Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ElmLikeSubscriptions.swift delete mode 100644 Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Lifecycle.swift delete mode 100644 Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Articles/MigratingToTheReducerProtocol.md delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/AnyReducerDeprecations.md delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/EffectDeprecations.md delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ReduceDeprecations.md delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ReducerDeprecations.md delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/StoreDeprecations.md delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ViewStoreDeprecations.md delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancel.md delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancelIds.md delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancellable.md rename Sources/ComposableArchitecture/Documentation.docc/Extensions/{ReducerProtocol.md => Reducer.md} (85%) rename Sources/ComposableArchitecture/Documentation.docc/Extensions/{ReducerProtocolForEach.md => ReducerForEach.md} (100%) rename Sources/ComposableArchitecture/Documentation.docc/Extensions/{ReducerProtocolIfLet.md => ReducerlIfLet.md} (100%) create mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStoreDependencies.md delete mode 100644 Sources/ComposableArchitecture/Documentation.docc/Extensions/WithTaskCancellation.md create mode 100644 Sources/ComposableArchitecture/Documentation.docc/Resources/02-homepage.png delete mode 100644 Sources/ComposableArchitecture/Effects/Publisher/Debouncing.swift delete mode 100644 Sources/ComposableArchitecture/Effects/Publisher/Deferring.swift delete mode 100644 Sources/ComposableArchitecture/Effects/Publisher/Throttling.swift delete mode 100644 Sources/ComposableArchitecture/Effects/Publisher/Timer.swift delete mode 100644 Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducer.swift delete mode 100644 Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerBinding.swift delete mode 100644 Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerCompatibility.swift delete mode 100644 Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerDebug.swift delete mode 100644 Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerSignpost.swift delete mode 100644 Tests/ComposableArchitectureTests/DeprecatedTests.swift delete mode 100644 Tests/ComposableArchitectureTests/EffectDebounceTests.swift delete mode 100644 Tests/ComposableArchitectureTests/EffectDeferredTests.swift delete mode 100644 Tests/ComposableArchitectureTests/EffectPublisherTests.swift delete mode 100644 Tests/ComposableArchitectureTests/EffectTaskTests.swift delete mode 100644 Tests/ComposableArchitectureTests/EffectThrottleTests.swift delete mode 100644 Tests/ComposableArchitectureTests/TimerTests.swift diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5a1cbf430e43..6b0a614d5009 100644 --- a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "ec62f32d21584214a4b27c8cee2b2ad70ab2c38a", - "version" : "0.11.0" + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", - "version" : "0.14.1" + "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81", + "version" : "1.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "0fbaebfc013715dab44d715a4d350ba37f297e4d", - "version" : "0.4.0" + "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", + "version" : "1.0.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "479750bd98fac2e813fffcf2af0728b5b0085795", - "version" : "0.1.1" + "revision" : "ea631ce892687f5432a833312292b80db238186a", + "version" : "1.0.0" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "4a87bb75be70c983a9548597e8783236feb3401e", - "version" : "0.11.1" + "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", + "version" : "1.0.0" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "16fd42ae04c6e7f74a6a86395d04722c641cccee", - "version" : "0.6.0" + "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", + "version" : "1.0.0" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections", "state" : { - "revision" : "d01446a78fb768adc9a78cbb6df07767c8ccfc29", - "version" : "0.8.0" + "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", + "version" : "1.0.0" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation", "state" : { - "revision" : "2aa885e719087ee19df251c08a5980ad3e787f12", - "version" : "0.8.0" + "revision" : "f5bcdac5b6bb3f826916b14705f37a3937c2fd34", + "version" : "1.0.0" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865", - "version" : "0.9.0" + "revision" : "302891700c7fa3b92ebde9fe7b42933f8349f3c7", + "version" : "1.0.0" } } ], diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj index 0897289dd2b1..fbbae558e4fe 100644 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj @@ -9,12 +9,10 @@ /* Begin PBXBuildFile section */ 433B8B762A49C9AF0035DEE4 /* 03-Navigation-Multiple-Destinations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433B8B752A49C9AF0035DEE4 /* 03-Navigation-Multiple-Destinations.swift */; }; 4F5AC11F24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F5AC11E24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift */; }; - CA0C0C4724B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0C0C4624B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift */; }; CA0C51FB245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */; }; CA25E5D224463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */; }; CA34170824A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */; }; CA3E421F26B8337500581ABC /* 01-GettingStarted-FocusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA3E421E26B8337500581ABC /* 01-GettingStarted-FocusState.swift */; }; - CA3E4C5B24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA3E4C5A24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift */; }; CA410EE0247A15FE00E41798 /* 02-Effects-WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */; }; CA410EE2247C73B400E41798 /* 02-Effects-WebSocketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */; }; CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift */; }; @@ -72,7 +70,6 @@ DC89C45324465452006900B9 /* 03-Navigation-Lists-NavigateAndLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C45224465451006900B9 /* 03-Navigation-Lists-NavigateAndLoad.swift */; }; DC89C45524465C44006900B9 /* 02-Effects-Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C45424465C44006900B9 /* 02-Effects-Timers.swift */; }; DC9EB4172450CBD2005F413B /* UIViewRepresented.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9EB4162450CBD2005F413B /* UIViewRepresented.swift */; }; - DCAC2A4F2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC2A4E2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift */; }; DCC68EAB244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC68EAA244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift */; }; DCC68EDD2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC68EDC2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift */; }; DCC68EDF2447BC810037F998 /* TemplateText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC68EDE2447BC810037F998 /* TemplateText.swift */; }; @@ -153,12 +150,10 @@ /* Begin PBXFileReference section */ 433B8B752A49C9AF0035DEE4 /* 03-Navigation-Multiple-Destinations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-Multiple-Destinations.swift"; sourceTree = ""; }; 4F5AC11E24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-SharedStateTests.swift"; sourceTree = ""; }; - CA0C0C4624B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-LifecycleTests.swift"; sourceTree = ""; }; CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift"; sourceTree = ""; }; CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Bindings-Basics.swift"; sourceTree = ""; }; CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AnimationsTests.swift"; sourceTree = ""; }; CA3E421E26B8337500581ABC /* 01-GettingStarted-FocusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-FocusState.swift"; sourceTree = ""; }; - CA3E4C5A24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-Lifecycle.swift"; sourceTree = ""; }; CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocket.swift"; sourceTree = ""; }; CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocketTests.swift"; sourceTree = ""; }; CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndConfirmationDialogsTests.swift"; sourceTree = ""; }; @@ -225,7 +220,6 @@ DC89C45224465451006900B9 /* 03-Navigation-Lists-NavigateAndLoad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-Lists-NavigateAndLoad.swift"; sourceTree = ""; }; DC89C45424465C44006900B9 /* 02-Effects-Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-Timers.swift"; sourceTree = ""; }; DC9EB4162450CBD2005F413B /* UIViewRepresented.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewRepresented.swift; sourceTree = ""; }; - DCAC2A4E2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ElmLikeSubscriptions.swift"; sourceTree = ""; }; DCC68EAA244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-Sheet-PresentAndLoad.swift"; sourceTree = ""; }; DCC68EDC2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-OptionalState.swift"; sourceTree = ""; }; DCC68EDE2447BC810037F998 /* TemplateText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateText.swift; sourceTree = ""; }; @@ -416,8 +410,6 @@ DCC68EAA244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift */, 433B8B752A49C9AF0035DEE4 /* 03-Navigation-Multiple-Destinations.swift */, DC3C87AF29A48C4D004D9104 /* 03-NavigationStack.swift */, - DCAC2A4E2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift */, - CA3E4C5A24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift */, DCE63B70245CC0B90080A23D /* 04-HigherOrderReducers-Recursion.swift */, DCC68EE22447C8540037F998 /* 04-HigherOrderReducers-ReusableFavoriting.swift */, DCFE195F278DBF0600C14CCF /* CaseStudiesApp.swift */, @@ -442,7 +434,6 @@ CABC4F3A26AEE20200D5FA2C /* 02-Effects-RefreshableTests.swift */, DC07231624465D1E003A8B65 /* 02-Effects-TimersTests.swift */, CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */, - CA0C0C4624B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift */, CA78F0CC28DA47D70026C4AD /* 04-HigherOrderReducers-RecursionTests.swift */, DC634B242448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift */, CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */, @@ -750,7 +741,6 @@ DCC68EE32447C8540037F998 /* 04-HigherOrderReducers-ReusableFavoriting.swift in Sources */, CA3E421F26B8337500581ABC /* 01-GettingStarted-FocusState.swift in Sources */, DCC68EDF2447BC810037F998 /* TemplateText.swift in Sources */, - DCAC2A4F2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift in Sources */, CA6AC2672451135C00C71CB3 /* DownloadClient.swift in Sources */, CAA9ADC624465C810003A984 /* 02-Effects-Cancellation.swift in Sources */, CA5ECF92267A79F0002067FF /* FactClient.swift in Sources */, @@ -760,7 +750,6 @@ DCFE1960278DBF0600C14CCF /* CaseStudiesApp.swift in Sources */, DCD442C6286CA91F008B4EA7 /* AboutView.swift in Sources */, CA6AC2662451135C00C71CB3 /* DownloadComponent.swift in Sources */, - CA3E4C5B24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift in Sources */, DC5B505125C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift in Sources */, CAA9ADC22446587C0003A984 /* 02-Effects-Basics.swift in Sources */, DC89C41B24460F95006900B9 /* 00-RootView.swift in Sources */, @@ -793,7 +782,6 @@ CA410EE2247C73B400E41798 /* 02-Effects-WebSocketTests.swift in Sources */, CABC4F3B26AEE20200D5FA2C /* 02-Effects-RefreshableTests.swift in Sources */, CA34170824A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift in Sources */, - CA0C0C4724B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift in Sources */, DC07231724465D1E003A8B65 /* 02-Effects-TimersTests.swift in Sources */, CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift in Sources */, CAA9ADC424465AB00003A984 /* 02-Effects-BasicsTests.swift in Sources */, diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift index 063399d31ee3..2ba85df63bc3 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift @@ -6,13 +6,11 @@ struct Root: Reducer { var animation = Animations.State() var bindingBasics = BindingBasics.State() var bindingForm = BindingForm.State() - var clock = ClockState() var counter = Counter.State() var effectsBasics = EffectsBasics.State() var effectsCancellation = EffectsCancellation.State() var episodes = Episodes.State(episodes: .mocks) var focusDemo = FocusDemo.State() - var lifecycle = LifecycleDemo.State() var loadThenPresent = LoadThenPresent.State() var longLivingEffects = LongLivingEffects.State() var map = MapApp.State(cityMaps: .mocks) @@ -35,13 +33,11 @@ struct Root: Reducer { case animation(Animations.Action) case bindingBasics(BindingBasics.Action) case bindingForm(BindingForm.Action) - case clock(ClockAction) case counter(Counter.Action) case effectsBasics(EffectsBasics.Action) case effectsCancellation(EffectsCancellation.Action) case episodes(Episodes.Action) case focusDemo(FocusDemo.Action) - case lifecycle(LifecycleDemo.Action) case loadThenPresent(LoadThenPresent.Action) case longLivingEffects(LongLivingEffects.Action) case map(MapApp.Action) @@ -86,9 +82,6 @@ struct Root: Reducer { Scope(state: \.bindingForm, action: /Action.bindingForm) { BindingForm() } - Scope(state: \.clock, action: /Action.clock) { - Reduce(clockReducer, environment: ClockEnvironment(clock: self.clock)) - } Scope(state: \.counter, action: /Action.counter) { Counter() } @@ -104,9 +97,6 @@ struct Root: Reducer { Scope(state: \.focusDemo, action: /Action.focusDemo) { FocusDemo() } - Scope(state: \.lifecycle, action: /Action.lifecycle) { - LifecycleDemo() - } Scope(state: \.loadThenPresent, action: /Action.loadThenPresent) { LoadThenPresent() } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift index c622386beda6..be6e5bde12b7 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift @@ -243,26 +243,6 @@ struct RootView: View { ) ) - NavigationLink( - "Lifecycle", - destination: LifecycleDemoView( - store: self.store.scope( - state: \.lifecycle, - action: Root.Action.lifecycle - ) - ) - ) - - NavigationLink( - "Elm-like subscriptions", - destination: ClockView( - store: self.store.scope( - state: \.clock, - action: Root.Action.clock - ) - ) - ) - NavigationLink( "Recursive state and actions", destination: NestedView( diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift index d8cda504a6b0..16012b635481 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift @@ -22,73 +22,78 @@ private let readMe = """ struct Animations: Reducer { struct State: Equatable { - var alert: AlertState? + @PresentationState var alert: AlertState? var circleCenter: CGPoint? var circleColor = Color.black var isCircleScaled = false } enum Action: Equatable, Sendable { - case alertDismissed + case alert(PresentationAction) case circleScaleToggleChanged(Bool) case rainbowButtonTapped case resetButtonTapped - case resetConfirmationButtonTapped case setColor(Color) case tapped(CGPoint) + + enum Alert: Equatable, Sendable { + case resetConfirmationButtonTapped + } } @Dependency(\.continuousClock) var clock - func reduce(into state: inout State, action: Action) -> Effect { - enum CancelID { case rainbow } + private enum CancelID { case rainbow } - switch action { - case .alertDismissed: - state.alert = nil - return .none + var body: some Reducer { + Reduce { state, action in + switch action { + case .alert(.presented(.resetConfirmationButtonTapped)): + state = State() + return .cancel(id: CancelID.rainbow) - case let .circleScaleToggleChanged(isScaled): - state.isCircleScaled = isScaled - return .none + case .alert: + return .none - case .rainbowButtonTapped: - return .run { send in - for color in [Color.red, .blue, .green, .orange, .pink, .purple, .yellow, .black] { - await send(.setColor(color), animation: .linear) - try await self.clock.sleep(for: .seconds(1)) - } - } - .cancellable(id: CancelID.rainbow) - - case .resetButtonTapped: - state.alert = AlertState { - TextState("Reset state?") - } actions: { - ButtonState( - role: .destructive, - action: .send(.resetConfirmationButtonTapped, animation: .default) - ) { - TextState("Reset") + case let .circleScaleToggleChanged(isScaled): + state.isCircleScaled = isScaled + return .none + + case .rainbowButtonTapped: + return .run { send in + for color in [Color.red, .blue, .green, .orange, .pink, .purple, .yellow, .black] { + await send(.setColor(color), animation: .linear) + try await self.clock.sleep(for: .seconds(1)) + } } - ButtonState(role: .cancel) { - TextState("Cancel") + .cancellable(id: CancelID.rainbow) + + case .resetButtonTapped: + state.alert = AlertState { + TextState("Reset state?") + } actions: { + ButtonState( + role: .destructive, + action: .send(.resetConfirmationButtonTapped, animation: .default) + ) { + TextState("Reset") + } + ButtonState(role: .cancel) { + TextState("Cancel") + } } - } - return .none + return .none - case .resetConfirmationButtonTapped: - state = State() - return .cancel(id: CancelID.rainbow) + case let .setColor(color): + state.circleColor = color + return .none - case let .setColor(color): - state.circleColor = color - return .none - - case let .tapped(point): - state.circleCenter = point - return .none + case let .tapped(point): + state.circleCenter = point + return .none + } } + .ifLet(\.$alert, action: /Action.alert) } } @@ -139,7 +144,7 @@ struct AnimationsView: View { Button("Reset") { viewStore.send(.resetButtonTapped) } .padding([.horizontal, .bottom]) } - .alert(self.store.scope(state: \.alert, action: { $0 }), dismiss: .alertDismissed) + .alert(store: self.store.scope(state: \.$alert, action: { .alert($0) })) .navigationBarTitleDisplayMode(.inline) } } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift index aec8c197c7fb..9f202a098077 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift @@ -74,7 +74,7 @@ struct SharedState: Reducer { struct Counter: Reducer { struct State: Equatable { - var alert: AlertState? + @PresentationState var alert: AlertState? var count = 0 var maxCount = 0 var minCount = 0 @@ -82,40 +82,44 @@ struct SharedState: Reducer { } enum Action: Equatable { - case alertDismissed + case alert(PresentationAction) case decrementButtonTapped case incrementButtonTapped case isPrimeButtonTapped - } - - func reduce(into state: inout State, action: Action) -> Effect { - switch action { - case .alertDismissed: - state.alert = nil - return .none - case .decrementButtonTapped: - state.count -= 1 - state.numberOfCounts += 1 - state.minCount = min(state.minCount, state.count) - return .none - - case .incrementButtonTapped: - state.count += 1 - state.numberOfCounts += 1 - state.maxCount = max(state.maxCount, state.count) - return .none + enum Alert: Equatable {} + } - case .isPrimeButtonTapped: - state.alert = AlertState { - TextState( - isPrime(state.count) - ? "👍 The number \(state.count) is prime!" - : "👎 The number \(state.count) is not prime :(" - ) + var body: some Reducer { + Reduce { state, action in + switch action { + case .alert: + return .none + + case .decrementButtonTapped: + state.count -= 1 + state.numberOfCounts += 1 + state.minCount = min(state.minCount, state.count) + return .none + + case .incrementButtonTapped: + state.count += 1 + state.numberOfCounts += 1 + state.maxCount = max(state.maxCount, state.count) + return .none + + case .isPrimeButtonTapped: + state.alert = AlertState { + TextState( + isPrime(state.count) + ? "👍 The number \(state.count) is prime!" + : "👎 The number \(state.count) is not prime :(" + ) + } + return .none } - return .none } + .ifLet(\.$alert, action: /Action.alert) } } @@ -218,7 +222,7 @@ struct SharedStateCounterView: View { } .padding(.top) .navigationTitle("Shared State Demo") - .alert(self.store.scope(state: \.alert, action: { $0 }), dismiss: .alertDismissed) + .alert(store: self.store.scope(state: \.$alert, action: { .alert($0) })) } } } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift index e073c08e2e48..c7eaf11f1c7a 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift @@ -14,7 +14,7 @@ private let readMe = """ struct WebSocket: Reducer { struct State: Equatable { - var alert: AlertState? + @PresentationState var alert: AlertState? var connectivityState = ConnectivityState.disconnected var messageToSend = "" var receivedMessages: [String] = [] @@ -27,106 +27,110 @@ struct WebSocket: Reducer { } enum Action: Equatable { - case alertDismissed + case alert(PresentationAction) case connectButtonTapped case messageToSendChanged(String) case receivedSocketMessage(TaskResult) case sendButtonTapped case sendResponse(didSucceed: Bool) case webSocket(WebSocketClient.Action) + + enum Alert: Equatable {} } @Dependency(\.continuousClock) var clock @Dependency(\.webSocket) var webSocket - func reduce(into state: inout State, action: Action) -> Effect { - switch action { - case .alertDismissed: - state.alert = nil - return .none - - case .connectButtonTapped: - switch state.connectivityState { - case .connected, .connecting: - state.connectivityState = .disconnected - return .cancel(id: WebSocketClient.ID()) - - case .disconnected: - state.connectivityState = .connecting - return .run { send in - let actions = await self.webSocket - .open(WebSocketClient.ID(), URL(string: "wss://echo.websocket.events")!, []) - await withThrowingTaskGroup(of: Void.self) { group in - for await action in actions { - // NB: Can't call `await send` here outside of `group.addTask` due to task local - // dependency mutation in `Effect.{task,run}`. Can maybe remove that explicit task - // local mutation (and this `addTask`?) in a world with - // `Effect(operation: .run { ... })`? - group.addTask { await send(.webSocket(action)) } - switch action { - case .didOpen: - group.addTask { - while !Task.isCancelled { - try await self.clock.sleep(for: .seconds(10)) - try? await self.webSocket.sendPing(WebSocketClient.ID()) + var body: some Reducer { + Reduce { state, action in + switch action { + case .alert: + return .none + + case .connectButtonTapped: + switch state.connectivityState { + case .connected, .connecting: + state.connectivityState = .disconnected + return .cancel(id: WebSocketClient.ID()) + + case .disconnected: + state.connectivityState = .connecting + return .run { send in + let actions = await self.webSocket + .open(WebSocketClient.ID(), URL(string: "wss://echo.websocket.events")!, []) + await withThrowingTaskGroup(of: Void.self) { group in + for await action in actions { + // NB: Can't call `await send` here outside of `group.addTask` due to task local + // dependency mutation in `Effect.{task,run}`. Can maybe remove that explicit task + // local mutation (and this `addTask`?) in a world with + // `Effect(operation: .run { ... })`? + group.addTask { await send(.webSocket(action)) } + switch action { + case .didOpen: + group.addTask { + while !Task.isCancelled { + try await self.clock.sleep(for: .seconds(10)) + try? await self.webSocket.sendPing(WebSocketClient.ID()) + } } - } - group.addTask { - for await result in try await self.webSocket.receive(WebSocketClient.ID()) { - await send(.receivedSocketMessage(result)) + group.addTask { + for await result in try await self.webSocket.receive(WebSocketClient.ID()) { + await send(.receivedSocketMessage(result)) + } } + case .didClose: + return } - case .didClose: - return } } } + .cancellable(id: WebSocketClient.ID()) } - .cancellable(id: WebSocketClient.ID()) - } - case let .messageToSendChanged(message): - state.messageToSend = message - return .none + case let .messageToSendChanged(message): + state.messageToSend = message + return .none - case let .receivedSocketMessage(.success(message)): - if case let .string(string) = message { - state.receivedMessages.append(string) - } - return .none - - case .receivedSocketMessage(.failure): - return .none - - case .sendButtonTapped: - let messageToSend = state.messageToSend - state.messageToSend = "" - return .run { send in - try await self.webSocket.send(WebSocketClient.ID(), .string(messageToSend)) - await send(.sendResponse(didSucceed: true)) - } catch: { _, send in - await send(.sendResponse(didSucceed: false)) - } - .cancellable(id: WebSocketClient.ID()) + case let .receivedSocketMessage(.success(message)): + if case let .string(string) = message { + state.receivedMessages.append(string) + } + return .none - case .sendResponse(didSucceed: false): - state.alert = AlertState { - TextState("Could not send socket message. Connect to the server first, and try again.") - } - return .none + case .receivedSocketMessage(.failure): + return .none - case .sendResponse(didSucceed: true): - return .none + case .sendButtonTapped: + let messageToSend = state.messageToSend + state.messageToSend = "" + return .run { send in + try await self.webSocket.send(WebSocketClient.ID(), .string(messageToSend)) + await send(.sendResponse(didSucceed: true)) + } catch: { _, send in + await send(.sendResponse(didSucceed: false)) + } + .cancellable(id: WebSocketClient.ID()) - case .webSocket(.didClose): - state.connectivityState = .disconnected - return .cancel(id: WebSocketClient.ID()) + case .sendResponse(didSucceed: false): + state.alert = AlertState { + TextState("Could not send socket message. Connect to the server first, and try again.") + } + return .none - case .webSocket(.didOpen): - state.connectivityState = .connected - state.receivedMessages.removeAll() - return .none + case .sendResponse(didSucceed: true): + return .none + + case .webSocket(.didClose): + state.connectivityState = .disconnected + return .cancel(id: WebSocketClient.ID()) + + case .webSocket(.didOpen): + state.connectivityState = .connected + state.receivedMessages.removeAll() + return .none + } } + .ifLet(\.$alert, action: /Action.alert) } } @@ -180,7 +184,7 @@ struct WebSocketView: View { Text("Received messages") } } - .alert(self.store.scope(state: \.alert, action: { $0 }), dismiss: .alertDismissed) + .alert(store: self.store.scope(state: \.$alert, action: { .alert($0) })) .navigationTitle("Web Socket") } } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/03-NavigationStack.swift b/Examples/CaseStudies/SwiftUICaseStudies/03-NavigationStack.swift index 1706c0655eb3..24288cb65ae4 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/03-NavigationStack.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/03-NavigationStack.swift @@ -235,7 +235,7 @@ struct ScreenA: Reducer { return .none case .dismissButtonTapped: - return .fireAndForget { + return .run { _ in await self.dismiss() } @@ -245,8 +245,8 @@ struct ScreenA: Reducer { case .factButtonTapped: state.isLoading = true - return .task { [count = state.count] in - await .factResponse(.init { try await self.factClient.fetch(count) }) + return .run { [count = state.count] send in + await send(.factResponse(.init { try await self.factClient.fetch(count) })) } case let .factResponse(.success(fact)): diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ElmLikeSubscriptions.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ElmLikeSubscriptions.swift deleted file mode 100644 index 2e37f959dd28..000000000000 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ElmLikeSubscriptions.swift +++ /dev/null @@ -1,159 +0,0 @@ -import ComposableArchitecture -import SwiftUI - -private let readMe = """ - This screen demonstrates how the `AnyReducer` struct can be extended to enhance reducers with \ - extra functionality. - - In this example we introduce a declarative interface for describing long-running effects, \ - inspired by Elm's `subscriptions` API. - """ - -@available(*, deprecated) -extension AnyReducer { - static func subscriptions( - _ subscriptions: @escaping (State, Environment) -> [AnyHashable: Effect] - ) -> Self { - var activeSubscriptions: [AnyHashable: Effect] = [:] - - return AnyReducer { state, _, environment in - let currentSubscriptions = subscriptions(state, environment) - defer { activeSubscriptions = currentSubscriptions } - return .merge( - Set(activeSubscriptions.keys).union(currentSubscriptions.keys).map { id in - switch (activeSubscriptions[id], currentSubscriptions[id]) { - case (.some, .none): - return .cancel(id: id) - case let (.none, .some(effect)): - return effect.cancellable(id: id) - default: - return .none - } - } - ) - } - } -} - -// MARK: - Feature domain - -struct ClockState: Equatable { - var isTimerActive = false - var secondsElapsed = 0 -} - -enum ClockAction: Equatable { - case timerTicked - case toggleTimerButtonTapped -} - -struct ClockEnvironment { - var clock: any Clock -} - -@available(*, deprecated) -let clockReducer = AnyReducer.combine( - AnyReducer { state, action, environment in - switch action { - case .timerTicked: - state.secondsElapsed += 1 - return .none - case .toggleTimerButtonTapped: - state.isTimerActive.toggle() - return .none - } - }, - .subscriptions { state, environment in - guard state.isTimerActive else { return [:] } - struct TimerID: Hashable {} - return [ - TimerID(): .run { send in - for await _ in environment.clock.timer(interval: .seconds(1)) { - await send(.timerTicked, animation: .interpolatingSpring(stiffness: 3000, damping: 40)) - } - } - ] - } -) - -// MARK: - Feature view - -struct ClockView: View { - let store: Store - - var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - Form { - AboutView(readMe: readMe) - - ZStack { - Circle() - .fill( - AngularGradient( - gradient: Gradient( - colors: [ - .blue.opacity(0.3), - .blue, - .blue, - .green, - .green, - .yellow, - .yellow, - .red, - .red, - .purple, - .purple, - .purple.opacity(0.3), - ] - ), - center: .center - ) - ) - .rotationEffect(.degrees(-90)) - GeometryReader { proxy in - Path { path in - path.move(to: CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2)) - path.addLine(to: CGPoint(x: proxy.size.width / 2, y: 0)) - } - .stroke(.primary, lineWidth: 3) - .rotationEffect(.degrees(Double(viewStore.secondsElapsed) * 360 / 60)) - } - } - .aspectRatio(1, contentMode: .fit) - .frame(maxWidth: 280) - .frame(maxWidth: .infinity) - .padding(.vertical, 16) - - Button { - viewStore.send(.toggleTimerButtonTapped) - } label: { - Text(viewStore.isTimerActive ? "Stop" : "Start") - .padding(8) - } - .frame(maxWidth: .infinity) - .tint(viewStore.isTimerActive ? Color.red : .accentColor) - .buttonStyle(.borderedProminent) - } - .navigationTitle("Elm-like subscriptions") - } - } -} - -// MARK: - SwiftUI previews - -@available(*, deprecated) -struct Subscriptions_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - ClockView( - store: Store( - initialState: ClockState(), - reducer: clockReducer, - environment: ClockEnvironment( - clock: ContinuousClock() - ) - ) - ) - } - } -} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Lifecycle.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Lifecycle.swift deleted file mode 100644 index 52e15d776218..000000000000 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Lifecycle.swift +++ /dev/null @@ -1,180 +0,0 @@ -import ComposableArchitecture -import SwiftUI - -private let readMe = """ - This demonstrates how to trigger effects when a view appears, and cancel effects when a view \ - disappears. This can be helpful for starting up a feature's long living effects, such as timers, \ - location managers, etc. when that feature is first presented. - - To accomplish this we define a higher-order reducer that enhances any reducer with two additional \ - actions, `.onAppear` and `.onDisappear`, and a way to automate running effects when those actions \ - are sent to the store. - """ - -struct LifecycleReducer: Reducer { - enum Action { - case onAppear - case onDisappear - case wrapped(Wrapped.Action) - } - - let wrapped: Wrapped - let onAppear: Effect - let onDisappear: Effect - - var body: some Reducer { - Reduce { state, lifecycleAction in - switch lifecycleAction { - case .onAppear: - return onAppear.map(Action.wrapped) - - case .onDisappear: - return onDisappear.fireAndForget() - - case .wrapped: - return .none - } - } - .ifLet(\.self, action: /Action.wrapped) { - self.wrapped - } - } -} - -extension LifecycleReducer.Action: Equatable where Wrapped.Action: Equatable {} - -extension Reducer { - func lifecycle( - onAppear: Effect, - onDisappear: Effect = .none - ) -> LifecycleReducer { - LifecycleReducer(wrapped: self, onAppear: onAppear, onDisappear: onDisappear) - } -} - -// MARK: - Feature domain - -struct LifecycleDemo: Reducer { - struct State: Equatable { - var count: Int? - } - - enum Action: Equatable { - case timer(LifecycleReducer.Action) - case toggleTimerButtonTapped - } - - @Dependency(\.continuousClock) var clock - private enum CancelID { case lifecycle } - - var body: some Reducer { - Reduce { state, action in - switch action { - case .timer: - return .none - - case .toggleTimerButtonTapped: - state.count = state.count == nil ? 0 : nil - return .none - } - } - - Scope(state: \.count, action: /Action.timer) { - Timer() - .lifecycle( - onAppear: .run { send in - for await _ in self.clock.timer(interval: .seconds(1)) { - await send(.tick) - } - } - .cancellable(id: CancelID.lifecycle), - onDisappear: .cancel(id: CancelID.lifecycle) - ) - } - } -} - -// MARK: - Feature view - -struct LifecycleDemoView: View { - let store: StoreOf - - var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - Form { - Section { - AboutView(readMe: readMe) - } - - Button("Toggle Timer") { viewStore.send(.toggleTimerButtonTapped) } - - IfLetStore(self.store.scope(state: \.count, action: LifecycleDemo.Action.timer)) { - TimerView(store: $0) - } - } - } - .navigationTitle("Lifecycle") - } -} - -struct Timer: Reducer { - typealias State = Int - - enum Action { - case decrementButtonTapped - case incrementButtonTapped - case tick - } - - var body: some Reducer { - Reduce { state, action in - switch action { - case .decrementButtonTapped: - state -= 1 - return .none - - case .incrementButtonTapped: - state += 1 - return .none - - case .tick: - state += 1 - return .none - } - } - } -} - -private struct TimerView: View { - let store: Store.Action> - - var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - Section { - Text("Count: \(viewStore.state)") - .onAppear { viewStore.send(.onAppear) } - .onDisappear { viewStore.send(.onDisappear) } - - Button("Decrement") { viewStore.send(.wrapped(.decrementButtonTapped)) } - - Button("Increment") { viewStore.send(.wrapped(.incrementButtonTapped)) } - } - } - } -} - -// MARK: - SwiftUI previews - -struct Lifecycle_Previews: PreviewProvider { - static var previews: some View { - Group { - NavigationView { - LifecycleDemoView( - store: Store(initialState: LifecycleDemo.State()) { - LifecycleDemo() - } - ) - } - } - } -} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index 42a15556c967..3c52bd0e5383 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -3,88 +3,87 @@ import SwiftUI struct DownloadComponent: Reducer { struct State: Equatable { - var alert: AlertState? + @PresentationState var alert: AlertState? let id: AnyHashable var mode: Mode let url: URL } enum Action: Equatable { - case alert(AlertAction) + case alert(PresentationAction) case buttonTapped case downloadClient(TaskResult) - } - enum AlertAction: Equatable { - case deleteButtonTapped - case dismissed - case nevermindButtonTapped - case stopButtonTapped + enum Alert: Equatable { + case deleteButtonTapped + case stopButtonTapped + } } @Dependency(\.downloadClient) var downloadClient - func reduce(into state: inout State, action: Action) -> Effect { - switch action { - case .alert(.deleteButtonTapped): - state.alert = nil - state.mode = .notDownloaded - return .none - - case .alert(.nevermindButtonTapped), - .alert(.dismissed): - state.alert = nil - return .none - - case .alert(.stopButtonTapped): - state.mode = .notDownloaded - state.alert = nil - return .cancel(id: state.id) - - case .buttonTapped: - switch state.mode { - case .downloaded: - state.alert = deleteAlert + var body: some Reducer { + Reduce { state, action in + switch action { + case .alert(.presented(.deleteButtonTapped)): + state.alert = nil + state.mode = .notDownloaded return .none - case .downloading: - state.alert = stopAlert - return .none + case .alert(.presented(.stopButtonTapped)): + state.mode = .notDownloaded + state.alert = nil + return .cancel(id: state.id) - case .notDownloaded: - state.mode = .startingToDownload + case .alert: + return .none - return .run { [url = state.url] send in - for try await event in self.downloadClient.download(url) { - await send(.downloadClient(.success(event)), animation: .default) + case .buttonTapped: + switch state.mode { + case .downloaded: + state.alert = deleteAlert + return .none + + case .downloading: + state.alert = stopAlert + return .none + + case .notDownloaded: + state.mode = .startingToDownload + + return .run { [url = state.url] send in + for try await event in self.downloadClient.download(url) { + await send(.downloadClient(.success(event)), animation: .default) + } + } catch: { error, send in + await send(.downloadClient(.failure(error)), animation: .default) } - } catch: { error, send in - await send(.downloadClient(.failure(error)), animation: .default) + .cancellable(id: state.id) + + case .startingToDownload: + state.alert = stopAlert + return .none } - .cancellable(id: state.id) - case .startingToDownload: - state.alert = stopAlert + case .downloadClient(.success(.response)): + state.mode = .downloaded + state.alert = nil return .none - } - - case .downloadClient(.success(.response)): - state.mode = .downloaded - state.alert = nil - return .none - case let .downloadClient(.success(.updateProgress(progress))): - state.mode = .downloading(progress: progress) - return .none + case let .downloadClient(.success(.updateProgress(progress))): + state.mode = .downloading(progress: progress) + return .none - case .downloadClient(.failure): - state.mode = .notDownloaded - state.alert = nil - return .none + case .downloadClient(.failure): + state.mode = .notDownloaded + state.alert = nil + return .none + } } + .ifLet(\.$alert, action: /Action.alert) } - private var deleteAlert: AlertState { + private var deleteAlert: AlertState { AlertState { TextState("Do you want to delete this map from your offline storage?") } actions: { @@ -95,7 +94,7 @@ struct DownloadComponent: Reducer { } } - private var stopAlert: AlertState { + private var stopAlert: AlertState { AlertState { TextState("Do you want to stop downloading this map?") } actions: { @@ -106,8 +105,8 @@ struct DownloadComponent: Reducer { } } - private var nevermindButton: ButtonState { - ButtonState(role: .cancel, action: .nevermindButtonTapped) { + private var nevermindButton: ButtonState { + ButtonState(role: .cancel) { TextState("Nevermind") } } @@ -163,10 +162,7 @@ struct DownloadComponentView: View { } } .foregroundStyle(.primary) - .alert( - self.store.scope(state: \.alert, action: DownloadComponent.Action.alert), - dismiss: .dismissed - ) + .alert(store: self.store.scope(state: \.$alert, action: { .alert($0) })) } } } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift index 3cfd39ce8fcb..50d9f959a7a8 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift @@ -17,7 +17,7 @@ private let readMe = """ struct CityMap: Reducer { struct State: Equatable, Identifiable { var download: Download - var downloadAlert: AlertState? + var downloadAlert: AlertState? var downloadMode: Mode var id: UUID { self.download.id } @@ -64,7 +64,7 @@ struct CityMap: Reducer { // NB: This is where you could perform the effect to save the data to a file on disk. return .none - case .downloadComponent(.alert(.deleteButtonTapped)): + case .downloadComponent(.alert(.presented(.deleteButtonTapped))): // NB: This is where you could perform the effect to delete the data from disk. return .none diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift index 67908f398144..b5b9b19d7698 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift @@ -20,15 +20,17 @@ private let readMe = """ // MARK: - Reusable favorite component struct FavoritingState: Equatable { - var alert: AlertState? + @PresentationState var alert: AlertState? let id: ID var isFavorite: Bool } enum FavoritingAction: Equatable { - case alertDismissed + case alert(PresentationAction) case buttonTapped case response(TaskResult) + + enum Alert: Equatable {} } struct Favoriting: Reducer { @@ -42,7 +44,7 @@ struct Favoriting: Reducer { into state: inout FavoritingState, action: FavoritingAction ) -> Effect { switch action { - case .alertDismissed: + case .alert(.dismiss): state.alert = nil state.isFavorite.toggle() return .none @@ -77,7 +79,7 @@ struct FavoriteButton: View { Image(systemName: "heart") .symbolVariant(viewStore.isFavorite ? .fill : .none) } - .alert(self.store.scope(state: \.alert, action: { $0 }), dismiss: .alertDismissed) + .alert(store: self.store.scope(state: \.$alert, action: { .alert($0) })) } } } @@ -86,7 +88,7 @@ struct FavoriteButton: View { struct Episode: Reducer { struct State: Equatable, Identifiable { - var alert: AlertState? + var alert: AlertState? let id: UUID var isFavorite: Bool let title: String diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AnimationsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AnimationsTests.swift index 2fd3bd7c0bc9..4fda0b57626e 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AnimationsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AnimationsTests.swift @@ -93,7 +93,7 @@ final class AnimationTests: XCTestCase { } } - await store.send(.resetConfirmationButtonTapped) { + await store.send(.alert(.presented(.resetConfirmationButtonTapped))) { $0 = Animations.State() } diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-SharedStateTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-SharedStateTests.swift index 7fa28b9434bd..d52a9ed987c0 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-SharedStateTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-SharedStateTests.swift @@ -78,7 +78,7 @@ final class SharedStateTests: XCTestCase { TextState("👍 The number 3 is prime!") } } - await store.send(.alertDismissed) { + await store.send(.alert(.dismiss)) { $0.alert = nil } } @@ -97,7 +97,7 @@ final class SharedStateTests: XCTestCase { TextState("👎 The number 6 is not prime :(") } } - await store.send(.alertDismissed) { + await store.send(.alert(.dismiss)) { $0.alert = nil } } diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift deleted file mode 100644 index 92b9716180c5..000000000000 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -import ComposableArchitecture -import XCTest - -@testable import SwiftUICaseStudies - -@MainActor -final class LifecycleTests: XCTestCase { - func testLifecycle() async { - let clock = TestClock() - - let store = TestStore(initialState: LifecycleDemo.State()) { - LifecycleDemo() - } withDependencies: { - $0.continuousClock = clock - } - - await store.send(.toggleTimerButtonTapped) { - $0.count = 0 - } - - await store.send(.timer(.onAppear)) - - await clock.advance(by: .seconds(1)) - await store.receive(.timer(.wrapped(.tick))) { - $0.count = 1 - } - - await clock.advance(by: .seconds(1)) - await store.receive(.timer(.wrapped(.tick))) { - $0.count = 2 - } - - await store.send(.timer(.wrapped(.incrementButtonTapped))) { - $0.count = 3 - } - - await store.send(.timer(.wrapped(.decrementButtonTapped))) { - $0.count = 2 - } - - await store.send(.toggleTimerButtonTapped) { - $0.count = nil - } - - await store.send(.timer(.onDisappear)) - } -} diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift index 8bf58cbb68da..327f4d15a92b 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift @@ -75,7 +75,7 @@ final class ReusableComponentsFavoritingTests: XCTestCase { } } - await store.send(.episode(id: episodes[0].id, action: .favorite(.alertDismissed))) { + await store.send(.episode(id: episodes[0].id, action: .favorite(.alert(.dismiss)))) { $0.episodes[id: episodes[0].id]?.alert = nil $0.episodes[id: episodes[0].id]?.isFavorite = false } diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift index 3f5805ede8a9..35665879fd5f 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift @@ -65,13 +65,13 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase { ButtonState(role: .destructive, action: .send(.stopButtonTapped, animation: .default)) { TextState("Stop") } - ButtonState(role: .cancel, action: .nevermindButtonTapped) { + ButtonState(role: .cancel) { TextState("Nevermind") } } } - await store.send(.alert(.stopButtonTapped)) { + await store.send(.alert(.presented(.stopButtonTapped))) { $0.alert = nil $0.mode = .notDownloaded } @@ -101,7 +101,7 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase { ButtonState(role: .destructive, action: .send(.stopButtonTapped, animation: .default)) { TextState("Stop") } - ButtonState(role: .cancel, action: .nevermindButtonTapped) { + ButtonState(role: .cancel) { TextState("Nevermind") } } @@ -137,13 +137,13 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase { ButtonState(role: .destructive, action: .send(.deleteButtonTapped, animation: .default)) { TextState("Delete") } - ButtonState(role: .cancel, action: .nevermindButtonTapped) { + ButtonState(role: .cancel) { TextState("Nevermind") } } } - await store.send(.alert(.deleteButtonTapped)) { + await store.send(.alert(.presented(.deleteButtonTapped))) { $0.alert = nil $0.mode = .notDownloaded } diff --git a/Examples/Integration/Integration.xcodeproj/project.pbxproj b/Examples/Integration/Integration.xcodeproj/project.pbxproj index a5091fe164b1..14b89fd76afc 100644 --- a/Examples/Integration/Integration.xcodeproj/project.pbxproj +++ b/Examples/Integration/Integration.xcodeproj/project.pbxproj @@ -746,7 +746,7 @@ repositoryURL = "https://github.com/pointfreeco/swiftui-navigation.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.5.0; + minimumVersion = 1.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/Integration/Integration/NavigationStackTestCase.swift b/Examples/Integration/Integration/NavigationStackTestCase.swift index dbe9aec999a6..e0ab744ee042 100644 --- a/Examples/Integration/Integration/NavigationStackTestCase.swift +++ b/Examples/Integration/Integration/NavigationStackTestCase.swift @@ -44,7 +44,7 @@ private struct ChildFeature: Reducer { state.count -= 1 return .none case .dismissButtonTapped: - return .fireAndForget { await self.dismiss() } + return .run { _ in await self.dismiss() } case .incrementButtonTapped: state.count += 1 return .none diff --git a/Examples/Integration/Integration/PresentationTestCase.swift b/Examples/Integration/Integration/PresentationTestCase.swift index 41102730662a..b368d13c7e58 100644 --- a/Examples/Integration/Integration/PresentationTestCase.swift +++ b/Examples/Integration/Integration/PresentationTestCase.swift @@ -257,7 +257,7 @@ private struct ChildFeature: Reducer { return .none case .childDismissButtonTapped: state.isDismissed = true - return .fireAndForget { await self.dismiss() } + return .run { _ in await self.dismiss() } case .dismissAndAlert: return .none case .incrementButtonTapped: diff --git a/Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift b/Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift index 8cb4abe12843..65b52cc70988 100644 --- a/Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift +++ b/Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift @@ -1,6 +1,6 @@ import ComposableArchitecture import Speech -@preconcurrency import SwiftUI +import SwiftUI private let readMe = """ This application demonstrates how to work with a complex dependency in the Composable \ diff --git a/Examples/Standups/Standups.xcodeproj/project.pbxproj b/Examples/Standups/Standups.xcodeproj/project.pbxproj index ce26eece8900..c5100eb7864d 100644 --- a/Examples/Standups/Standups.xcodeproj/project.pbxproj +++ b/Examples/Standups/Standups.xcodeproj/project.pbxproj @@ -673,7 +673,7 @@ repositoryURL = "https://github.com/pointfreeco/swiftui-navigation.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.5.0; + minimumVersion = 1.0.0; }; }; DC808DB429E9C58F0072B4A9 /* XCRemoteSwiftPackageReference "swift-tagged" */ = { diff --git a/Examples/TicTacToe/tic-tac-toe/Package.swift b/Examples/TicTacToe/tic-tac-toe/Package.swift index 059458f91eba..281a3f814ae2 100644 --- a/Examples/TicTacToe/tic-tac-toe/Package.swift +++ b/Examples/TicTacToe/tic-tac-toe/Package.swift @@ -28,7 +28,7 @@ let package = Package( ], dependencies: [ .package(name: "swift-composable-architecture", path: "../../.."), - .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "0.1.0"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), ], targets: [ .target( diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/GameCore/GameCore.swift b/Examples/TicTacToe/tic-tac-toe/Sources/GameCore/GameCore.swift index 3d5cf506f2fc..2d49dc42b35a 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/GameCore/GameCore.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/GameCore/GameCore.swift @@ -52,7 +52,7 @@ public struct Game: Reducer, Sendable { return .none case .quitButtonTapped: - return .fireAndForget { + return .run { _ in await self.dismiss() } } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift b/Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift index 85f9582056b0..bb6735e94bc7 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift @@ -15,13 +15,13 @@ public struct Login: Reducer, Sendable { public init() {} } - public enum Action: Equatable { + public enum Action: Equatable, Sendable { case alert(PresentationAction) case loginResponse(TaskResult) case twoFactor(PresentationAction) case view(View) - public enum View: BindableAction, Equatable { + public enum View: BindableAction, Equatable, Sendable { case binding(BindingAction) case loginButtonTapped } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorCore/TwoFactorCore.swift b/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorCore/TwoFactorCore.swift index d4fbe1d6f6ea..ff79ea025154 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorCore/TwoFactorCore.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorCore/TwoFactorCore.swift @@ -16,14 +16,14 @@ public struct TwoFactor: Reducer, Sendable { } } - public enum Action: Equatable { + public enum Action: Equatable, Sendable { case alert(PresentationAction) case twoFactorResponse(TaskResult) case view(View) - public enum Alert: Equatable {} + public enum Alert: Equatable, Sendable {} - public enum View: BindableAction, Equatable { + public enum View: BindableAction, Equatable, Sendable { case binding(BindingAction) case submitButtonTapped } @@ -55,11 +55,13 @@ public struct TwoFactor: Reducer, Sendable { case .view(.submitButtonTapped): state.isTwoFactorRequestInFlight = true - return .task { [code = state.code, token = state.token] in - .twoFactorResponse( - await TaskResult { - try await self.authenticationClient.twoFactor(.init(code: code, token: token)) - } + return .run { [code = state.code, token = state.token] send in + await send( + .twoFactorResponse( + await TaskResult { + try await self.authenticationClient.twoFactor(.init(code: code, token: token)) + } + ) ) } } diff --git a/Examples/Todos/Todos/Todos.swift b/Examples/Todos/Todos/Todos.swift index ab42e4d46516..9f8342b2a8c0 100644 --- a/Examples/Todos/Todos/Todos.swift +++ b/Examples/Todos/Todos/Todos.swift @@ -74,9 +74,9 @@ struct Todos: Reducer { state.todos.move(fromOffsets: source, toOffset: destination) - return .task { + return .run { send in try await self.clock.sleep(for: .milliseconds(100)) - return .sortCompletedTodos + await send(.sortCompletedTodos) } case .sortCompletedTodos: diff --git a/Package.resolved b/Package.resolved index 55ca61bb885b..505c484c3e37 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "ec62f32d21584214a4b27c8cee2b2ad70ab2c38a", - "version" : "0.11.0" + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", - "version" : "0.14.1" + "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81", + "version" : "1.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "0fbaebfc013715dab44d715a4d350ba37f297e4d", - "version" : "0.4.0" + "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", + "version" : "1.0.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "479750bd98fac2e813fffcf2af0728b5b0085795", - "version" : "0.1.1" + "revision" : "ea631ce892687f5432a833312292b80db238186a", + "version" : "1.0.0" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "4a87bb75be70c983a9548597e8783236feb3401e", - "version" : "0.11.1" + "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", + "version" : "1.0.0" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "16fd42ae04c6e7f74a6a86395d04722c641cccee", - "version" : "0.6.0" + "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", + "version" : "1.0.0" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin", "state" : { - "revision" : "9b1258905c21fc1b97bf03d1b4ca12c4ec4e5fda", - "version" : "1.2.0" + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections", "state" : { - "revision" : "f52eee28bdc6065aa2f8424067e6f04c74bda6e6", - "version" : "0.7.1" + "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", + "version" : "1.0.0" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation", "state" : { - "revision" : "2aa885e719087ee19df251c08a5980ad3e787f12", - "version" : "0.8.0" + "revision" : "f5bcdac5b6bb3f826916b14705f37a3937c2fd34", + "version" : "1.0.0" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "4af50b38daf0037cfbab15514a241224c3f62f98", - "version" : "0.8.5" + "revision" : "302891700c7fa3b92ebde9fe7b42933f8349f3c7", + "version" : "1.0.0" } } ], diff --git a/Package.swift b/Package.swift index 52dd0fc72d19..08b6d1ac55cc 100644 --- a/Package.swift +++ b/Package.swift @@ -20,14 +20,14 @@ let package = Package( .package(url: "https://github.com/apple/swift-collections", from: "1.0.2"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), .package(url: "https://github.com/google/swift-benchmark", from: "0.1.0"), - .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.11.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.14.1"), - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "0.1.1"), - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "0.11.1"), - .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "0.6.0"), - .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "0.7.0"), - .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.8.0"), - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.8.4"), + .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), ], targets: [ .target( diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md index 2e37a1a0f819..fb7078922db4 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md @@ -94,8 +94,8 @@ struct Feature: Reducer { } ``` -And then we implement the ``Reducer/reduce(into:action:)-4zl56`` method which is responsible -for handling the actual logic and behavior for the feature. It describes how to change the current +And then we implement the ``Reducer/reduce(into:action:)-1t2ri`` method which is responsible for +handling the actual logic and behavior for the feature. It describes how to change the current state to the next state, and describes what effects need to be executed. Some actions don't need to execute effects, and they can return `.none` to represent that: diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigratingToTheReducerProtocol.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigratingToTheReducerProtocol.md deleted file mode 100644 index 95a7617b783d..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigratingToTheReducerProtocol.md +++ /dev/null @@ -1,710 +0,0 @@ -# Migrating to the reducer protocol - -Learn how to migrate existing applications to use the new ``Reducer``, in both Swift 5.7 and -Swift 5.6. - -## Overview - -Migrating an application that uses the ``Reducer`` type over to the new ``Reducer`` can be -done slowly and incrementally. The library provides the tools to convert one reducer at a time, -allowing you to plug protocol-style reducers into old-style reducers, and vice-versa. - -Although we recommend migrating your code when you have time, the newest version of the library -is still 100% backwards compatible with all previous versions. The ``Reducer`` type is now -"soft" deprecated, which means we consider it deprecated, and it says so in the documentation, but -you will not get any warnings about it. Sometime in the future, we will officially deprecate it, -and then sometime even later we will remove it so that we can rename the protocol to `Reducer`. - -This article outlines a number of strategies you can employ to convert your reducers to the protocol -when you are ready: - -* [Leaf node features](#Leaf-node-features) -* [Composition of features](#Composition-of-features) -* [Optional and pullback reducers](#Optional-and-pullback-reducers) -* [For-each reducers](#For-each-reducers) -* [Binding reducers](#Binding-reducers) -* [Dependencies](#Dependencies) -* [Stores](#Stores) -* [Testing](#Testing) -* [Embedding old reducer values in a new reducer conformance](#Embedding-old-reducer-values-in-a-new-reducer-conformance) -* [Migration using Swift 5.6](#Migration-using-Swift-56) - -## Leaf node features - -The simplest parts of an application to convert to ``Reducer`` are leaf node features that -do not compose multiple reducers at once. For example, suppose you have a feature domain with a -dependency like this: - -```swift -struct FeatureState { - // ... -} -enum FeatureAction { - // ... -} -struct FeatureEnvironment { - var date: () -> Date -} - -let featureReducer = Reducer< - FeatureState, - FeatureAction, - FeatureEnvironment -> { state, action, environment in - switch action { - // ... - } -} -``` - -You can convert this to the protocol style by: - -1. Creating a dedicated type that conforms to the ``Reducer``. -1. Nest the state and action types inside this new type, and rename them to just `State` and -`Action`. -1. Move the fields on the environment to be fields on this new reducer type, and delete the -environment type. -1. Move the reducer's closure implementation to the ``Reducer/reduce(into:action:)-4zl56`` -method. - -Performing these 4 steps on the feature produces the following: - -```swift -struct Feature: Reducer { - struct State { - // ... - } - - enum Action { - // ... - } - - let date: () -> Date - - func reduce(into state: inout State, action: Action) -> Effect { - switch action { - // ... - } - } -} -``` - -Once this feature's domain and reducer are converted to the protocol-style you will invariably have -compiler errors wherever you were referring to the old types. For example, suppose you have a -parent feature that is currently trying to embed the old-style domain and reducer into its domain -and reducer: - -```swift -struct ParentState { - var feature: FeatureState - // ... -} - -enum ParentAction { - case feature(FeatureAction) - // ... -} - -struct ParentEnvironment { - var date: () -> Date - var dependency: Dependency - // ... -} - -let parentReducer = Reducer.combine( - featureReducer - .pullback( - state: \.feature, - action: /ParentAction.feature, - environment: { - FeatureEnvironment(date: $0.date) - } - ), - - Reducer { state, action, environment in - // ... - } -) -``` - -This can be updated to work with the new `Feature` reducer conformance by first fixing any -references to the state and action types: - -```swift -struct ParentState { - var feature: Feature.State - // ... -} - -enum ParentAction { - case feature(Feature.Action) - // ... -} -``` - -And then the `parentReducer` can be fixed by making use of the helper ``AnyReducer/init(_:)-29rlv`` -which aids in converting protocol-style reducers into old-style reducers. It is initialized with a -closure that is passed an environment, which is the one thing protocol-style reducers don't have, -and you are to return a protocol-style reducer: - -```swift -let parentReducer = Reducer.combine( - AnyReducer { environment in - Feature(date: environment.date) - } - .pullback( - state: \.feature, - action: /ParentAction.feature, - environment: { $0 } - ), - - Reducer { state, action, environment in - // ... - } -) -``` - -Note that the ``AnyReducer``'s only purpose is to convert the protocol-style reducer to the -old-style so that it can be plugged into existing old-style reducers. You can then chain on the -operators you were using before to the end of the ``AnyReducer`` usage. - -With those few changes your application should now build, and you have successfully converted one -leaf node feature to the new ``Reducer``-style of doing things. - -## Composition of features - -Some features in your application are an amalgamation of other features. For example, a tab-based -application may have a separate domain and reducer for each tab, and then an app-level domain and -reducer that composes everything together. - -Suppose that all of the tab features have already been converted to the protocol-style: - -```swift -struct TabA: Reducer { - struct State { - // ... - } - enum Action { - // ... - } - func reduce(into state: inout State, action: Action) -> Effect { - // ... - } -} - -struct TabB: Reducer { - // ... -} - -struct TabC: Reducer { - // ... -} -``` - -But, suppose that the app-level domain and reducer have not yet been converted and so have compiler -errors due to referencing types and values that no longer exist: - -```swift -struct AppState { - var tabA: TabAState - var tabB: TabBState - var tabC: TabCState -} - -enum AppAction { - case tabA(TabAAction) - case tabB(TabBAction) - case tabC(TabCAction) -} - -struct AppEnvironment {} - -let appReducer = Reducer< - AppState, - AppAction, - AppEnvironment -> { state, action, environment in - // ... -} -``` - -To convert this to the protocol-style we again introduce a new type that conforms to the -``Reducer``, we nest the domain types inside the conformance, we inline the environment -fields, but this time we use the ``Reducer/body-swift.property-8lumc`` requirement of the -protocol to describe how to compose multiple reducers: - -```swift -struct AppReducer: Reducer { - struct State { - var tabA: TabA.State - var tabB: TabB.State - var tabC: TabC.State - } - - enum Action { - case tabA(TabA.Action) - case tabB(TabB.Action) - case tabC(TabC.Action) - } - - var body: some Reducer { - Scope(state: \.tabA, action: /Action.tabA) { - TabA() - } - Scope(state: \.tabB, action: /Action.tabB) { - TabB() - } - Scope(state: \.tabC, action: /Action.tabC) { - TabC() - } - } -} -``` - -With those few small changes we have now converted a composition of many reducers into the new -protocol-style. - -## Optional and pullback reducers - -A common pattern in the Composable Architecture is to model a feature that can be presented and -dismissed as optional state. For example, suppose you have the feature's domain and reducer modeled -like so: - -```swift -struct FeatureState { - // ... -} -enum FeatureAction { - // ... -} -struct FeatureEnvironment { - var date: () -> Date -} - -let featureReducer = Reducer< - FeatureState, - FeatureAction, - FeatureEnvironment -> { state, action, environment in - // Feature logic -} -``` - -Then, the parent feature can embed this child feature as an optional in its state: - -```swift -struct ParentState { - var feature: FeatureState? - // ... -} -enum ParentAction { - case feature(FeatureAction) - // ... -} -struct ParentEnvironment { - var date: () -> Date -} -``` - -A non-`nil` value for `feature` indicates that the feature view is being presented, and when it -switches to `nil` the view should be dismissed. The actual showing and hiding of the view can be -done using the ``IfLetStore`` SwiftUI view. - -In order to construct a single reducer that can handle the logic for the parent domain as well as -allow the child feature to run its logic on the `feature` state when non-`nil`, we can make use the -``AnyReducer/optional(file:fileID:line:)`` and ``AnyReducer/pullback(state:action:environment:)`` -operators: - -```swift -let parentReducer = Reducer< - ParentState, - ParentAction, - ParentEnvironment ->.combine( - featureReducer - .optional() - .pullback( - state: \.feature, - action: /ParentAction.feature, - environment: { FeatureEnvironment(date: $0.date) } - ), - - Reducer { state, action, environment in - // Parent logic - } -) -``` - -It seems complex, but we have now combined the logic for the parent feature and child feature into -one package, and the child feature will only run when the state is non-`nil`. - -Migrating the `featureReducer` to the protocol by following the earlier instructions will -yield a new `Feature` type that conforms to ``Reducer``, and the `parentReducer` will -look something like this: - -```swift -let parentReducer = Reducer< - ParentState, - ParentAction, - ParentEnvironment ->.combine( - AnyReducer { environment in - Feature(date: environment.date) - } - .optional() - .pullback( - state: \.feature, - action: /ParentAction.feature, - environment: { $0 } - ), - - Reducer { state, action, environment in - // Parent logic - } -) -``` - -Now the question is, how do we migrate `parentReducer` to a protocol conformance? - -This gives us an opportunity to improve the correctness of this code. It turns out there is a gotcha -with the `optional` operator: it must be run _before_ the parent logic runs. If it is not, then it -is possible for a child action to come into the system, the parent observes the action and decides -to `nil` out the child state, and then the child reducer will not get a chance to react to the -action. This can cause subtle bugs, and so we have documentation advising you to order things the -correct way, and if we detect a child action while state is `nil` we display a runtime warning. - -A `Parent` reducer conformances can be made by implementing the -``Reducer/body-swift.property-8lumc`` property of the ``Reducer``, which allows you to express the -parent's logic as a composition of multiple reducers. In particular, you can use the ``Reduce`` -entry point to implement the core parent logic, and then chain on the -``Reducer/ifLet(_:action:then:fileID:line:)`` operator to identify the optional child state that you -want to run the `Feature` reducer on when non-`nil`: - -```swift -struct Parent: Reducer { - struct State { - var feature: Feature.State? - // ... - } - enum Action { - case feature(Feature.Action) - // ... - } - - let date: () -> Date - - var body: some Reducer { - Reduce { state, action in - // Parent logic - } - .ifLet(\.feature, action: /Action.feature) { - Feature(date: self.date) - } - } -} -``` - -Because the `ifLet` operator has knowledge of both the parent and child reducers it can enforce the -order to add an additional layer of correctness. - -If you are using an enum to model your state, then there is a corresponding -``Reducer/ifCaseLet(_:action:then:fileID:line:)`` operator that can help you run a reducer on just -one case of the enum. - -## For-each reducers - -Similar to `optional` reducers, another common pattern in applications is the use of the -``AnyReducer/forEach(state:action:environment:file:fileID:line:)-2ypoa`` to allow running a reducer -on each element of a collection. Converting such child and parent reducers will look nearly -identical to what we did above for optional reducers, but it will make use of the new -``Reducer/forEach(_:action:element:fileID:line:)`` operator instead. - -In particular, the new `forEach` method operates on the parent reducer by specifying the collection -sub-state you want to work on, and providing the element reducer you want to be able to run on -each element: - -```swift -struct Parent: Reducer { - struct State { - var rows: IdentifiedArrayOf - // ... - } - enum Action { - case row(id: Feature.State.ID, action: Feature.Action) - // ... - } - - let date: () -> Date - - var body: some Reducer { - Reduce { state, action in - // Parent logic - } - .forEach(\.rows, action: /Action.row) { - Feature(date: self.date) - } - } -} -``` - -## Binding reducers - -Previously, reducers with bindable state and a binding action used the `Reducer.binding()` method -to automatically make mutations to state before running the main logic of a reducer. - -```swift -Reducer { state, action, environment in - // Logic to run after bindable state mutations are applied -} -.binding() -``` - -In reducer builders, use the new top-level ``BindingReducer`` type to specify when to apply -mutations to binding state: - -```swift -var body: some Reducer { - Reduce { state, action in - // Logic to run before binding state mutations are applied - } - - BindingReducer() // Apply binding state mutations - - Reduce { state, action in - // Logic to run after binding state mutations are applied - } -} -``` - -## Dependencies - -In the previous sections we inlined all dependencies directly into the conforming type: - -```swift -struct Feature: Reducer { - let apiClient: APIClient - let date: () -> Date - // ... -} -``` - -But this means that you must explicitly thread all dependencies from the root of the application -through to every child feature. This can be arduous and make it difficult to add, remove or change -dependencies. - -The Composable Architecture now uses the [Dependencies][swift-dependencies] library to manage -dependencies in a more ergonomic manner, and even comes with some common dependencies pre-integrated -allowing you to access them with no additional work. For example, the `date` dependency ships with -the library so that you can declare your feature's dependence on that functionality in the following -way: - -```swift -struct Feature: Reducer { - let apiClient: APIClient - @Dependency(\.date) var date - // ... -} -``` - -With that one declaration you can stop explicitly passing the date dependency through every layer -of your application. A date function will be automatically provided to your feature's reducer. - -> Important: [Dependencies][swift-dependencies] is powered by Swift task locals and is intended to -> be used in structured contexts. If your reducer's effects make use of escaping closures, then -> you must do additional work to propagate the dependencies to that context. For example, using -> a dependency from within a Combine operator such as `.map`, `.flatMap` and even `.filter` will -> use the default dependency value. -> -> See the [Dependencies documentation][swift-dependencies-docs] on -> [Dependency lifetimes][swift-dependencies-docs-lifetimes] for more information, and how to -> integrate the `@Dependency` property wrapper into pre-structured concurrency using the -> `withEscapedDependencies` function. - -For domain-specific dependencies you can perform a little bit of upfront work to register your -dependency with the system, and then it will be automatically available to every layer in your -application: - -```swift -private enum APIClientKey: DependencyKey { - static let liveValue = APIClient.live -} -extension DependencyValues { - var apiClient: APIClient { - get { self[APIClientKey.self] } - set { self[APIClientKey.self] = newValue } - } -} -``` - -With that work done you can access the dependency from any feature's reducer using the `@Dependency` -property wrapper: - -```swift -struct Feature: Reducer { - @Dependency(\.apiClient) var apiClient - @Dependency(\.date) var date - // ... -} -``` - -For more information on designing your dependencies and providing live and test dependencies, see -our article. - -[swift-dependencies]: https://github.com/pointfreeco/swift-dependencies -[swift-dependencies-docs]: https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/ -[swift-dependencies-docs-lifetimes]: https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/lifetimes - -## Stores - -Stores can be initialized from an initial state and an instance of a type conforming to -``Reducer``: - -```swift -FeatureView( - store: Store(initialState: Feature.State()) { - Feature() - } -) -``` - -Views that hold onto stores can also employ the ``StoreOf`` type alias to clean up the property -declaration: - -```swift -let store: StoreOf -// Expands to: -// let store: Store -``` - -## Testing - -Test stores can be initialized from an initial state and an instance of a type conforming to -``Reducer``. - -```swift -let store = TestStore(initialState: Feature.State()) { - Feature() -} -``` - -By default test stores will employ "test" dependencies wherever a dependency is accessed from a -reducer via the `@Dependency` property wrapper. - -Instead of passing an environment of test dependencies to the store, or mutating the store's -``TestStore/environment``, you can either provide a trailing closure when initializing ``TestStore`` -or you can directly mutate the test store's ``TestStore/dependencies`` to -override dependencies driving a feature. - -For example, to install a test clock as the continuous clock dependency you can do the following: - -```swift -let clock = TestClock() - -let store = TestStore(initialState: Feature.State()) { - Feature() -} withDependencies: { - $0.continuousClock = .clock -} -``` - -…or you can do: - -```swift -let clock = TestClock() -store.dependencies.continuousClock = clock - -await store.send(.timerButtonStarted) - -await clock.advance(by: .seconds(1)) -await store.receive(.timerTick) { - $0.secondsElapsed = 1 -} - -await store.send(.timerButtonStopped) -``` - -## Embedding old reducer values in a new reducer conformance - -It may not be feasible to migrate your entire application at once, and you may find yourself -needing to compose an existing value of ``Reducer`` into a type conforming to ``Reducer``. -This can be done by passing the value and its environment of dependencies to -``Reduce/init(_:environment:)``. - -For example, suppose a tab of your application has not yet been converted to the protocol-style of -reducers, and it has an environment of dependencies: - -```swift -struct TabCState { - // ... -} -enum TabCAction { - // ... -} -struct TabCEnvironment { - var date: () -> Date -} -let tabCReducer = Reducer< - TabCState, - TabCAction, - TabCEnvironment -} { state, action, environment in - // ... -} -``` - -It can still be embedded in `AppReducer` using ``Reduce/init(_:environment:)`` and passing along the -necessary dependencies. - -```swift -struct AppReducer: Reducer { - struct State { - // ... - var tabC: TabCState - } - - enum Action { - // ... - case tabC(TabCAction) - } - - @Dependency(\.date) var date - - var body: some Reducer { - // ... - Scope(state: \.tabC, action: /Action.tabC) { - Reduce( - tabCReducer, - environment: TabCEnvironment(date: self.date) - ) - } - } -} -``` - -## Migration using Swift 5.6 - -The migration strategy described above for Swift 5.7 also applies to applications that are still -using Xcode 13 and Swift 5.6, but with one small change. When conforming your types to the -``Reducer`` you are not allowed to use the syntax `some Reducer` -because that is only available in Swift 5.7. Instead, you must specify `Reduce` -as the type of the `body` property: - -```swift -struct AppReducer: Reducer { - // ... - var body: Reduce { - FeatureA() - FeatureB() - FeatureC() - } -} -``` - -The ``Reduce`` type is like a type-erased reducer that allows you to construct a reducer from a -closure. In Swift 5.6, the ``ReducerBuilder`` will automatically erase the reducer you build for -you so that you do not have to worry about specifying its type explicitly. This may come with a -slight performance cost compared to using full opaque types for `body`, but should be of comparable -performance to reducers using the ``Reducer`` type, which is now soft-deprecated. - -All other features of the library should work in Swift 5.6 without any other changes. This includes -`@Dependency` and all dependency management tools. diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md index f257636fa59f..9d7c234509c8 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md @@ -17,8 +17,8 @@ them. A common performance pitfall when using the library comes from constructing ``ViewStore``s, which is the object that observes changes to your feature's state. When constructed naively, using either -view store's initializer ``ViewStore/init(_:)-1pfeq`` or the SwiftUI helper ``WithViewStore``, it -will observe every change to state in the store: +view store's initializer ``ViewStore/init(_:observe:)-3ak1y`` or the SwiftUI helper +``WithViewStore``, it will observe every change to state in the store: ```swift WithViewStore(self.store, observe: { $0 }) { viewStore in diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/StackBasedNavigation.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/StackBasedNavigation.md index 6c97d32966e1..78d5027ec334 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/StackBasedNavigation.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/StackBasedNavigation.md @@ -285,9 +285,7 @@ struct Feature: Reducer { ``` > Note: The ``DismissEffect`` function is async which means it cannot be invoked directly inside a -> reducer. Instead it must be called from either -> ``EffectPublisher/run(priority:operation:catch:fileID:line:)`` or -> ``EffectPublisher/fireAndForget(priority:_:)``. +> reducer. Instead it must be called from ``Effect/run(priority:operation:catch:fileID:line:)`` When `self.dismiss()` is invoked it will remove the corresponding value from the ``StackState`` powering the navigation stack. It does this by sending a ``StackAction/popFrom(id:)`` action back @@ -468,7 +466,7 @@ await store.send(.path(.element(id: 0, action: .incrementButtonTapped))) { And then we finally expect that the child dismisses itself, which manifests itself as the ``StackAction/popFrom(id:)`` action being sent to pop the counter feature off the stack, which we -can assert using the ``TestStore/receive(_:timeout:assert:file:line:)-1rwdd`` method on +can assert using the ``TestStore/receive(_:timeout:assert:file:line:)-5awso`` method on ``TestStore``: ```swift diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/SwiftConcurrency.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/SwiftConcurrency.md index 13b3d4ca9808..cd8f1e0ce5fa 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/SwiftConcurrency.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/SwiftConcurrency.md @@ -8,10 +8,10 @@ for the time being, but in Swift 6 most (if not all) of these warnings will beco you will need to know how to prove to the compiler that your types are safe to use concurrently. There primary way to create an ``Effect`` in the library is via -``EffectPublisher/run(priority:operation:catch:fileID:line:)``. It takes a `@Sendable`, -asynchronous closure, which restricts the types of closures you can use for your effects. In -particular, the closure can only capture `Sendable` variables that are bound with `let`. Mutable -variables and non-`Sendable` types are simply not allowed to be passed to `@Sendable` closures. +``Effect/run(priority:operation:catch:fileID:line:)``. It takes a `@Sendable`, asynchronous closure, +which restricts the types of closures you can use for your effects. In particular, the closure can +only capture `Sendable` variables that are bound with `let`. Mutable variables and non-`Sendable` +types are simply not allowed to be passed to `@Sendable` closures. There are two primary ways you will run into this restriction when building a feature in the Composable Architecture: accessing state from within an effect, and accessing a dependency from diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md index 9fa9372f306f..2c83b0236c1a 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md @@ -73,8 +73,8 @@ class CounterTests: XCTestCase { > Tip: Test cases that use ``TestStore`` should be annotated as `@MainActor` and test methods should be marked as `async` since most assertion helpers on ``TestStore`` can suspend. -Test stores have a ``TestStore/send(_:assert:file:line:)-1ax61`` method, but it behaves differently -from stores and view stores. You provide an action to send into the system, but then you must also +Test stores have a ``TestStore/send(_:assert:file:line:)`` method, but it behaves differently from +stores and view stores. You provide an action to send into the system, but then you must also provide a trailing closure to describe how the state of the feature changed after sending the action: @@ -94,8 +94,8 @@ await store.send(.incrementButtonTapped) { } ``` -> The ``TestStore/send(_:assert:file:line:)-1ax61`` method is `async` for technical reasons that we -> do not have to worry about right now. +> The ``TestStore/send(_:assert:file:line:)`` method is `async` for technical reasons that we do not +> have to worry about right now. If your mutation is incorrect, meaning you perform a mutation that is different from what happened in the ``Reducer``, then you will get a test failure with a nicely formatted message showing exactly @@ -148,7 +148,7 @@ await store.send(.decrementButtonTapped) { > by one, but we haven't proven we know the precise value of `count` at each step of the way. > > In general, the less logic you have in the trailing closure of -> ``TestStore/send(_:assert:file:line:)-1ax61``, the stronger your assertion will be. It is best to +> ``TestStore/send(_:assert:file:line:)``, the stronger your assertion will be. It is best to > use simple, hard-coded data for the mutation. Test stores do expose a ``TestStore/state`` property, which can be useful for performing assertions @@ -162,7 +162,7 @@ store.send(.incrementButtonTapped) { XCTAssertTrue(store.state.isPrime) ``` -However, when inside the trailing closure of ``TestStore/send(_:assert:file:line:)-1ax61``, the +However, when inside the trailing closure of ``TestStore/send(_:assert:file:line:)``, the ``TestStore/state`` property is equal to the state _before_ sending the action, not after. That prevents you from being able to use an escape hatch to get around needing to actually describe the state mutation, like so: @@ -186,8 +186,8 @@ Location, Core Motion, Speech Recognition, etc.), and more. As a simple example, suppose we have a feature with a button such that when you tap it, it starts a timer that counts up until you reach 5, and then stops. This can be accomplished using the -``EffectPublisher/run(priority:operation:catch:fileID:line:)`` helper on ``Effect``, which provides -you with an asynchronous context to operate in and can send multiple actions back into the system: +``Effect/run(priority:operation:catch:fileID:line:)`` helper on ``Effect``, which provides you with +an asynchronous context to operate in and can send multiple actions back into the system: ```swift struct Feature: Reducer { @@ -249,7 +249,7 @@ supposed to be running, or perhaps the data it feeds into the system later is wr requires all effects to finish. To get this test passing we need to assert on the actions that are sent back into the system -by the effect. We do this by using the ``TestStore/receive(_:timeout:assert:file:line:)-1rwdd`` +by the effect. We do this by using the ``TestStore/receive(_:timeout:assert:file:line:)-5awso`` method, which allows you to assert which action you expect to receive from an effect, as well as how the state changes after receiving that effect: @@ -265,7 +265,7 @@ going to be received, but after waiting around for a small amount of time no act > ❌ Failure: Expected to receive an action, but received none after 0.1 seconds. This is because our timer is on a 1 second interval, and by default -``TestStore/receive(_:timeout:assert:file:line:)-1rwdd`` only waits for a fraction of a second. This +``TestStore/receive(_:timeout:assert:file:line:)-5awso`` only waits for a fraction of a second. This is because typically you should not be performing real time-based asynchrony in effects, and instead using a controlled entity, such as a clock, that can be sped up in tests. We will demonstrate this in a moment, so for now let's increase the timeout: @@ -363,7 +363,7 @@ let store = TestStore(initialState: Feature.State(count: 0)) { ``` With that small change we can drop the `timeout` arguments from the -``TestStore/receive(_:timeout:assert:file:line:)-1rwdd`` invocations: +``TestStore/receive(_:timeout:assert:file:line:)-5awso`` invocations: ```swift await store.receive(.timerTick) { @@ -548,7 +548,7 @@ It can be important to understand how non-exhaustive testing works under the hoo limit the ways in which you can assert on state changes. When you construct an _exhaustive_ test store, which is the default, the `$0` used inside the -trailing closure of ``TestStore/send(_:assert:file:line:)-1ax61`` represents the state _before_ the +trailing closure of ``TestStore/send(_:assert:file:line:)`` represents the state _before_ the action is sent: ```swift @@ -640,10 +640,10 @@ await store.send(.removeButtonTapped) { Further, when using non-exhaustive test stores that also show skipped assertions (via ``Exhaustivity/off(showSkippedAssertions:)``), then there is another caveat to keep in mind. In -such test stores, the trailing closure of ``TestStore/send(_:assert:file:line:)-1ax61`` is invoked -_twice_ by the test store. First with `$0` representing the state after the action is sent to see if -it does not match the true state, and then again with `$0` representing the state before the action -is sent so that we can show what state assertions were skipped. +such test stores, the trailing closure of ``TestStore/send(_:assert:file:line:)`` is invoked _twice_ +by the test store. First with `$0` representing the state after the action is sent to see if it does +not match the true state, and then again with `$0` representing the state before the action is sent +so that we can show what state assertions were skipped. Because the test store can invoke your trailing assertion closure twice you must be careful if your closure performs any side effects, because those effects will be executed twice. For example, diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/TreeBasedNavigation.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/TreeBasedNavigation.md index eb843e4519ed..01107f5943d8 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/TreeBasedNavigation.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/TreeBasedNavigation.md @@ -424,9 +424,7 @@ struct Feature: Reducer { ``` > Note: The ``DismissEffect`` function is async which means it cannot be invoked directly inside a -> reducer. Instead it must be called from either -> ``EffectPublisher/run(priority:operation:catch:fileID:line:)`` or -> ``EffectPublisher/fireAndForget(priority:_:)``. +> reducer. Instead it must be called from ``Effect/run(priority:operation:catch:fileID:line:)``. When `self.dismiss()` is invoked it will `nil` out the state responsible for presenting the feature by sending a ``PresentationAction/dismiss`` action back into the system, causing the feature to be @@ -546,7 +544,7 @@ await store.send(.counter(.presented(.incrementButtonTapped))) { And then we finally expect that the child dismisses itself, which manifests itself as the ``PresentationAction/dismiss`` action being sent to `nil` out the `counter` state, which we can -assert using the ``TestStore/receive(_:timeout:assert:file:line:)-1rwdd`` method on ``TestStore``: +assert using the ``TestStore/receive(_:timeout:assert:file:line:)-5awso`` method on ``TestStore``: ```swift await store.receive(.counter(.dismiss)) { diff --git a/Sources/ComposableArchitecture/Documentation.docc/ComposableArchitecture.md b/Sources/ComposableArchitecture/Documentation.docc/ComposableArchitecture.md index 70499cdfdab1..58ae173280da 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/ComposableArchitecture.md +++ b/Sources/ComposableArchitecture/Documentation.docc/ComposableArchitecture.md @@ -60,20 +60,15 @@ day-to-day when building applications, such as: - ``Effect`` - ``Store`` - ``ViewStore`` - -### Integrations - -- -- - ### Testing - ``TestStore`` -### Upgrade guides +### Integrations - -- +- +- ## See Also diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/AnyReducerDeprecations.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/AnyReducerDeprecations.md deleted file mode 100644 index e2ca01ffb82f..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/AnyReducerDeprecations.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``ComposableArchitecture/AnyReducer`` - -## Topics - -### Types - -- ``ActionFormat`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/EffectDeprecations.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/EffectDeprecations.md deleted file mode 100644 index af6503f496f2..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/EffectDeprecations.md +++ /dev/null @@ -1,52 +0,0 @@ -# Deprecations - -Review unsupported effect APIs and their replacements. - -## Overview - -Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use instead. - -## Topics - -### Type names and aliases - -- ``EffectPublisher`` -- ``EffectTask`` - -### Creating an effect - -- ``EffectPublisher/task(priority:operation:catch:fileID:line:)`` -- ``EffectPublisher/task(priority:operation:)`` -- ``EffectPublisher/fireAndForget(priority:_:)`` - -### Cancellation - -- ``EffectPublisher/cancel(ids:)-8q1hl`` - -### Testing - -- ``EffectPublisher/failing(_:)`` -- ``EffectPublisher/unimplemented(_:)`` - -### Combine integration - -- ``EffectPublisher/Output`` -- ``EffectPublisher/init(_:)`` -- ``EffectPublisher/init(value:)`` -- ``EffectPublisher/init(error:)`` -- ``EffectPublisher/upstream`` -- ``EffectPublisher/catching(_:)`` -- ``EffectPublisher/debounce(id:for:scheduler:options:)-1xdnj`` -- ``EffectPublisher/debounce(id:for:scheduler:options:)-1oaak`` -- ``EffectPublisher/deferred(for:scheduler:options:)`` -- ``EffectPublisher/fireAndForget(_:)`` -- ``EffectPublisher/future(_:)`` -- ``EffectPublisher/receive(subscriber:)`` -- ``EffectPublisher/result(_:)`` -- ``EffectPublisher/run(_:)`` -- ``EffectPublisher/throttle(id:for:scheduler:latest:)-3gibe`` -- ``EffectPublisher/throttle(id:for:scheduler:latest:)-85y01`` -- ``EffectPublisher/timer(id:every:tolerance:on:options:)-6yv2m`` -- ``EffectPublisher/timer(id:every:tolerance:on:options:)-8t3is`` -- ``EffectPublisher/Subscriber`` - diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ReduceDeprecations.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ReduceDeprecations.md deleted file mode 100644 index 6b8aac153777..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ReduceDeprecations.md +++ /dev/null @@ -1,9 +0,0 @@ -# Deprecations - -Review unsupported `Reduce` APIs. - -## Topics - -### Reducer structure - -- ``Reduce/init(_:environment:)`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ReducerDeprecations.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ReducerDeprecations.md deleted file mode 100644 index 791912e3d48a..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ReducerDeprecations.md +++ /dev/null @@ -1,21 +0,0 @@ -# Deprecations - -Review unsupported reducer APIs and their replacements. - -## Overview - -Avoid using deprecated APIs in your app. Select an API to see the replacement that you should use -instead. - -## Topics - -### Reducer structure - -- ``AnyReducer`` -- ``ReducerProtocol`` -- ``ReducerProtocolOf`` -- ``DebugEnvironment`` - -### Reducer modifiers - -- ``Reducer/debug()`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/StoreDeprecations.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/StoreDeprecations.md deleted file mode 100644 index 0e02d1a0b0e5..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/StoreDeprecations.md +++ /dev/null @@ -1,21 +0,0 @@ -# Deprecations - -Review unsupported store APIs and their replacements. - -## Overview - -Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use instead. - -## Topics - -### Creating a store - -- ``Store/init(initialState:reducer:environment:)`` -- ``Store/init(initialState:reducer:prepareDependencies:)`` - -### Scoping stores - -- ``Store/scope(state:)`` -- ``Store/publisherScope(state:action:)`` -- ``Store/publisherScope(state:)`` -- ``Store/unchecked(initialState:reducer:environment:)`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/SwiftUIDeprecations.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/SwiftUIDeprecations.md index 8d7f7026b83b..1da1e98923b9 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/SwiftUIDeprecations.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/SwiftUIDeprecations.md @@ -8,25 +8,6 @@ Avoid using deprecated APIs in your app. Select a method to see the replacement ## Topics -### ActionSheetState - -- ``ActionSheetState`` - -### Bindings - -- ``BindableState`` -- ``ViewStore/binding(_:fileID:line:)`` - -### ForEachStore - -- ``ForEachStore/init(_:content:)-34mtj`` -- ``ForEachStore/init(_:id:content:)`` - ### NavigationLinkStore - ``NavigationLinkStore`` - -### WithViewStore - -- ``WithViewStore/Action`` -- ``WithViewStore/State`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/TestStoreDeprecations.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/TestStoreDeprecations.md index 375ac6c7bb2d..9cf4edda8175 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/TestStoreDeprecations.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/TestStoreDeprecations.md @@ -4,41 +4,18 @@ Review unsupported test store APIs and their replacements. ## Overview -Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use instead. +Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use +instead. ## Topics ### Creating a test store -- ``TestStore/init(initialState:reducer:prepareDependencies:file:line:)-5ef6m`` -- ``TestStore/init(initialState:reducer:observe:prepareDependencies:file:line:)`` -- ``TestStore/init(initialState:reducer:observe:send:prepareDependencies:file:line:)`` -- ``TestStore/init(initialState:reducer:environment:file:line:)`` -- ``TestStore/init(initialState:reducer:withDependencies:file:line:)-762zz`` -- ``TestStore/init(initialState:reducer:prepareDependencies:file:line:)-4wg3t`` +- ``TestStore/init(initialState:reducer:withDependencies:file:line:)-8f79s`` -### Configuring a test store +### Nanosecond timeouts -- ``TestStore/environment`` - -### Testing reducers - -- ``TestStore/send(_:assert:file:line:)-30pjj`` -- ``TestStore/receive(_:assert:file:line:)-2nhm0`` -- ``TestStore/receive(_:assert:file:line:)-1bfw4`` -- ``TestStore/receive(_:assert:file:line:)-5o4u3`` -- ``TestStore/assert(_:file:line:)-707lb`` -- ``TestStore/assert(_:file:line:)-4gff7`` -- ``TestStore/LocalState`` -- ``TestStore/LocalAction`` -- ``TestStore/Step`` - -### Methods for skipping tests - -- ``TestStore/skipReceivedActions(strict:file:line:)-3nldt`` -- ``TestStore/skipInFlightEffects(strict:file:line:)-95n5f`` - -### Scoping test stores - -- ``TestStore/scope(state:action:)`` -- ``TestStore/scope(state:)`` +- ``TestStore/finish(timeout:file:line:)-43l4y`` +- ``TestStore/receive(_:timeout:assert:file:line:)-5vi0x`` +- ``TestStore/receive(_:timeout:assert:file:line:)-1t9vb`` +- ``TestStore/receive(_:timeout:assert:file:line:)-8r59i`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ViewStoreDeprecations.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ViewStoreDeprecations.md deleted file mode 100644 index b27c0df60187..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ViewStoreDeprecations.md +++ /dev/null @@ -1,28 +0,0 @@ -# Deprecations - -Review unsupported view store APIs and their replacements. - -## Overview - -Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use instead. - -## Topics - -### Creating a view store - -- ``ViewStore/init(_:removeDuplicates:)`` -- ``ViewStore/init(_:)-1pfeq`` - -### Interacting with Concurrency - -- ``ViewStore/suspend(while:)`` - -### Supporting types - -- ``ViewStore/State-swift.typealias`` -- ``ViewStore/Action`` - -### SwiftUI integration - -- ``ViewStore/subscript(dynamicMember:)-3q4xh`` -- ``ViewStore/binding(keyPath:send:)`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md index ce2384b85ffa..40ad859bec85 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md @@ -4,37 +4,33 @@ ### Creating an effect -- ``EffectPublisher/none`` -- ``EffectPublisher/run(priority:operation:catch:fileID:line:)`` -- ``EffectPublisher/send(_:)`` +- ``none`` +- ``run(priority:operation:catch:fileID:line:)`` +- ``send(_:)`` - ``EffectOf`` - ``TaskResult`` ### Cancellation -- ``EffectPublisher/cancellable(id:cancelInFlight:)-29q60`` -- ``EffectPublisher/cancel(id:)-6hzsl`` -- ``EffectPublisher/cancel(ids:)-1cqqx`` -- ``withTaskCancellation(id:cancelInFlight:operation:)-4dtr6`` +- ``cancellable(id:cancelInFlight:)`` +- ``cancel(id:)`` +- ``withTaskCancellation(id:cancelInFlight:operation:)`` ### Composition -- ``EffectPublisher/map(_:)-yn70`` -- ``EffectPublisher/merge(_:)-45guh`` -- ``EffectPublisher/merge(_:)-3d54p`` - -### Testing - -- ``EffectPublisher/unimplemented(_:)`` - -### Combine integration - -- ``EffectPublisher/publisher(_:)`` +- ``map(_:)`` +- ``merge(_:)-5ai73`` +- ``merge(_:)-8ckqn`` +- ``merge(with:)`` +- ``concatenate(_:)-3iza9`` +- ``concatenate(_:)-4gba2`` +- ``concatenate(with:)`` ### SwiftUI integration -- ``EffectPublisher/animation(_:)`` +- ``animation(_:)`` +- ``transaction(_:)`` -### Deprecations +### Combine integration -- +- ``publisher(_:)`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancel.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancel.md deleted file mode 100644 index 08aaece5db17..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancel.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``ComposableArchitecture/EffectPublisher/cancel(id:)-6hzsl`` - -## Topics - -### Overloads - -- ``cancel(id:)-1c1dw`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancelIds.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancelIds.md deleted file mode 100644 index cec3c002fc74..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancelIds.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``ComposableArchitecture/EffectPublisher/cancel(ids:)-8gan2`` - -## Topics - -### Overloads - -- ``cancel(ids:)-1cqqx`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancellable.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancellable.md deleted file mode 100644 index b4eca283dc86..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancellable.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``ComposableArchitecture/EffectPublisher/cancellable(id:cancelInFlight:)-499iv`` - -## Topics - -### Overloads - -- ``cancellable(id:cancelInFlight:)-17skv`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectRun.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectRun.md index 229ed938fccb..45201eee12d9 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectRun.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectRun.md @@ -1,4 +1,4 @@ -# ``ComposableArchitecture/EffectPublisher/run(priority:operation:catch:fileID:line:)`` +# ``ComposableArchitecture/Effect/run(priority:operation:catch:fileID:line:)`` ## Topics diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectSend.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectSend.md index 132f94b7f0ac..4fcc9e525705 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectSend.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectSend.md @@ -1,7 +1,7 @@ -# ``ComposableArchitecture/EffectPublisher/send(_:)`` +# ``ComposableArchitecture/Effect/send(_:)`` ## Topics ### Animating actions -- ``EffectPublisher/send(_:animation:)`` +- ``Effect/send(_:animation:)`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Reduce.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Reduce.md index cec319de85c0..8a34d8fb30b5 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Reduce.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Reduce.md @@ -4,12 +4,8 @@ ### Creating a reducer -- ``init(_:)-17fld`` +- ``init(_:)-6xl6k`` ### Type erased reducers - ``init(_:)-9kwa6`` - -### Deprecations - -- diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocol.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Reducer.md similarity index 85% rename from Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocol.md rename to Sources/ComposableArchitecture/Documentation.docc/Extensions/Reducer.md index 71fa0f4f2ab8..45adf41e58eb 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocol.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Reducer.md @@ -4,14 +4,14 @@ ### Implementing a reducer -- ``reduce(into:action:)-4zl56`` +- ``reduce(into:action:)-1t2ri`` - ``State`` - ``Action`` - ``Effect`` ### Reducer composition -- ``body-swift.property-8lumc`` +- ``body-swift.property`` - ``Body-swift.typealias`` - ``ReducerBuilder`` @@ -40,7 +40,3 @@ ### Supporting types - ``ReducerOf`` - -### Deprecations - -- diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerBuilder.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerBuilder.md index 8ce2e5cd8fdb..e3a26c25eb23 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerBuilder.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerBuilder.md @@ -4,7 +4,6 @@ ### Building reducers -- ``ReducerBuilderOf`` - ``buildExpression(_:)`` - ``buildBlock(_:)`` - ``buildPartialBlock(first:)`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocolForEach.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerForEach.md similarity index 100% rename from Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocolForEach.md rename to Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerForEach.md diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocolIfLet.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerlIfLet.md similarity index 100% rename from Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocolIfLet.md rename to Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerlIfLet.md diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Store.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Store.md index 0dcaf7087734..e1a7bf10036d 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Store.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Store.md @@ -18,6 +18,9 @@ ### Sending actions - ``send(_:)`` +- ``send(_:animation:)`` +- ``send(_:transaction:)`` +- ``StoreTask`` ### Combine integration @@ -26,7 +29,3 @@ ### UIKit integration - ``ifLet(then:else:)`` - -### Deprecations - -- diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/StoreScope.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/StoreScope.md index d278210e6793..e9eb8dd0117b 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/StoreScope.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/StoreScope.md @@ -5,5 +5,3 @@ ### Overloads - ``scope(state:action:)-hei8`` -- ``stateless`` -- ``actionless`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/SwitchStore.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/SwitchStore.md index 23225870057e..7f320605eda9 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/SwitchStore.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/SwitchStore.md @@ -5,4 +5,3 @@ ### Building Content - ``CaseLet`` -- ``Default`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md index 80e46713dd23..2d776120d373 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md @@ -4,9 +4,7 @@ ### Creating a test store -- ``init(initialState:reducer:withDependencies:file:line:)-t6cu`` -- ``init(initialState:reducer:observe:withDependencies:file:line:)`` -- ``init(initialState:reducer:observe:send:withDependencies:file:line:)`` +- ``init(initialState:reducer:withDependencies:file:line:)-3zio1`` ### Configuring a test store @@ -17,11 +15,11 @@ ### Testing a reducer -- ``send(_:assert:file:line:)-1ax61`` -- ``receive(_:timeout:assert:file:line:)-1rwdd`` -- ``receive(_:timeout:assert:file:line:)-8xkqt`` -- ``receive(_:timeout:assert:file:line:)-2ju31`` -- ``assert(_:file:line:)-21bdg`` +- ``send(_:assert:file:line:)`` +- ``receive(_:timeout:assert:file:line:)-5awso`` +- ``receive(_:timeout:assert:file:line:)-6m8t6`` +- ``receive(_:timeout:assert:file:line:)-7md3m`` +- ``assert(_:file:line:)`` - ``finish(timeout:file:line:)-53gi5`` - ``TestStoreTask`` @@ -32,7 +30,9 @@ ### Accessing state -While the most common way of interacting with a test store's state is via its ``send(_:assert:file:line:)-1ax61`` and ``receive(_:timeout:assert:file:line:)-1rwdd`` methods, you may also access it directly throughout a test. +While the most common way of interacting with a test store's state is via its +``send(_:assert:file:line:)`` and ``receive(_:timeout:assert:file:line:)-5awso`` methods, you may +also access it directly throughout a test. - ``state`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStoreDependencies.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStoreDependencies.md new file mode 100644 index 000000000000..accbfced4a48 --- /dev/null +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStoreDependencies.md @@ -0,0 +1,8 @@ +# ``ComposableArchitecture/TestStore/dependencies`` + +## Topics + +### Configuring exhaustivity + +- ``withDependencies(_:operation:)-3x2vc`` +- ``withDependencies(_:operation:)-61in2`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStoreExhaustivity.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStoreExhaustivity.md index 9090dcd22295..a357671e4b46 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStoreExhaustivity.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStoreExhaustivity.md @@ -5,3 +5,5 @@ ### Configuring exhaustivity - ``Exhaustivity`` +- ``withExhaustivity(_:operation:)-9psu7`` +- ``withExhaustivity(_:operation:)-1mhu4`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/ViewStore.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/ViewStore.md index 2ebf9bfe47f0..d5a1d77e379a 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/ViewStore.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/ViewStore.md @@ -4,11 +4,10 @@ ### Creating a view store -- ``init(_:observe:send:removeDuplicates:)`` -- ``init(_:observe:removeDuplicates:)`` -- ``init(_:observe:send:)`` -- ``init(_:observe:)`` -- ``init(_:)-4il0f`` +- ``init(_:observe:send:removeDuplicates:)-9mg12`` +- ``init(_:observe:removeDuplicates:)-4f9j5`` +- ``init(_:observe:send:)-1m32f`` +- ``init(_:observe:)-3ak1y`` - ``ViewStoreOf`` ### Accessing state @@ -21,7 +20,6 @@ - ``send(_:)`` - ``send(_:while:)`` - ``yield(while:)`` -- ``ViewStoreTask`` ### SwiftUI integration @@ -30,7 +28,8 @@ - ``send(_:transaction:)`` - - ``objectWillChange-5oies`` - -### Deprecations - -- +- ``init(_:observe:send:removeDuplicates:)-9v9l0`` +- ``init(_:observe:removeDuplicates:)-81c6d`` +- ``init(_:observe:send:)-4hzhi`` +- ``init(_:observe:)-96hm5`` +- ``subscript(dynamicMember:)-3q4xh`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithTaskCancellation.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithTaskCancellation.md deleted file mode 100644 index 2b8db23dd49e..000000000000 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithTaskCancellation.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``ComposableArchitecture/withTaskCancellation(id:cancelInFlight:operation:)-4dtr6`` - -## Topics - -### Overloads - -- ``withTaskCancellation(id:cancelInFlight:operation:)-88kxz`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithViewStore.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithViewStore.md index f53f60bef2ef..76d70774ca09 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithViewStore.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithViewStore.md @@ -6,8 +6,8 @@ ### Creating a view -- ``init(_:observe:content:file:line:)`` +- ``init(_:observe:content:file:line:)-8g15l`` ### Debugging view updates -- ``debug(_:)`` +- ``_printChanges(_:)`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithViewStoreInit.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithViewStoreInit.md index 4a842133311b..c4f668b8b24b 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithViewStoreInit.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/WithViewStoreInit.md @@ -1,9 +1,16 @@ -# ``ComposableArchitecture/WithViewStore/init(_:observe:content:file:line:)`` +# ``ComposableArchitecture/WithViewStore/init(_:observe:content:file:line:)-8g15l`` ## Topics ### Overloads -- ``WithViewStore/init(_:observe:removeDuplicates:content:file:line:)`` -- ``WithViewStore/init(_:observe:send:content:file:line:)`` -- ``WithViewStore/init(_:observe:send:removeDuplicates:content:file:line:)`` +- ``WithViewStore/init(_:observe:removeDuplicates:content:file:line:)-7y5bp`` +- ``WithViewStore/init(_:observe:send:content:file:line:)-5d0z5`` +- ``WithViewStore/init(_:observe:send:removeDuplicates:content:file:line:)-dheh`` + +### Bindings + +- ``WithViewStore/init(_:observe:content:file:line:)-4gpoj`` +- ``WithViewStore/init(_:observe:removeDuplicates:content:file:line:)-1zbzi`` +- ``WithViewStore/init(_:observe:send:content:file:line:)-3r7aq`` +- ``WithViewStore/init(_:observe:send:removeDuplicates:content:file:line:)-4izbr`` diff --git a/Sources/ComposableArchitecture/Documentation.docc/Resources/01-homepage.png b/Sources/ComposableArchitecture/Documentation.docc/Resources/01-homepage.png index 8a7edd5003e4d4fcc663a271793912dcb11b5ab0..def1d67224f688cf7ded4d483b6fac6e8bd3cec3 100644 GIT binary patch literal 14195 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Ξ5l|0+;PQ8&&2*RIXPOm_rqk!6kbY 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

6PrSg$d^v&@+AU!BGZhxv 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 ze619W Note: The ``ComposableArchitecture/TestStore/send(_:assert:file:line:)-1ax61`` method on + > Note: The ``ComposableArchitecture/TestStore/send(_:assert:file:line:)`` method on the test store is async because most features involve asynchronous side effects, and the test store using the async context to track those effects. @@ -146,7 +146,7 @@ While this test does pass, it also isn't asserting on any of the timer behavior. We would like to assert that after some time a `timerTick` action is sent into the system and causes the `count` to increment. This can be done by using the - ``ComposableArchitecture/TestStore/receive(_:timeout:assert:file:line:)-1rwdd`` method on + ``ComposableArchitecture/TestStore/receive(_:timeout:assert:file:line:)-5awso`` method on test store to assert that you expect to receive an action, and describe how state mutates upon receiving that action. diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/02-MultipleDestinations/02-02-MultipleDestinations.tutorial b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/02-MultipleDestinations/02-02-MultipleDestinations.tutorial index 2651b9b192fd..a5b53073dc4c 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/02-MultipleDestinations/02-02-MultipleDestinations.tutorial +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/02-MultipleDestinations/02-02-MultipleDestinations.tutorial @@ -153,7 +153,7 @@ } @Step { - Implement the ``ComposableArchitecture/Reducer/body-swift.property-8lumc`` of the reducer. + Implement the ``ComposableArchitecture/Reducer/body-swift.property`` of the reducer. @Code(name: "ContactsFeatures.swift", file: 02-02-02-code-0005) } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/04-NavigationStacks/02-04-NavigationStacks.tutorial b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/04-NavigationStacks/02-04-NavigationStacks.tutorial index 2023de58292e..0adaee667ee4 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/04-NavigationStacks/02-04-NavigationStacks.tutorial +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/02-Navigation/04-NavigationStacks/02-04-NavigationStacks.tutorial @@ -104,7 +104,7 @@ stack. We will also handle the `.path` case in the reducer and return - ``ComposableArchitecture/EffectPublisher/none`` for now. + ``ComposableArchitecture/Effect/none`` for now. @Code(name: "ContactsFeature.swift", file: 02-04-02-code-0001) } diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/MeetComposableArchitecture.tutorial b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/MeetComposableArchitecture.tutorial index 243fd26bc4b8..a2755d163f49 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/MeetComposableArchitecture.tutorial +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/MeetTheComposableArchitecture/MeetComposableArchitecture.tutorial @@ -9,6 +9,8 @@ Explore the basics of creating a new feature in the Composable Architecture, layering on side effects, and writing a complete test suite for the feature. + @Image(source: "01-homepage.png") + @TutorialReference(tutorial: "doc:01-01-YourFirstFeature") @TutorialReference(tutorial: "doc:01-02-AddingSideEffects") @TutorialReference(tutorial: "doc:01-03-TestingYourFeature") @@ -18,6 +20,8 @@ Learn how to model your domains for navigation from parent feature to child feature, and how to do so in a concise manner using optionals, enums and collections. + @Image(source: "02-homepage.png") + @TutorialReference(tutorial: "doc:02-01-YourFirstPresentation") @TutorialReference(tutorial: "doc:02-02-MultipleDestinations") @TutorialReference(tutorial: "doc:02-03-TestingPresentation") @@ -39,9 +43,9 @@ There are multiple places online to discuss the Composable Architecture with others that are also using the library. + - [Join the community Slack](http://pointfree.co/slack-invite) - [Discuss on GitHub](https://github.com/pointfreeco/swift-composable-architecture/discussions) - [Discuss on Swift Forums](https://forums.swift.org/c/related-projects/swift-composable-architecture/61) - - [Join the community Slack](http://pointfree.co/slack-invite) - [Message us on Twitter](http://twitter.com/pointfreeco) - [Message us on Mastodon](http://hachyderm.io/@pointfreeco) } @@ -53,6 +57,7 @@ - [Case studies](https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/CaseStudies) - [Search](https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/Search) - [Speech Recognition](https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/SpeechRecognition) + - [Standups](https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/Standups) - [TicTacToe](https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/TicTacToe) - [Todos](https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/Todos) - [Voice memos](https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/VoiceMemos) diff --git a/Sources/ComposableArchitecture/Effect.swift b/Sources/ComposableArchitecture/Effect.swift index ca7aa4b54c6f..9e1e7a8c00bd 100644 --- a/Sources/ComposableArchitecture/Effect.swift +++ b/Sources/ComposableArchitecture/Effect.swift @@ -3,60 +3,11 @@ import Foundation import SwiftUI import XCTestDynamicOverlay -/// This type is deprecated in favor of ``Effect``. See its documentation for more information. -@available( - iOS, - deprecated: 9999, - message: - """ - 'EffectPublisher' has been deprecated in favor of 'Effect'. - - You are encouraged to use `Effect` to model the output of your reducers, and to use Swift concurrency to model asynchrony in dependencies. - - See the migration roadmap for more information: https://github.com/pointfreeco/swift-composable-architecture/discussions/1477 - """ -) -@available( - macOS, - deprecated: 9999, - message: - """ - 'EffectPublisher' has been deprecated in favor of 'Effect'. - - You are encouraged to use `Effect` to model the output of your reducers, and to use Swift concurrency to model asynchrony in dependencies. - - See the migration roadmap for more information: https://github.com/pointfreeco/swift-composable-architecture/discussions/1477 - """ -) -@available( - tvOS, - deprecated: 9999, - message: - """ - 'EffectPublisher' has been deprecated in favor of 'Effect'. - - You are encouraged to use `Effect` to model the output of your reducers, and to use Swift concurrency to model asynchrony in dependencies. - - See the migration roadmap for more information: https://github.com/pointfreeco/swift-composable-architecture/discussions/1477 - """ -) -@available( - watchOS, - deprecated: 9999, - message: - """ - 'EffectPublisher' has been deprecated in favor of 'Effect'. - - You are encouraged to use `Effect` to model the output of your reducers, and to use Swift concurrency to model asynchrony in dependencies. - - See the migration roadmap for more information: https://github.com/pointfreeco/swift-composable-architecture/discussions/1477 - """ -) -public struct EffectPublisher { +public struct Effect { @usableFromInline enum Operation { case none - case publisher(AnyPublisher) + case publisher(AnyPublisher) case run(TaskPriority? = nil, @Sendable (_ send: Send) async -> Void) } @@ -82,51 +33,18 @@ public struct EffectPublisher { /// ```swift /// let effect: EffectOf /// ``` -public typealias EffectOf = EffectPublisher +public typealias EffectOf = Effect // MARK: - Creating Effects -extension EffectPublisher { +extension Effect { /// An effect that does nothing and completes immediately. Useful for situations where you must /// return an effect, but you don't need to do anything. @inlinable public static var none: Self { Self(operation: .none) } -} - -/// A type that encapsulates a unit of work that can be run in the outside world, and can feed -/// actions back to the ``Store``. -/// -/// Effects are the perfect place to do side effects, such as network requests, saving/loading -/// from disk, creating timers, interacting with dependencies, and more. They are returned from -/// reducers so that the ``Store`` can perform the effects after the reducer is done running. -/// -/// There are 2 distinct ways to create an `Effect`: one using Swift's native concurrency tools, and -/// the other using Apple's Combine framework: -/// -/// * If using Swift's native structured concurrency tools then there is one main way to create an -/// effect: ``EffectPublisher/run(priority:operation:catch:fileID:line:)``. -/// -/// * If using Combine in your application, in particular for the dependencies of your feature -/// then you can create effects by making use of any of Combine's operators, and then erasing the -/// publisher type to ``EffectPublisher`` with either `eraseToEffect` or `catchToEffect`. Note that -/// the Combine interface to ``EffectPublisher`` is considered soft deprecated, and you should -/// eventually port to Swift's native concurrency tools. -/// -/// > Important: The publisher interface to ``Effect`` is considered deprecated, and you should try -/// > converting any uses of that interface to Swift's native concurrency tools. -/// > -/// > Also, ``Store`` is not thread safe, and so all effects must receive values on the same -/// > thread. This is typically the main thread, **and** if the store is being used to drive UI -/// > then it must receive values on the main thread. -/// > -/// > This is only an issue if using the Combine interface of ``EffectPublisher`` as mentioned -/// > above. If you are using Swift's concurrency tools and the `.run` function on ``Effect``, -/// > then threading is automatically handled for you. -public typealias Effect = EffectPublisher -extension EffectPublisher where Failure == Never { /// Wraps an asynchronous unit of work that can emit actions any number of times in an effect. /// /// For example, if you had an async stream in a dependency client: @@ -234,7 +152,7 @@ extension EffectPublisher where Failure == Never { } /// A type that can send actions back into the system when used from -/// ``EffectPublisher/run(priority:operation:catch:fileID:line:)``. +/// ``Effect/run(priority:operation:catch:fileID:line:)``. /// /// This type implements [`callAsFunction`][callAsFunction] so that you invoke it as a function /// rather than calling methods on it: @@ -256,7 +174,7 @@ extension EffectPublisher where Failure == Never { /// defer { send(.finished, animation: .default) } /// ``` /// -/// See ``EffectPublisher/run(priority:operation:catch:fileID:line:)`` for more information on how to +/// See ``Effect/run(priority:operation:catch:fileID:line:)`` for more information on how to /// use this value to construct effects that can emit any number of times in an asynchronous /// context. /// @@ -301,7 +219,7 @@ public struct Send: Sendable { // MARK: - Composing Effects -extension EffectPublisher { +extension Effect { /// Merges a variadic list of effects together into a single effect, which runs the effects at the /// same time. /// @@ -337,8 +255,8 @@ extension EffectPublisher { return Self( operation: .publisher( Publishers.Merge( - EffectPublisherWrapper(self), - EffectPublisherWrapper(other) + _EffectPublisher(self), + _EffectPublisher(other) ) .eraseToAnyPublisher() ) @@ -397,8 +315,8 @@ extension EffectPublisher { return Self( operation: .publisher( Publishers.Concatenate( - prefix: EffectPublisherWrapper(self), - suffix: EffectPublisherWrapper(other) + prefix: _EffectPublisher(self), + suffix: _EffectPublisher(other) ) .eraseToAnyPublisher() ) @@ -427,7 +345,7 @@ extension EffectPublisher { /// - Returns: A publisher that uses the provided closure to map elements from the upstream effect /// to new elements that it then publishes. @inlinable - public func map(_ transform: @escaping (Action) -> T) -> EffectPublisher { + public func map(_ transform: @escaping (Action) -> T) -> Effect { switch self.operation { case .none: return .none @@ -464,117 +382,3 @@ extension EffectPublisher { } } } - -// MARK: - Testing Effects - -extension EffectPublisher { - /// An effect that causes a test to fail if it runs. - /// - /// > Important: This Combine-based interface has been soft-deprecated in favor of Swift - /// > concurrency. Prefer using async functions and `AsyncStream`s directly in your dependencies, - /// > and using `unimplemented` from the [XCTest Dynamic Overlay](gh-xctest-dynamic-overlay) - /// > library to stub in a function that fails when invoked: - /// > - /// > ```swift - /// > struct NumberFactClient { - /// > var fetch: (Int) async throws -> String - /// > } - /// > - /// > extension NumberFactClient: TestDependencyKey { - /// > static let testValue = Self( - /// > fetch: unimplemented( - /// > "\(Self.self).fetch", - /// > placeholder: "Not an interesting number." - /// > ) - /// > } - /// > } - /// > ``` - /// - /// This effect can provide an additional layer of certainty that a tested code path does not - /// execute a particular effect. - /// - /// For example, let's say we have a very simple counter application, where a user can increment - /// and decrement a number. The state and actions are simple enough: - /// - /// ```swift - /// struct CounterState: Equatable { - /// var count = 0 - /// } - /// - /// enum CounterAction: Equatable { - /// case decrementButtonTapped - /// case incrementButtonTapped - /// } - /// ``` - /// - /// Let's throw in a side effect. If the user attempts to decrement the counter below zero, the - /// application should refuse and play an alert sound instead. - /// - /// We can model playing a sound in the environment with an effect: - /// - /// ```swift - /// struct CounterEnvironment { - /// let playAlertSound: () -> EffectPublisher - /// } - /// ``` - /// - /// Now that we've defined the domain, we can describe the logic in a reducer: - /// - /// ```swift - /// let counterReducer = AnyReducer< - /// CounterState, CounterAction, CounterEnvironment - /// > { state, action, environment in - /// switch action { - /// case .decrementButtonTapped: - /// if state > 0 { - /// state.count -= 0 - /// return .none - /// } else { - /// return environment.playAlertSound() - /// .fireAndForget() - /// } - /// - /// case .incrementButtonTapped: - /// state.count += 1 - /// return .none - /// } - /// } - /// ``` - /// - /// Let's say we want to write a test for the increment path. We can see in the reducer that it - /// should never play an alert, so we can configure the environment with an effect that will - /// fail if it ever executes: - /// - /// ```swift - /// @MainActor - /// func testIncrement() async { - /// let store = TestStore( - /// initialState: CounterState(count: 0) - /// reducer: counterReducer, - /// environment: CounterEnvironment( - /// playSound: .unimplemented("playSound") - /// ) - /// ) - /// - /// await store.send(.increment) { - /// $0.count = 1 - /// } - /// } - /// ``` - /// - /// By using an `.unimplemented` effect in our environment we have strengthened the assertion and - /// made the test easier to understand at the same time. We can see, without consulting the - /// reducer itself, that this particular action should not access this effect. - /// - /// [gh-xctest-dynamic-overlay]: http://github.com/pointfreeco/xctest-dynamic-overlay - /// - /// - Parameter prefix: A string that identifies this effect and will prefix all failure - /// messages. - /// - Returns: An effect that causes a test to fail if it runs. - @available(*, deprecated, message: "Call 'unimplemented' from your dependencies, instead.") - public static func unimplemented(_ prefix: String) -> Self { - .fireAndForget { - XCTFail("\(prefix.isEmpty ? "" : "\(prefix) - ")An unimplemented effect ran.") - } - } -} diff --git a/Sources/ComposableArchitecture/Effects/Animation.swift b/Sources/ComposableArchitecture/Effects/Animation.swift index c429d120f996..a65a2148b1b5 100644 --- a/Sources/ComposableArchitecture/Effects/Animation.swift +++ b/Sources/ComposableArchitecture/Effects/Animation.swift @@ -1,7 +1,7 @@ import Combine import SwiftUI -extension EffectPublisher { +extension Effect { /// Wraps the emission of each element with SwiftUI's `withAnimation`. /// /// ```swift diff --git a/Sources/ComposableArchitecture/Effects/Cancellation.swift b/Sources/ComposableArchitecture/Effects/Cancellation.swift index 715bcda6c401..fd91186a8728 100644 --- a/Sources/ComposableArchitecture/Effects/Cancellation.swift +++ b/Sources/ComposableArchitecture/Effects/Cancellation.swift @@ -1,11 +1,11 @@ import Combine import Foundation -extension EffectPublisher { +extension Effect { /// Turns an effect into one that is capable of being canceled. /// /// To turn an effect into a cancellable one you must provide an identifier, which is used in - /// ``EffectPublisher/cancel(id:)-6hzsl`` to identify which in-flight effect should be canceled. + /// ``Effect/cancel(id:)`` to identify which in-flight effect should be canceled. /// Any hashable value can be used for the identifier, such as a string, but you can add a bit of /// protection against typos by defining a new type for the identifier: /// @@ -41,7 +41,7 @@ extension EffectPublisher { () -> Publishers.HandleEvents< Publishers.PrefixUntilOutput< - AnyPublisher, PassthroughSubject + AnyPublisher, PassthroughSubject > > in _cancellablesLock.lock() @@ -99,17 +99,18 @@ extension EffectPublisher { public static func cancel(id: ID) -> Self { let dependencies = DependencyValues._current @Dependency(\.navigationIDPath) var navigationIDPath - return Deferred { () -> Publishers.CompactMap.Publisher, Action> in + // NB: Ideally we'd return a `Deferred` wrapping an `Empty(completeImmediately: true)`, but + // due to a bug in iOS 13.2 that publisher will never complete. The bug was fixed in + // iOS 13.3, but to remain compatible with iOS 13.2 and higher we need to do a little + // trickery to make sure the deferred publisher completes. + return .publisher { () -> Publishers.CompactMap, Action> in DependencyValues.$_current.withValue(dependencies) { _cancellablesLock.sync { _cancellationCancellables.cancel(id: id, path: navigationIDPath) } } - return Just(nil) - .setFailureType(to: Failure.self) - .compactMap { $0 } + return Just(nil).compactMap { $0 } } - .eraseToEffectPublisher() } } diff --git a/Sources/ComposableArchitecture/Effects/EffectActions.swift b/Sources/ComposableArchitecture/Effects/EffectActions.swift index dd147cc15a05..2c01bfb8d4be 100644 --- a/Sources/ComposableArchitecture/Effects/EffectActions.swift +++ b/Sources/ComposableArchitecture/Effects/EffectActions.swift @@ -1,4 +1,4 @@ -extension Effect where Failure == Never { +extension Effect { @_spi(Internals) public var actions: AsyncStream { switch self.operation { diff --git a/Sources/ComposableArchitecture/Effects/Publisher.swift b/Sources/ComposableArchitecture/Effects/Publisher.swift index b8805f4eeb4d..d27292803ed8 100644 --- a/Sources/ComposableArchitecture/Effects/Publisher.swift +++ b/Sources/ComposableArchitecture/Effects/Publisher.swift @@ -1,6 +1,6 @@ import Combine -extension EffectPublisher where Failure == Never { +extension Effect { /// Creates an effect from a Combine publisher. /// /// - Parameter createPublisher: The closure to execute when the effect is performed. @@ -22,466 +22,17 @@ extension EffectPublisher where Failure == Never { } } -@available(*, deprecated) -extension EffectPublisher: Publisher { +public struct _EffectPublisher: Publisher { public typealias Output = Action + public typealias Failure = Never - public func receive( - subscriber: S - ) where S.Input == Action, S.Failure == Failure { - self.publisher.subscribe(subscriber) - } - - var publisher: AnyPublisher { - switch self.operation { - case .none: - return Empty().eraseToAnyPublisher() - case let .publisher(publisher): - return publisher - case let .run(priority, operation): - return .create { subscriber in - let task = Task(priority: priority) { @MainActor in - defer { subscriber.send(completion: .finished) } - #if DEBUG - let isCompleted = LockIsolated(false) - defer { isCompleted.setValue(true) } - #endif - let send = Send { - #if DEBUG - if isCompleted.value { - runtimeWarn( - """ - An action was sent from a completed effect: - - Action: - \(debugCaseOutput($0)) - - Avoid sending actions using the 'send' argument from 'Effect.run' after \ - the effect has completed. This can happen if you escape the 'send' argument in \ - an unstructured context. + let effect: Effect - To fix this, make sure that your 'run' closure does not return until you're \ - done calling 'send'. - """ - ) - } - #endif - subscriber.send($0) - } - await operation(send) - } - return AnyCancellable { - task.cancel() - } - } - } - } -} - -extension EffectPublisher { - /// Initializes an effect that wraps a publisher. - /// - /// > Important: This Combine interface has been soft-deprecated in favor of Swift concurrency. - /// > Prefer performing asynchronous work directly in - /// > ``EffectPublisher/run(priority:operation:catch:fileID:line:)`` by adopting a non-Combine - /// > interface, or by iterating over the publisher's asynchronous sequence of `values`: - /// > - /// > ```swift - /// > return .run { send in - /// > for await value in publisher.values { - /// > send(.response(value)) - /// > } - /// > } - /// > ``` - /// - /// - Parameter publisher: A publisher. - @available( - *, - deprecated, - message: - "Iterate over 'Publisher.values' in an 'Effect.run', instead, or use 'Effect.publisher'." - ) - public init(_ publisher: P) where P.Output == Output, P.Failure == Failure { - self.operation = .publisher(publisher.eraseToAnyPublisher()) - } - - /// Initializes an effect that immediately emits the value passed in. - /// - /// - Parameter value: The value that is immediately emitted by the effect. - @available(*, deprecated, message: "Use 'Effect.send', instead.") - public init(value: Action) { - self.init(Just(value).setFailureType(to: Failure.self)) - } - - /// Initializes an effect that immediately fails with the error passed in. - /// - /// - Parameter error: The error that is immediately emitted by the effect. - @available( - *, - deprecated, - message: "Throw and catch errors directly in an 'Effect.run', instead." - ) - public init(error: Failure) { - // NB: Ideally we'd return a `Fail` publisher here, but due to a bug in iOS 13 that publisher - // can crash when used with certain combinations of operators such as `.retry.catch`. The - // bug was fixed in iOS 14, but to remain compatible with iOS 13 and higher we need to do - // a little trickery to fail in a slightly different way. - self.init( - Deferred { - Future { $0(.failure(error)) } - } - ) - } - - /// Creates an effect that can supply a single value asynchronously in the future. - /// - /// This can be helpful for converting APIs that are callback-based into ones that deal with - /// ``EffectPublisher``s. - /// - /// For example, to create an effect that delivers an integer after waiting a second: - /// - /// ```swift - /// EffectPublisher.future { callback in - /// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - /// callback(.success(42)) - /// } - /// } - /// ``` - /// - /// Note that you can only deliver a single value to the `callback`. If you send more they will be - /// discarded: - /// - /// ```swift - /// EffectPublisher.future { callback in - /// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - /// callback(.success(42)) - /// callback(.success(1729)) // Will not be emitted by the effect - /// } - /// } - /// ``` - /// - /// If you need to deliver more than one value to the effect, you should use the - /// ``EffectPublisher`` initializer that accepts a ``Subscriber`` value. - /// - /// - Parameter attemptToFulfill: A closure that takes a `callback` as an argument which can be - /// used to feed it `Result` values. - @available(*, deprecated, message: "Use 'Effect.run', instead.") - public static func future( - _ attemptToFulfill: @escaping (@escaping (Result) -> Void) -> Void - ) -> Self { - withEscapedDependencies { escaped in - Deferred { - escaped.yield { - Future(attemptToFulfill) - } - }.eraseToEffect() - } - } - - /// Initializes an effect that lazily executes some work in the real world and synchronously sends - /// that data back into the store. - /// - /// For example, to load a user from some JSON on the disk, one can wrap that work in an effect: - /// - /// ```swift - /// EffectPublisher.result { - /// let fileUrl = URL( - /// fileURLWithPath: NSSearchPathForDirectoriesInDomains( - /// .documentDirectory, .userDomainMask, true - /// )[0] - /// ) - /// .appendingPathComponent("user.json") - /// - /// let result = Result { - /// let data = try Data(contentsOf: fileUrl) - /// return try JSONDecoder().decode(User.self, from: $0) - /// } - /// - /// return result - /// } - /// ``` - /// - /// - Parameter attemptToFulfill: A closure encapsulating some work to execute in the real world. - /// - Returns: An effect. - @available(*, deprecated, message: "Use 'Effect.run', instead.") - public static func result(_ attemptToFulfill: @escaping () -> Result) -> Self { - .future { $0(attemptToFulfill()) } - } - - /// Initializes an effect from a callback that can send as many values as it wants, and can send - /// a completion. - /// - /// This initializer is useful for bridging callback APIs, delegate APIs, and manager APIs to the - /// ``EffectPublisher`` type. One can wrap those APIs in an Effect so that its events are sent - /// through the effect, which allows the reducer to handle them. - /// - /// For example, one can create an effect to ask for access to `MPMediaLibrary`. It can start by - /// sending the current status immediately, and then if the current status is `notDetermined` it - /// can request authorization, and once a status is received it can send that back to the effect: - /// - /// ```swift - /// EffectPublisher.run { subscriber in - /// subscriber.send(MPMediaLibrary.authorizationStatus()) - /// - /// guard MPMediaLibrary.authorizationStatus() == .notDetermined else { - /// subscriber.send(completion: .finished) - /// return AnyCancellable {} - /// } - /// - /// MPMediaLibrary.requestAuthorization { status in - /// subscriber.send(status) - /// subscriber.send(completion: .finished) - /// } - /// return AnyCancellable { - /// // Typically clean up resources that were created here, but this effect doesn't - /// // have any. - /// } - /// } - /// ``` - /// - /// - Parameter work: A closure that accepts a ``Subscriber`` value and returns a cancellable. - /// When the ``EffectPublisher`` is completed, the cancellable will be used to clean up any - /// resources created when the effect was started. - @available(*, deprecated, message: "Use the async version of 'Effect.run', instead.") - public static func run( - _ work: @escaping (EffectPublisher.Subscriber) -> Cancellable - ) -> Self { - withEscapedDependencies { escaped in - AnyPublisher.create { subscriber in - escaped.yield { - work(subscriber) - } - } - .eraseToEffect() - } - } - - /// Creates an effect that executes some work in the real world that doesn't need to feed data - /// back into the store. If an error is thrown, the effect will complete and the error will be - /// ignored. - /// - /// - Parameter work: A closure encapsulating some work to execute in the real world. - /// - Returns: An effect. - @available(*, deprecated, message: "Use 'Effect.run { _ in … }', instead.") - public static func fireAndForget(_ work: @escaping () throws -> Void) -> Self { - // NB: Ideally we'd return a `Deferred` wrapping an `Empty(completeImmediately: true)`, but - // due to a bug in iOS 13.2 that publisher will never complete. The bug was fixed in - // iOS 13.3, but to remain compatible with iOS 13.2 and higher we need to do a little - // trickery to make sure the deferred publisher completes. - withEscapedDependencies { escaped in - Deferred { () -> Publishers.CompactMap.Publisher, Action> in - escaped.yield { - try? work() - } - return Just(nil) - .setFailureType(to: Failure.self) - .compactMap { $0 } - } - .eraseToEffect() - } - } -} - -extension EffectPublisher where Failure == Error { - /// Initializes an effect that lazily executes some work in the real world and synchronously sends - /// that data back into the store. - /// - /// For example, to load a user from some JSON on the disk, one can wrap that work in an effect: - /// - /// ```swift - /// EffectPublisher.catching { - /// let fileUrl = URL( - /// fileURLWithPath: NSSearchPathForDirectoriesInDomains( - /// .documentDirectory, .userDomainMask, true - /// )[0] - /// ) - /// .appendingPathComponent("user.json") - /// - /// let data = try Data(contentsOf: fileUrl) - /// return try JSONDecoder().decode(User.self, from: $0) - /// } - /// ``` - /// - /// - Parameter work: A closure encapsulating some work to execute in the real world. - /// - Returns: An effect. - @available( - *, - deprecated, - message: "Throw and catch errors directly in an 'Effect.run', instead." - ) - public static func catching(_ work: @escaping () throws -> Action) -> Self { - .future { $0(Result { try work() }) } - } -} - -extension Publisher { - /// Turns any publisher into an ``EffectPublisher``. - /// - /// This can be useful for when you perform a chain of publisher transformations in a reducer, and - /// you need to convert that publisher to an effect so that you can return it from the reducer: - /// - /// ```swift - /// case .buttonTapped: - /// return fetchUser(id: 1) - /// .filter(\.isAdmin) - /// .eraseToEffect() - /// ``` - /// - /// - Returns: An effect that wraps `self`. - @available( - *, - deprecated, - message: - "Iterate over 'Publisher.values' in an 'Effect.run', instead, or use 'Effect.publisher'." - ) - public func eraseToEffect() -> EffectPublisher { - EffectPublisher(self) - } - - /// Turns any publisher into an ``EffectPublisher``. - /// - /// This is a convenience operator for writing ``EffectPublisher/eraseToEffect()`` followed by - /// ``EffectPublisher/map(_:)-28ghh`. - /// - /// ```swift - /// case .buttonTapped: - /// return fetchUser(id: 1) - /// .filter(\.isAdmin) - /// .eraseToEffect(ProfileAction.adminUserFetched) - /// ``` - /// - /// - Parameters: - /// - transform: A mapping function that converts `Output` to another type. - /// - Returns: An effect that wraps `self` after mapping `Output` values. - @available( - *, - deprecated, - message: - "Iterate over 'Publisher.values' in an 'Effect.run', instead, or use 'Effect.publisher'." - ) - public func eraseToEffect( - _ transform: @escaping (Output) -> T - ) -> EffectPublisher { - self.map( - withEscapedDependencies { escaped in - { action in - escaped.yield { - transform(action) - } - } - } - ) - .eraseToEffect() - } - - /// Turns any publisher into an ``Effect`` that cannot fail by wrapping its output and failure - /// in a result. - /// - /// This can be useful when you are working with a failing API but want to deliver its data to an - /// action that handles both success and failure. - /// - /// ```swift - /// case .buttonTapped: - /// return self.apiClient.fetchUser(id: 1) - /// .catchToEffect() - /// .map(ProfileAction.userResponse) - /// ``` - /// - /// - Returns: An effect that wraps `self`. - @available( - *, deprecated, - message: - "Iterate over 'Publisher.values' in an 'Effect.run', instead, or use 'Effect.publisher'." - ) - public func catchToEffect() -> Effect> { - self.catchToEffect { $0 } - } - - /// Turns any publisher into an ``Effect`` that cannot fail by wrapping its output and failure - /// into a result and then applying passed in function to it. - /// - /// This is a convenience operator for writing ``EffectPublisher/eraseToEffect()`` followed by - /// ``EffectPublisher/map(_:)-28ghh`. - /// - /// ```swift - /// case .buttonTapped: - /// return self.apiClient.fetchUser(id: 1) - /// .catchToEffect(ProfileAction.userResponse) - /// ``` - /// - /// - Parameters: - /// - transform: A mapping function that converts `Result` to another type. - /// - Returns: An effect that wraps `self`. - @available( - *, - deprecated, - message: - "Iterate over 'Publisher.values' in an 'Effect.run', instead, or use 'Effect.publisher'." - ) - public func catchToEffect( - _ transform: @escaping (Result) -> T - ) -> Effect { - return - self - .map( - withEscapedDependencies { escaped in - { action in - escaped.yield { - transform(.success(action)) - } - } - } - ) - .catch { Just(transform(.failure($0))) } - .eraseToEffect() - } - - /// Turns any publisher into an ``EffectPublisher`` for any output and failure type by ignoring - /// all output and any failure. - /// - /// This is useful for times you want to fire off an effect but don't want to feed any data back - /// into the system. It can automatically promote an effect to your reducer's domain. - /// - /// ```swift - /// case .buttonTapped: - /// return analyticsClient.track("Button Tapped") - /// .fireAndForget() - /// ``` - /// - /// - Parameters: - /// - outputType: An output type. - /// - failureType: A failure type. - /// - Returns: An effect that never produces output or errors. - @available( - *, - deprecated, - message: "Iterate over 'Publisher.values' in an 'Effect.run', instead, or use Effect.publisher" - ) - public func fireAndForget( - outputType: NewOutput.Type = NewOutput.self, - failureType: NewFailure.Type = NewFailure.self - ) -> EffectPublisher { - return - self - .flatMap { _ in Empty() } - .catch { _ in Empty() } - .eraseToEffect() - } -} - -@usableFromInline -internal struct EffectPublisherWrapper: Publisher { - @usableFromInline typealias Output = Action - - let effect: EffectPublisher - - @usableFromInline - init(_ effect: EffectPublisher) { + public init(_ effect: Effect) { self.effect = effect } - @usableFromInline - func receive( + public func receive( subscriber: S ) where S.Input == Action, S.Failure == Failure { self.publisher.subscribe(subscriber) @@ -497,8 +48,7 @@ internal struct EffectPublisherWrapper: Publisher { return .create { subscriber in let task = Task(priority: priority) { @MainActor in defer { subscriber.send(completion: .finished) } - let send = Send { subscriber.send($0) } - await operation(send) + await operation(Send { subscriber.send($0) }) } return AnyCancellable { task.cancel() @@ -507,10 +57,3 @@ internal struct EffectPublisherWrapper: Publisher { } } } - -extension Publisher { - @usableFromInline - func eraseToEffectPublisher() -> EffectPublisher { - EffectPublisher(operation: .publisher(self.eraseToAnyPublisher())) - } -} diff --git a/Sources/ComposableArchitecture/Effects/Publisher/Debouncing.swift b/Sources/ComposableArchitecture/Effects/Publisher/Debouncing.swift deleted file mode 100644 index e17c555e7730..000000000000 --- a/Sources/ComposableArchitecture/Effects/Publisher/Debouncing.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Combine - -@available( - *, - deprecated, - message: "Use 'withTaskCancellation(id: _, cancelInFlight: true)' in 'Effect.run', instead." -) -extension EffectPublisher { - /// Turns an effect into one that can be debounced. - /// - /// To turn an effect into a debounce-able one you must provide an identifier, which is used to - /// determine which in-flight effect should be canceled in order to start a new effect. Any - /// hashable value can be used for the identifier, such as a string, but you can add a bit of - /// protection against typos by defining a new type that conforms to `Hashable`, such as an empty - /// struct: - /// - /// ```swift - /// case let .textChanged(text): - /// enum CancelID { case search } - /// - /// return self.apiClient.search(text) - /// .debounce(id: CancelID.search, for: 0.5, scheduler: self.mainQueue) - /// .map(Action.searchResponse) - /// ``` - /// - /// - Parameters: - /// - id: The effect's identifier. - /// - dueTime: The duration you want to debounce for. - /// - scheduler: The scheduler you want to deliver the debounced output to. - /// - options: Scheduler options that customize the effect's delivery of elements. - /// - Returns: An effect that publishes events only after a specified time elapses. - public func debounce( - id: ID, - for dueTime: S.SchedulerTimeType.Stride, - scheduler: S, - options: S.SchedulerOptions? = nil - ) -> Self { - switch self.operation { - case .none: - return .none - case .publisher, .run: - return Self( - operation: .publisher( - Just(()) - .setFailureType(to: Failure.self) - .delay(for: dueTime, scheduler: scheduler, options: options) - .flatMap { self.publisher.receive(on: scheduler) } - .eraseToAnyPublisher() - ) - ) - .cancellable(id: id, cancelInFlight: true) - } - } - - /// Turns an effect into one that can be debounced. - /// - /// A convenience for calling ``EffectPublisher/debounce(id:for:scheduler:options:)-1xdnj`` with a - /// static type as the effect's unique identifier. - /// - /// - Parameters: - /// - id: A unique type identifying the effect. - /// - dueTime: The duration you want to debounce for. - /// - scheduler: The scheduler you want to deliver the debounced output to. - /// - options: Scheduler options that customize the effect's delivery of elements. - /// - Returns: An effect that publishes events only after a specified time elapses. - public func debounce( - id: Any.Type, - for dueTime: S.SchedulerTimeType.Stride, - scheduler: S, - options: S.SchedulerOptions? = nil - ) -> Self { - self.debounce(id: ObjectIdentifier(id), for: dueTime, scheduler: scheduler, options: options) - } -} diff --git a/Sources/ComposableArchitecture/Effects/Publisher/Deferring.swift b/Sources/ComposableArchitecture/Effects/Publisher/Deferring.swift deleted file mode 100644 index 0b91e3024fb0..000000000000 --- a/Sources/ComposableArchitecture/Effects/Publisher/Deferring.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Combine - -extension EffectPublisher { - /// Returns an effect that will be executed after given `dueTime`. - /// - /// ```swift - /// case let .textChanged(text): - /// return self.apiClient.search(text) - /// .deferred(for: 0.5, scheduler: self.mainQueue) - /// .map(Action.searchResponse) - /// ``` - /// - /// - Parameters: - /// - dueTime: The duration you want to defer for. - /// - scheduler: The scheduler you want to deliver the defer output to. - /// - options: Scheduler options that customize the effect's delivery of elements. - /// - Returns: An effect that will be executed after `dueTime` - @available( - *, deprecated, message: "Use 'clock/scheduler.sleep' in 'Effect.task' or 'Effect.run', instead." - ) - public func deferred( - for dueTime: S.SchedulerTimeType.Stride, - scheduler: S, - options: S.SchedulerOptions? = nil - ) -> Self { - switch self.operation { - case .none: - return .none - case .publisher, .run: - return Self( - operation: .publisher( - Just(()) - .setFailureType(to: Failure.self) - .delay(for: dueTime, scheduler: scheduler, options: options) - .flatMap { self.publisher.receive(on: scheduler) } - .eraseToAnyPublisher() - ) - ) - } - } -} diff --git a/Sources/ComposableArchitecture/Effects/Publisher/Throttling.swift b/Sources/ComposableArchitecture/Effects/Publisher/Throttling.swift deleted file mode 100644 index dd99b5615e26..000000000000 --- a/Sources/ComposableArchitecture/Effects/Publisher/Throttling.swift +++ /dev/null @@ -1,95 +0,0 @@ -import Combine -import Dispatch -import Foundation - -@available(*, deprecated) -extension EffectPublisher { - /// Throttles an effect so that it only publishes one output per given interval. - /// - /// - Parameters: - /// - id: The effect's identifier. - /// - interval: The interval at which to find and emit the most recent element, expressed in - /// the time system of the scheduler. - /// - scheduler: The scheduler you want to deliver the throttled output to. - /// - latest: A boolean value that indicates whether to publish the most recent element. If - /// `false`, the publisher emits the first element received during the interval. - /// - Returns: An effect that emits either the most-recent or first element received during the - /// specified interval. - public func throttle( - id: ID, - for interval: S.SchedulerTimeType.Stride, - scheduler: S, - latest: Bool - ) -> Self { - switch self.operation { - case .none: - return .none - case .publisher, .run: - return self.receive(on: scheduler) - .flatMap { value -> AnyPublisher in - throttleLock.lock() - defer { throttleLock.unlock() } - - guard let throttleTime = throttleTimes[id] as! S.SchedulerTimeType? else { - throttleTimes[id] = scheduler.now - throttleValues[id] = nil - return Just(value).setFailureType(to: Failure.self).eraseToAnyPublisher() - } - - let value = latest ? value : (throttleValues[id] as! Action? ?? value) - throttleValues[id] = value - - guard throttleTime.distance(to: scheduler.now) < interval else { - throttleTimes[id] = scheduler.now - throttleValues[id] = nil - return Just(value).setFailureType(to: Failure.self).eraseToAnyPublisher() - } - - return Just(value) - .delay( - for: scheduler.now.distance(to: throttleTime.advanced(by: interval)), - scheduler: scheduler - ) - .handleEvents( - receiveOutput: { _ in - throttleLock.sync { - throttleTimes[id] = scheduler.now - throttleValues[id] = nil - } - } - ) - .setFailureType(to: Failure.self) - .eraseToAnyPublisher() - } - .eraseToEffect() - .cancellable(id: id, cancelInFlight: true) - } - } - - /// Throttles an effect so that it only publishes one output per given interval. - /// - /// A convenience for calling ``EffectPublisher/throttle(id:for:scheduler:latest:)-3gibe`` with a - /// static type as the effect's unique identifier. - /// - /// - Parameters: - /// - id: The effect's identifier. - /// - interval: The interval at which to find and emit the most recent element, expressed in - /// the time system of the scheduler. - /// - scheduler: The scheduler you want to deliver the throttled output to. - /// - latest: A boolean value that indicates whether to publish the most recent element. If - /// `false`, the publisher emits the first element received during the interval. - /// - Returns: An effect that emits either the most-recent or first element received during the - /// specified interval. - public func throttle( - id: Any.Type, - for interval: S.SchedulerTimeType.Stride, - scheduler: S, - latest: Bool - ) -> Self { - self.throttle(id: ObjectIdentifier(id), for: interval, scheduler: scheduler, latest: latest) - } -} - -var throttleTimes: [AnyHashable: Any] = [:] -var throttleValues: [AnyHashable: Any] = [:] -let throttleLock = NSRecursiveLock() diff --git a/Sources/ComposableArchitecture/Effects/Publisher/Timer.swift b/Sources/ComposableArchitecture/Effects/Publisher/Timer.swift deleted file mode 100644 index 330b869d4743..000000000000 --- a/Sources/ComposableArchitecture/Effects/Publisher/Timer.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Combine -import CombineSchedulers - -extension EffectPublisher where Failure == Never { - /// Returns an effect that repeatedly emits the current time of the given scheduler on the given - /// interval. - /// - /// While it is possible to use Foundation's `Timer.publish(every:tolerance:on:in:options:)` API - /// to create a timer in the Composable Architecture, it is not advisable. This API only allows - /// creating a timer on a run loop, which means when writing tests you will need to explicitly - /// wait for time to pass in order to see how the effect evolves in your feature. - /// - /// In the Composable Architecture we test time-based effects like this by using the - /// `TestScheduler`, which allows us to explicitly and immediately advance time forward so that - /// we can see how effects emit. However, because `Timer.publish` takes a concrete `RunLoop` as - /// its scheduler, we can't substitute in a `TestScheduler` during tests`. - /// - /// That is why we provide `Effect.timer`. It allows you to create a timer that works with any - /// scheduler, not just a run loop, which means you can use a `DispatchQueue` or `RunLoop` when - /// running your live app, but use a `TestScheduler` in tests. - /// - /// To start and stop a timer in your feature you can create the timer effect from an action - /// and then use the ``EffectPublisher/cancel(id:)-6hzsl`` effect to stop the timer: - /// - /// ```swift - /// struct Feature: Reducer { - /// struct State { var count = 0 } - /// enum Action { case startButtonTapped, stopButtonTapped, timerTicked } - /// @Dependency(\.mainQueue) var mainQueue - /// struct TimerID: Hashable {} - /// - /// func reduce(into state: inout State, action: Action) -> Effect { - /// switch action { - /// case .startButtonTapped: - /// return Effect.timer(id: TimerID(), every: 1, on: self.mainQueue) - /// .map { _ in .timerTicked } - /// - /// case .stopButtonTapped: - /// return .cancel(id: TimerID()) - /// - /// case .timerTicked: - /// state.count += 1 - /// return .none - /// } - /// } - /// ``` - /// - /// Then to test the timer in this feature you can use a test scheduler to advance time: - /// - /// ```swift - /// @MainActor - /// func testTimer() async { - /// let mainQueue = DispatchQueue.test - /// - /// let store = TestStore(initialState: Feature.State()) { - /// Feature() - /// } withDependencies: { - /// $0.mainQueue = mainQueue.eraseToAnyScheduler() - /// } - /// - /// await store.send(.startButtonTapped) - /// - /// await mainQueue.advance(by: .seconds(1)) - /// await store.receive(.timerTicked) { $0.count = 1 } - /// - /// await mainQueue.advance(by: .seconds(5)) - /// await store.receive(.timerTicked) { $0.count = 2 } - /// await store.receive(.timerTicked) { $0.count = 3 } - /// await store.receive(.timerTicked) { $0.count = 4 } - /// await store.receive(.timerTicked) { $0.count = 5 } - /// await store.receive(.timerTicked) { $0.count = 6 } - /// - /// await store.send(.stopButtonTapped) - /// } - /// ``` - /// - /// - Note: This effect is only meant to be used with features built in the Composable - /// Architecture, and returned from a reducer. If you want a testable alternative to - /// Foundation's `Timer.publish` you can use the publisher `Publishers.Timer` that is included - /// in this library via the - /// [`CombineSchedulers`](https://github.com/pointfreeco/combine-schedulers) module. - /// - /// - Parameters: - /// - id: The effect's identifier. - /// - interval: The time interval on which to publish events. For example, a value of `0.5` - /// publishes an event approximately every half-second. - /// - scheduler: The scheduler on which the timer runs. - /// - tolerance: The allowed timing variance when emitting events. Defaults to `nil`, which - /// allows any variance. - /// - options: Scheduler options passed to the timer. Defaults to `nil`. - @available(*, deprecated, message: "Use 'clock/scheduler.timer' in an 'Effect.run', instead.") - public static func timer( - id: ID, - every interval: S.SchedulerTimeType.Stride, - tolerance: S.SchedulerTimeType.Stride? = nil, - on scheduler: S, - options: S.SchedulerOptions? = nil - ) -> Self where S.SchedulerTimeType == Action { - Publishers.Timer(every: interval, tolerance: tolerance, scheduler: scheduler, options: options) - .autoconnect() - .setFailureType(to: Failure.self) - .eraseToEffect() - .cancellable(id: id, cancelInFlight: true) - } - - /// Returns an effect that repeatedly emits the current time of the given scheduler on the given - /// interval. - /// - /// A convenience for calling ``EffectPublisher/timer(id:every:tolerance:on:options:)-6yv2m`` with - /// a static type as the effect's unique identifier. - /// - /// - Parameters: - /// - id: A unique type identifying the effect. - /// - interval: The time interval on which to publish events. For example, a value of `0.5` - /// publishes an event approximately every half-second. - /// - scheduler: The scheduler on which the timer runs. - /// - tolerance: The allowed timing variance when emitting events. Defaults to `nil`, which - /// allows any variance. - /// - options: Scheduler options passed to the timer. Defaults to `nil`. - @available(*, deprecated, message: "Use 'clock/scheduler.timer' in an 'Effect.run', instead.") - public static func timer( - id: Any.Type, - every interval: S.SchedulerTimeType.Stride, - tolerance: S.SchedulerTimeType.Stride? = nil, - on scheduler: S, - options: S.SchedulerOptions? = nil - ) -> Self where S.SchedulerTimeType == Action { - self.timer( - id: ObjectIdentifier(id), - every: interval, - tolerance: tolerance, - on: scheduler, - options: options - ) - } -} diff --git a/Sources/ComposableArchitecture/Effects/TaskResult.swift b/Sources/ComposableArchitecture/Effects/TaskResult.swift index 3dc079620243..30de268e5faa 100644 --- a/Sources/ComposableArchitecture/Effects/TaskResult.swift +++ b/Sources/ComposableArchitecture/Effects/TaskResult.swift @@ -31,9 +31,9 @@ import XCTestDynamicOverlay /// } /// ``` /// -/// And finally you can use ``EffectPublisher/run(priority:operation:catch:fileID:line:)`` to -/// construct an effect in the reducer that invokes the `numberFact` endpoint and wraps its response -/// in a ``TaskResult`` by using its catching initializer, ``TaskResult/init(catching:)``: +/// And finally you can use ``Effect/run(priority:operation:catch:fileID:line:)`` to construct an +/// effect in the reducer that invokes the `numberFact` endpoint and wraps its response in a +/// ``TaskResult`` by using its catching initializer, ``TaskResult/init(catching:)``: /// /// ```swift /// case .factButtonTapped: diff --git a/Sources/ComposableArchitecture/Internal/Create.swift b/Sources/ComposableArchitecture/Internal/Create.swift index 53a932a8f18a..736e1b615082 100644 --- a/Sources/ComposableArchitecture/Internal/Create.swift +++ b/Sources/ComposableArchitecture/Internal/Create.swift @@ -103,25 +103,27 @@ final class DemandBuffer: @unchecked Sendable { } } -extension AnyPublisher { +extension AnyPublisher where Failure == Never { private init( - _ callback: @escaping (EffectPublisher.Subscriber) -> Cancellable + _ callback: @escaping (Effect.Subscriber) -> Cancellable ) { self = Publishers.Create(callback: callback).eraseToAnyPublisher() } static func create( - _ factory: @escaping (EffectPublisher.Subscriber) -> Cancellable + _ factory: @escaping (Effect.Subscriber) -> Cancellable ) -> AnyPublisher { AnyPublisher(factory) } } extension Publishers { - fileprivate class Create: Publisher { - private let callback: (EffectPublisher.Subscriber) -> Cancellable + fileprivate class Create: Publisher { + typealias Failure = Never - init(callback: @escaping (EffectPublisher.Subscriber) -> Cancellable) { + private let callback: (Effect.Subscriber) -> Cancellable + + init(callback: @escaping (Effect.Subscriber) -> Cancellable) { self.callback = callback } @@ -133,12 +135,12 @@ extension Publishers { extension Publishers.Create { fileprivate final class Subscription: Combine.Subscription - where Downstream.Input == Output, Downstream.Failure == Failure { + where Downstream.Input == Output, Downstream.Failure == Never { private let buffer: DemandBuffer private var cancellable: Cancellable? init( - callback: @escaping (EffectPublisher.Subscriber) -> Cancellable, + callback: @escaping (Effect.Subscriber) -> Cancellable, downstream: Downstream ) { self.buffer = DemandBuffer(subscriber: downstream) @@ -165,18 +167,18 @@ extension Publishers.Create { extension Publishers.Create.Subscription: CustomStringConvertible { var description: String { - return "Create.Subscription<\(Output.self), \(Failure.self)>" + return "Create.Subscription<\(Output.self)>" } } -extension EffectPublisher { - public struct Subscriber { +extension Effect { + struct Subscriber { private let _send: (Action) -> Void - private let _complete: (Subscribers.Completion) -> Void + private let _complete: (Subscribers.Completion) -> Void init( send: @escaping (Action) -> Void, - complete: @escaping (Subscribers.Completion) -> Void + complete: @escaping (Subscribers.Completion) -> Void ) { self._send = send self._complete = complete @@ -186,7 +188,7 @@ extension EffectPublisher { self._send(value) } - public func send(completion: Subscribers.Completion) { + public func send(completion: Subscribers.Completion) { self._complete(completion) } } diff --git a/Sources/ComposableArchitecture/Internal/Deprecations.swift b/Sources/ComposableArchitecture/Internal/Deprecations.swift index 8cf9b6ecb069..0224b24958f9 100644 --- a/Sources/ComposableArchitecture/Internal/Deprecations.swift +++ b/Sources/ComposableArchitecture/Internal/Deprecations.swift @@ -1,1092 +1,10 @@ -import CasePaths -import Combine -import SwiftUI -import XCTestDynamicOverlay - -// MARK: - Deprecated after 1.0.0: - -@available(iOS, deprecated: 9999, renamed: "Effect") -@available(macOS, deprecated: 9999, renamed: "Effect") -@available(tvOS, deprecated: 9999, renamed: "Effect") -@available(watchOS, deprecated: 9999, renamed: "Effect") +@available(*, unavailable, renamed: "Effect") public typealias EffectTask = Effect -@available(iOS, deprecated: 9999, renamed: "Reducer") -@available(macOS, deprecated: 9999, renamed: "Reducer") -@available(tvOS, deprecated: 9999, renamed: "Reducer") -@available(watchOS, deprecated: 9999, renamed: "Reducer") +@available(*, unavailable, renamed: "Reducer") public typealias ReducerProtocol = Reducer #if swift(>=5.7.1) - @available(iOS, deprecated: 9999, renamed: "ReducerOf") - @available(macOS, deprecated: 9999, renamed: "ReducerOf") - @available(tvOS, deprecated: 9999, renamed: "ReducerOf") - @available(watchOS, deprecated: 9999, renamed: "ReducerOf") + @available(*, unavailable, renamed: "ReducerOf") public typealias ReducerProtocolOf = Reducer #endif - -// MARK: - Deprecated after 0.54.1 - -extension WithViewStore where ViewState == Void, Content: View { - @available(*, deprecated, message: "Use 'store.send(action)' directly on the 'Store' instead.") - public init( - _ store: Store, - @ViewBuilder content: @escaping (ViewStore) -> Content, - file: StaticString = #fileID, - line: UInt = #line - ) { - self.init(store, removeDuplicates: ==, content: content, file: file, line: line) - } -} - -extension EffectPublisher { - @available(*, deprecated, message: "Use 'Effect.merge([.cancel(id: …), …])' instead.") - public static func cancel(ids: [AnyHashable]) -> Self { - .merge(ids.map(EffectPublisher.cancel(id:))) - } -} - -// MARK: - Deprecated after 0.52.0 - -extension WithViewStore { - @available(*, deprecated, renamed: "_printChanges(_:)") - public func debug(_ prefix: String = "") -> Self { - self._printChanges(prefix) - } -} - -extension EffectPublisher where Failure == Never { - @available(iOS, deprecated: 9999, message: "Use 'Effect.run' and pass the action to 'send'.") - @available(macOS, deprecated: 9999, message: "Use 'Effect.run' and pass the action to 'send'.") - @available(tvOS, deprecated: 9999, message: "Use 'Effect.run' and pass the action to 'send'.") - @available(watchOS, deprecated: 9999, message: "Use 'Effect.run' and pass the action to 'send'.") - public static func task( - priority: TaskPriority? = nil, - operation: @escaping @Sendable () async throws -> Action, - catch handler: (@Sendable (Error) async -> Action)? = nil, - fileID: StaticString = #fileID, - line: UInt = #line - ) -> Self { - withEscapedDependencies { escaped in - Self( - operation: .run(priority) { send in - await escaped.yield { - do { - try await send(operation()) - } catch is CancellationError { - return - } catch { - guard let handler = handler else { - #if DEBUG - var errorDump = "" - customDump(error, to: &errorDump, indent: 4) - runtimeWarn( - """ - An "Effect.task" returned from "\(fileID):\(line)" threw an unhandled error. … - - \(errorDump) - - All non-cancellation errors must be explicitly handled via the "catch" \ - parameter on "Effect.task", or via a "do" block. - """ - ) - #endif - return - } - await send(handler(error)) - } - } - } - ) - } - } - - @available(iOS, deprecated: 9999, message: "Use 'Effect.run' and ignore 'send' instead.") - @available(macOS, deprecated: 9999, message: "Use 'Effect.run' and ignore 'send' instead.") - @available(tvOS, deprecated: 9999, message: "Use 'Effect.run' and ignore 'send' instead.") - @available(watchOS, deprecated: 9999, message: "Use 'Effect.run' and ignore 'send' instead.") - public static func fireAndForget( - priority: TaskPriority? = nil, - _ work: @escaping @Sendable () async throws -> Void - ) -> Self { - Self.run(priority: priority) { _ in try? await work() } - } -} - -extension Store { - @available(iOS, deprecated: 9999, message: "Pass a closure as the reducer.") - @available(macOS, deprecated: 9999, message: "Pass a closure as the reducer.") - @available(tvOS, deprecated: 9999, message: "Pass a closure as the reducer.") - @available(watchOS, deprecated: 9999, message: "Pass a closure as the reducer.") - public convenience init( - initialState: @autoclosure () -> R.State, - reducer: R, - prepareDependencies: ((inout DependencyValues) -> Void)? = nil - ) where R.State == State, R.Action == Action { - if let prepareDependencies = prepareDependencies { - self.init( - initialState: withDependencies(prepareDependencies) { initialState() }, - reducer: reducer.transformDependency(\.self, transform: prepareDependencies), - mainThreadChecksEnabled: true - ) - } else { - self.init( - initialState: initialState(), - reducer: reducer, - mainThreadChecksEnabled: true - ) - } - } -} - -extension TestStore { - @available( - *, deprecated, - message: "Pass a closure as the reducer." - ) - public convenience init( - initialState: @autoclosure () -> State, - reducer: R, - prepareDependencies: (inout DependencyValues) -> Void = { _ in }, - file: StaticString = #file, - line: UInt = #line - ) - where - R.State == State, - R.Action == Action, - State == ScopedState, - State: Equatable, - Action == ScopedAction, - Environment == Void - { - self.init( - initialState: initialState(), - reducer: reducer, - observe: { $0 }, - send: { $0 }, - prepareDependencies: prepareDependencies, - file: file, - line: line - ) - } - - @available(*, deprecated, message: "Pass a closure as the reducer.") - public convenience init( - initialState: @autoclosure () -> State, - reducer: R, - observe toScopedState: @escaping (State) -> ScopedState, - prepareDependencies: (inout DependencyValues) -> Void = { _ in }, - file: StaticString = #file, - line: UInt = #line - ) - where - R.State == State, - R.Action == Action, - ScopedState: Equatable, - Action == ScopedAction, - Environment == Void - { - self.init( - initialState: initialState(), - reducer: reducer, - observe: toScopedState, - send: { $0 }, - prepareDependencies: prepareDependencies, - file: file, - line: line - ) - } - - @available( - *, deprecated, - message: "Test the reducer domain directly. To test view state and actions, write a unit test." - ) - public convenience init( - initialState: @autoclosure () -> State, - reducer: R, - observe toScopedState: @escaping (State) -> ScopedState, - send fromScopedAction: @escaping (ScopedAction) -> Action, - prepareDependencies: (inout DependencyValues) -> Void = { _ in }, - file: StaticString = #file, - line: UInt = #line - ) - where - R.State == State, - R.Action == Action, - ScopedState: Equatable, - Environment == Void - { - self.init( - initialState: initialState(), - reducer: { reducer }, - observe: toScopedState, - send: fromScopedAction, - withDependencies: prepareDependencies, - file: file, - line: line - ) - } - - @available(*, deprecated, message: "State must be equatable to perform assertions.") - public convenience init( - initialState: @autoclosure () -> State, - reducer: R, - prepareDependencies: (inout DependencyValues) -> Void = { _ in }, - file: StaticString = #file, - line: UInt = #line - ) - where - R.State == State, - R.Action == Action, - State == ScopedState, - Action == ScopedAction, - Environment == Void - { - self.init( - initialState: initialState(), - reducer: { reducer }, - withDependencies: prepareDependencies, - file: file, - line: line - ) - } -} - -extension Store { - @available( - *, - deprecated, - message: - """ - 'Store.scope' requires an explicit 'action' transform and is intended to be used to transform a store of a parent domain into a store of a child domain. - - When transforming store state into view state, use the 'observe' parameter when constructing a view store. - """ - ) - public func scope( - state toChildState: @escaping (State) -> ChildState - ) -> Store { - self.scope(state: toChildState, action: { $0 }) - } -} - -extension EffectPublisher { - @available( - *, - deprecated, - message: - """ - Types defined for cancellation may be compiled out of release builds in Swift and are unsafe to use. Use a hashable value, instead, e.g. define a timer cancel identifier as 'enum CancelID { case timer }' and call 'Effect.cancellable(id: CancelID.timer)'. - """ - ) - public func cancellable(id: Any.Type, cancelInFlight: Bool = false) -> Self { - self.cancellable(id: ObjectIdentifier(id), cancelInFlight: cancelInFlight) - } - - @available( - *, - deprecated, - message: - """ - Types defined for cancellation may be compiled out of release builds in Swift and are unsafe to use. Use a hashable value, instead, e.g. define a timer cancel identifier as 'enum CancelID { case timer }' and call 'Effect.cancellable(id: CancelID.timer)'. - """ - ) - public static func cancel(id: Any.Type) -> Self { - .cancel(id: ObjectIdentifier(id)) - } - - @available( - *, - deprecated, - message: - """ - Types defined for cancellation may be compiled out of release builds in Swift and are unsafe to use. Use a hashable value, instead, e.g. define a timer cancel identifier as 'enum CancelID { case timer }' and call 'Effect.cancel(id: CancelID.timer)'. - """ - ) - public static func cancel(ids: [Any.Type]) -> Self { - .merge(ids.map(EffectPublisher.cancel(id:))) - } -} - -@available( - *, - deprecated, - message: - """ - Types defined for cancellation may be compiled out of release builds in Swift and are unsafe to use. Use a hashable value, instead, e.g. define a timer cancel identifier as 'enum CancelID { case timer }' and call 'withTaskCancellation(id: CancelID.timer)'. - """ -) -public func withTaskCancellation( - id: Any.Type, - cancelInFlight: Bool = false, - operation: @Sendable @escaping () async throws -> T -) async rethrows -> T { - try await withTaskCancellation( - id: ObjectIdentifier(id), - cancelInFlight: cancelInFlight, - operation: operation - ) -} - -extension Task where Success == Never, Failure == Never { - @available( - *, - deprecated, - message: - """ - Types defined for cancellation may be compiled out of release builds in Swift and are unsafe to use. Use a hashable value, instead, e.g. define a timer cancel identifier as 'enum CancelID { case timer }' and call 'Effect.cancel(id: CancelID.timer)'. - """ - ) - public static func cancel(id: Any.Type) { - self.cancel(id: ObjectIdentifier(id)) - } -} - -// MARK: - Deprecated after 0.49.2 - -@available( - *, - deprecated, - message: "Use 'ReducerBuilder<_, _>' with explicit 'State' and 'Action' generics, instead." -) -public typealias ReducerBuilderOf = ReducerBuilder - -// NB: As of Swift 5.7, property wrapper deprecations are not diagnosed, so we may want to keep this -// deprecation around for now: -// https://github.com/apple/swift/issues/63139 -@available(*, deprecated, renamed: "BindingState") -public typealias BindableState = BindingState - -// MARK: - Deprecated after 0.47.2: - -extension ActorIsolated { - @available( - *, - deprecated, - message: "Use the non-async version of 'withValue'." - ) - public func withValue( - _ operation: @Sendable (inout Value) async throws -> T - ) async rethrows -> T { - var value = self.value - defer { self.value = value } - return try await operation(&value) - } -} - -// MARK: - Deprecated after 0.45.0: - -@available( - *, - deprecated, - message: "Pass 'TextState' to the 'SwiftUI.Text' initializer, instead, e.g., 'Text(textState)'." -) -extension TextState: View { - public var body: some View { - Text(self) - } -} - -// MARK: - Deprecated after 0.41.0: - -extension ViewStore { - @available(*, deprecated, renamed: "ViewState") - public typealias State = ViewState - - @available(*, deprecated, renamed: "ViewAction") - public typealias Action = ViewAction -} - -extension Reducer { - @available(*, deprecated, renamed: "_printChanges") - @warn_unqualified_access - public func debug() -> _PrintChangesReducer { - self._printChanges() - } -} - -extension ReducerBuilder { - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ - Reducer bodies should return 'some Reducer' instead of 'Reduce'. - """ - ) - @inlinable - public static func buildFinalResult(_ reducer: R) -> Reduce - where R.State == State, R.Action == Action { - Reduce(reducer) - } - - @_disfavoredOverload - @inlinable - public static func buildFinalResult(_ reducer: Reduce) -> Reduce { - reducer - } -} - -// MARK: - Deprecated after 0.39.1: - -extension WithViewStore { - @available(*, deprecated, renamed: "ViewState") - public typealias State = ViewState - - @available(*, deprecated, renamed: "ViewAction") - public typealias Action = ViewAction -} - -// MARK: - Deprecated after 0.39.0: - -extension CaseLet { - @available(*, deprecated, renamed: "EnumState") - public typealias GlobalState = EnumState - - @available(*, deprecated, renamed: "EnumAction") - public typealias GlobalAction = EnumAction - - @available(*, deprecated, renamed: "CaseState") - public typealias LocalState = CaseState - - @available(*, deprecated, renamed: "CaseAction") - public typealias LocalAction = CaseAction -} - -extension TestStore { - @available(*, deprecated, renamed: "ScopedState") - public typealias LocalState = ScopedState - - @available(*, deprecated, renamed: "ScopedAction") - public typealias LocalAction = ScopedAction -} - -// MARK: - Deprecated after 0.38.2: - -extension EffectPublisher { - @available(*, deprecated) - public var upstream: AnyPublisher { - self.publisher - } -} - -extension EffectPublisher where Failure == Error { - @_disfavoredOverload - @available( - *, - deprecated, - message: "Use the non-failing version of 'Effect.task'" - ) - public static func task( - priority: TaskPriority? = nil, - operation: @escaping @Sendable () async throws -> Action - ) -> Self { - Deferred>> { - let subject = PassthroughSubject() - let task = Task(priority: priority) { @MainActor in - do { - try Task.checkCancellation() - let output = try await operation() - try Task.checkCancellation() - subject.send(output) - subject.send(completion: .finished) - } catch is CancellationError { - subject.send(completion: .finished) - } catch { - subject.send(completion: .failure(error)) - } - } - return subject.handleEvents(receiveCancel: task.cancel) - } - .eraseToEffect() - } -} - -/// Initializes a store from an initial state, a reducer, and an environment, and the main thread -/// check is disabled for all interactions with this store. -/// -/// - Parameters: -/// - initialState: The state to start the application in. -/// - reducer: The reducer that powers the business logic of the application. -/// - environment: The environment of dependencies for the application. -@available( - *, deprecated, - message: - """ - If you use this initializer, please open a discussion on GitHub and let us know how: https://github.com/pointfreeco/swift-composable-architecture/discussions/new - """ -) -extension Store { - public static func unchecked( - initialState: State, - reducer: AnyReducer, - environment: Environment - ) -> Self { - self.init( - initialState: initialState, - reducer: Reduce(reducer, environment: environment), - mainThreadChecksEnabled: false - ) - } -} - -// MARK: - Deprecated after 0.38.0: - -extension EffectPublisher { - @available(*, deprecated, renamed: "unimplemented") - public static func failing(_ prefix: String) -> Self { - self.unimplemented(prefix) - } -} - -// MARK: - Deprecated after 0.36.0: - -extension ViewStore { - @available(*, deprecated, renamed: "yield(while:)") - @MainActor - public func suspend(while predicate: @escaping (ViewState) -> Bool) async { - await self.yield(while: predicate) - } -} - -// MARK: - Deprecated after 0.34.0: - -extension EffectPublisher { - @available( - *, - deprecated, - message: - """ - Using a variadic list is no longer supported. Use an array of identifiers instead. For more \ - on this change, see: https://github.com/pointfreeco/swift-composable-architecture/pull/1041 - """ - ) - @_disfavoredOverload - public static func cancel(ids: AnyHashable...) -> Self { - .cancel(ids: ids) - } -} - -// MARK: - Deprecated after 0.31.0: - -@available(*, deprecated) -extension AnyReducer { - @available( - *, - deprecated, - message: "'pullback' no longer takes a 'breakpointOnNil' argument" - ) - public func pullback( - state toChildState: CasePath, - action toChildAction: CasePath, - environment toChildEnvironment: @escaping (ParentEnvironment) -> Environment, - breakpointOnNil: Bool, - file: StaticString = #fileID, - line: UInt = #line - ) -> AnyReducer { - self.pullback( - state: toChildState, - action: toChildAction, - environment: toChildEnvironment, - file: file, - line: line - ) - } - - @available( - *, - deprecated, - message: "'optional' no longer takes a 'breakpointOnNil' argument" - ) - public func optional( - breakpointOnNil: Bool, - file: StaticString = #fileID, - line: UInt = #line - ) -> AnyReducer { - self.optional(file: file, line: line) - } - - @available( - *, - deprecated, - message: "'forEach' no longer takes a 'breakpointOnNil' argument" - ) - public func forEach( - state toElementsState: WritableKeyPath>, - action toElementAction: CasePath, - environment toElementEnvironment: @escaping (ParentEnvironment) -> Environment, - breakpointOnNil: Bool, - file: StaticString = #fileID, - line: UInt = #line - ) -> AnyReducer { - self.forEach( - state: toElementsState, - action: toElementAction, - environment: toElementEnvironment, - file: file, - line: line - ) - } - - @available( - *, - deprecated, - message: "'forEach' no longer takes a 'breakpointOnNil' argument" - ) - public func forEach( - state toElementsState: WritableKeyPath, - action toElementAction: CasePath, - environment toElementEnvironment: @escaping (ParentEnvironment) -> Environment, - breakpointOnNil: Bool, - file: StaticString = #fileID, - line: UInt = #line - ) -> AnyReducer { - self.forEach( - state: toElementsState, - action: toElementAction, - environment: toElementEnvironment, - file: file, - line: line - ) - } -} - -// MARK: - Deprecated after 0.29.0: - -extension TestStore where ScopedState: Equatable, Action: Equatable { - @available( - *, deprecated, message: "Use 'TestStore.send' and 'TestStore.receive' directly, instead." - ) - public func assert( - _ steps: Step..., - file: StaticString = #file, - line: UInt = #line - ) { - assert(steps, file: file, line: line) - } - - @available( - *, deprecated, message: "Use 'TestStore.send' and 'TestStore.receive' directly, instead." - ) - public func assert( - _ steps: [Step], - file: StaticString = #file, - line: UInt = #line - ) { - - func assert(step: Step) { - switch step.type { - case let .send(action, updateStateToExpectedResult): - self.send(action, assert: updateStateToExpectedResult, file: step.file, line: step.line) - - case let .receive(expectedAction, updateStateToExpectedResult): - self.receive( - expectedAction, assert: updateStateToExpectedResult, file: step.file, line: step.line - ) - - case let .environment(work): - if !self.reducer.receivedActions.isEmpty { - var actions = "" - customDump(self.reducer.receivedActions.map(\.action), to: &actions) - XCTFail( - """ - Must handle \(self.reducer.receivedActions.count) received \ - action\(self.reducer.receivedActions.count == 1 ? "" : "s") before performing this \ - work: … - - Unhandled actions: \(actions) - """, - file: step.file, line: step.line - ) - } - do { - try work(&self.environment) - } catch { - XCTFail("Threw error: \(error)", file: step.file, line: step.line) - } - - case let .do(work): - if !self.reducer.receivedActions.isEmpty { - var actions = "" - customDump(self.reducer.receivedActions.map(\.action), to: &actions) - XCTFail( - """ - Must handle \(self.reducer.receivedActions.count) received \ - action\(self.reducer.receivedActions.count == 1 ? "" : "s") before performing this \ - work: … - - Unhandled actions: \(actions) - """, - file: step.file, line: step.line - ) - } - do { - try work() - } catch { - XCTFail("Threw error: \(error)", file: step.file, line: step.line) - } - - case let .sequence(subSteps): - subSteps.forEach(assert(step:)) - } - } - - steps.forEach(assert(step:)) - - self.completed() - } - - public struct Step { - fileprivate let type: StepType - fileprivate let file: StaticString - fileprivate let line: UInt - - private init( - _ type: StepType, - file: StaticString = #file, - line: UInt = #line - ) { - self.type = type - self.file = file - self.line = line - } - - @available(*, deprecated, message: "Call 'TestStore.send' directly, instead.") - public static func send( - _ action: ScopedAction, - file: StaticString = #file, - line: UInt = #line, - _ update: ((inout ScopedState) throws -> Void)? = nil - ) -> Step { - Step(.send(action, update), file: file, line: line) - } - - @available(*, deprecated, message: "Call 'TestStore.receive' directly, instead.") - public static func receive( - _ action: Action, - file: StaticString = #file, - line: UInt = #line, - _ update: ((inout ScopedState) throws -> Void)? = nil - ) -> Step { - Step(.receive(action, update), file: file, line: line) - } - - @available(*, deprecated, message: "Mutate 'TestStore.environment' directly, instead.") - public static func environment( - file: StaticString = #file, - line: UInt = #line, - _ update: @escaping (inout Environment) throws -> Void - ) -> Step { - Step(.environment(update), file: file, line: line) - } - - @available(*, deprecated, message: "Perform this work directly in your test, instead.") - public static func `do`( - file: StaticString = #file, - line: UInt = #line, - _ work: @escaping () throws -> Void - ) -> Step { - Step(.do(work), file: file, line: line) - } - - @available(*, deprecated, message: "Perform this work directly in your test, instead.") - public static func sequence( - _ steps: [Step], - file: StaticString = #file, - line: UInt = #line - ) -> Step { - Step(.sequence(steps), file: file, line: line) - } - - @available(*, deprecated, message: "Perform this work directly in your test, instead.") - public static func sequence( - _ steps: Step..., - file: StaticString = #file, - line: UInt = #line - ) -> Step { - Step(.sequence(steps), file: file, line: line) - } - - fileprivate indirect enum StepType { - case send(ScopedAction, ((inout ScopedState) throws -> Void)?) - case receive(Action, ((inout ScopedState) throws -> Void)?) - case environment((inout Environment) throws -> Void) - case `do`(() throws -> Void) - case sequence([Step]) - } - } -} - -// MARK: - Deprecated after 0.27.1: - -@available(iOS 13, *) -@available(macOS 12, *) -@available(tvOS 13, *) -@available(watchOS 6, *) -@available(*, deprecated, renamed: "ConfirmationDialogState") -public typealias ActionSheetState = ConfirmationDialogState - -extension View { - @available(iOS 13, *) - @available(macOS 12, *) - @available(tvOS 13, *) - @available(watchOS 6, *) - @available(*, deprecated, renamed: "confirmationDialog") - public func actionSheet( - _ store: Store?, Action>, - dismiss: Action - ) -> some View { - self.confirmationDialog(store, dismiss: dismiss) - } -} - -extension Store { - @available( - *, deprecated, - message: - """ - If you use this method, please open a discussion on GitHub and let us know how: \ - https://github.com/pointfreeco/swift-composable-architecture/discussions/new - """ - ) - public func publisherScope( - state toChildState: @escaping (AnyPublisher) -> P, - action fromChildAction: @escaping (ChildAction) -> Action - ) -> AnyPublisher, Never> - where P.Output == ChildState, P.Failure == Never { - - func extractChildState(_ state: State) -> ChildState? { - var childState: ChildState? - _ = toChildState(Just(state).eraseToAnyPublisher()) - .sink { childState = $0 } - return childState - } - - return toChildState(self.state.eraseToAnyPublisher()) - .map { childState in - let childStore = Store( - initialState: childState, - reducer: .init { childState, childAction, _ in - let task = self.send(fromChildAction(childAction), originatingFrom: nil) - childState = extractChildState(self.state.value) ?? childState - if let task = task { - return .run { _ in await task.cancellableValue } - } else { - return .none - } - }, - environment: () - ) - - childStore.parentCancellable = self.state - .sink { [weak childStore] state in - guard let childStore = childStore else { return } - childStore.state.value = extractChildState(state) ?? childStore.state.value - } - return childStore - } - .eraseToAnyPublisher() - } - - @available( - *, deprecated, - message: - """ - If you use this method, please open a discussion on GitHub and let us know how: \ - https://github.com/pointfreeco/swift-composable-architecture/discussions/new - """ - ) - public func publisherScope( - state toChildState: @escaping (AnyPublisher) -> P - ) -> AnyPublisher, Never> - where P.Output == ChildState, P.Failure == Never { - self.publisherScope(state: toChildState, action: { $0 }) - } -} - -// MARK: - Deprecated after 0.25.0: - -extension BindingAction { - @available( - *, deprecated, - message: - """ - For improved safety, bindable properties must now be wrapped explicitly in 'BindingState', \ - and accessed via key paths to that 'BindingState', like '\\.$value' - """ - ) - public static func set( - _ keyPath: WritableKeyPath, - _ value: Value - ) -> Self { - .init( - keyPath: keyPath, - set: { $0[keyPath: keyPath] = value }, - value: AnySendable(value), - valueIsEqualTo: { $0 as? Value == value } - ) - } - - @available( - *, deprecated, - message: - """ - For improved safety, bindable properties must now be wrapped explicitly in 'BindingState', \ - and accessed via key paths to that 'BindingState', like '\\.$value' - """ - ) - public static func ~= ( - keyPath: WritableKeyPath, - bindingAction: Self - ) -> Bool { - keyPath == bindingAction.keyPath - } -} - -@available(*, deprecated) -extension AnyReducer { - @available( - *, deprecated, - message: - """ - 'Reducer.binding()' no longer takes an explicit extract function and instead the reducer's \ - 'Action' type must conform to 'BindableAction' - """ - ) - public func binding(action toBindingAction: @escaping (Action) -> BindingAction?) -> Self { - Self { state, action, environment in - toBindingAction(action)?.set(&state) - return self.run(&state, action, environment) - } - } -} - -extension ViewStore { - @available( - *, deprecated, - message: - """ - For improved safety, bindable properties must now be wrapped explicitly in 'BindingState'. \ - Bindings are now derived via 'ViewStore.binding' with a key path to that 'BindingState' \ - (for example, 'viewStore.binding(\\.$value)'). For dynamic member lookup to be available, \ - the view store's 'Action' type must also conform to 'BindableAction'. - """ - ) - @MainActor - public func binding( - keyPath: WritableKeyPath, - send action: @escaping (BindingAction) -> ViewAction - ) -> Binding { - self.binding( - get: { $0[keyPath: keyPath] }, - send: { action(.set(keyPath, $0)) } - ) - } -} - -// MARK: - Deprecated after 0.20.0: - -@available(*, deprecated) -extension AnyReducer { - @available(*, deprecated, message: "Use the 'IdentifiedArray'-based version, instead.") - public func forEach( - state toElementsState: WritableKeyPath, - action toElementAction: CasePath, - environment toElementEnvironment: @escaping (ParentEnvironment) -> Environment, - breakpointOnNil: Bool = true, - file: StaticString = #file, - fileID: StaticString = #fileID, - line: UInt = #line - ) -> AnyReducer { - .init { parentState, parentAction, parentEnvironment in - guard let (index, action) = toElementAction.extract(from: parentAction) else { - return .none - } - if index >= parentState[keyPath: toElementsState].endIndex { - runtimeWarn( - """ - A "forEach" reducer at "\(fileID):\(line)" received an action when state contained no \ - element at that index. … - - Action: - \(debugCaseOutput(action)) - Index: - \(index) - - This is generally considered an application logic error, and can happen for a few \ - reasons: - - • This "forEach" reducer was combined with or run from another reducer that removed \ - the element at this index when it handled this action. To fix this make sure that this \ - "forEach" reducer is run before any other reducers that can move or remove elements \ - from state. This ensures that "forEach" reducers can handle their actions for the \ - element at the intended index. - - • An in-flight effect emitted this action while state contained no element at this \ - index. While it may be perfectly reasonable to ignore this action, you may want to \ - cancel the associated effect when moving or removing an element. If your "forEach" \ - reducer returns any long-living effects, you should use the identifier-based "forEach" \ - instead. - - • This action was sent to the store while its state contained no element at this index \ - To fix this make sure that actions for this reducer can only be sent to a view store \ - when its state contains an element at this index. In SwiftUI applications, use \ - "ForEachStore". - """ - ) - return .none - } - return self.run( - &parentState[keyPath: toElementsState][index], - action, - toElementEnvironment(parentEnvironment) - ) - .map { toElementAction.embed((index, $0)) } - } - } -} - -extension ForEachStore { - @available(*, deprecated, message: "Use the 'IdentifiedArray'-based version, instead.") - public init( - _ store: Store, - id: KeyPath, - @ViewBuilder content: @escaping (Store) -> EachContent - ) - where - Data == [EachState], - Content == WithViewStore< - [ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent> - > - { - let data = store.state.value - self.data = data - self.content = WithViewStore(store, observe: { $0.map { $0[keyPath: id] } }) { viewStore in - ForEach(Array(viewStore.state.enumerated()), id: \.element) { index, _ in - content( - store.scope( - state: { index < $0.endIndex ? $0[index] : data[index] }, - action: { (index, $0) } - ) - ) - } - } - } - - @available(*, deprecated, message: "Use the 'IdentifiedArray'-based version, instead.") - public init( - _ store: Store, - @ViewBuilder content: @escaping (Store) -> EachContent - ) - where - Data == [EachState], - Content == WithViewStore< - [ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent> - >, - EachState: Identifiable, - EachState.ID == ID - { - self.init(store, id: \.id, content: content) - } -} diff --git a/Sources/ComposableArchitecture/Reducer.swift b/Sources/ComposableArchitecture/Reducer.swift index 93dc85044985..63aeffa57e83 100644 --- a/Sources/ComposableArchitecture/Reducer.swift +++ b/Sources/ComposableArchitecture/Reducer.swift @@ -21,7 +21,7 @@ /// /// The logic of your feature is implemented by mutating the feature's current state when an action /// comes into the system. This is most easily done by implementing the -/// ``Reducer/reduce(into:action:)-4zl56`` method of the protocol. +/// ``Reducer/reduce(into:action:)-1t2ri`` method of the protocol. /// /// ```swift /// struct Feature: Reducer { @@ -44,7 +44,7 @@ /// The `reduce` method's first responsibility is to mutate the feature's current state given an /// action. Its second responsibility is to return effects that will be executed asynchronously /// and feed their data back into the system. Currently `Feature` does not need to run any effects, -/// and so ``EffectPublisher/none`` is returned. +/// and so ``Effect/none`` is returned. /// /// If the feature does need to do effectful work, then more would need to be done. For example, /// suppose the feature has the ability to start and stop a timer, and with each tick of the timer @@ -103,16 +103,16 @@ /// That is the basics of implementing a feature as a conformance to ``Reducer``. There are /// actually two ways to define a reducer: /// -/// 1. You can either implement the ``reduce(into:action:)-4zl56`` method, as shown above, which +/// 1. You can either implement the ``reduce(into:action:)-1t2ri`` method, as shown above, which /// is given direct mutable access to application ``State`` whenever an ``Action`` is fed into /// the system, and returns an ``Effect`` that can communicate with the outside world and /// feed additional ``Action``s back into the system. /// -/// 2. Or you can implement the ``body-swift.property-8lumc`` property, which combines one or +/// 2. Or you can implement the ``body-swift.property`` property, which combines one or /// more reducers together. /// /// At most one of these requirements should be implemented. If a conformance implements both -/// requirements, only ``reduce(into:action:)-4zl56`` will be called by the ``Store``. If your +/// requirements, only ``reduce(into:action:)-1t2ri`` will be called by the ``Store``. If your /// reducer assembles a body from other reducers _and_ has additional business logic it needs to /// layer onto the feature, introduce this logic into the body instead, either with ``Reduce``: /// @@ -142,10 +142,9 @@ /// } /// ``` /// -/// If you are implementing a custom reducer operator that transforms an existing reducer, -/// _always_ invoke the ``reduce(into:action:)-4zl56`` method, never the -/// ``body-swift.property-8lumc``. For example, this operator that logs all actions sent to the -/// reducer: +/// If you are implementing a custom reducer operator that transforms an existing reducer, _always_ +/// invoke the ``reduce(into:action:)-1t2ri`` method, never the ``body-swift.property``. For +/// example, this operator that logs all actions sent to the reducer: /// /// ```swift /// extension Reducer { @@ -174,19 +173,19 @@ public protocol Reducer { /// A type representing the body of this reducer. // 6f25w /// - /// When you create a custom reducer by implementing the ``body-swift.property-8lumc``, Swift - /// infers this type from the value returned. + /// When you create a custom reducer by implementing the ``body-swift.property``, Swift infers + /// this type from the value returned. /// - /// If you create a custom reducer by implementing the ``reduce(into:action:)-4zl56``, Swift + /// If you create a custom reducer by implementing the ``reduce(into:action:)-1t2ri``, Swift /// infers this type to be `Never`. typealias Body = _Body #else /// A type representing the body of this reducer. /// - /// When you create a custom reducer by implementing the ``body-swift.property-8lumc``, Swift - /// infers this type from the value returned. + /// When you create a custom reducer by implementing the ``body-swift.property``, Swift infers + /// this type from the value returned. /// - /// If you create a custom reducer by implementing the ``reduce(into:action:)-4zl56``, Swift + /// If you create a custom reducer by implementing the ``reduce(into:action:)-1t2ri``, Swift /// infers this type to be `Never`. associatedtype Body #endif @@ -194,8 +193,8 @@ public protocol Reducer { /// Evolves the current state of the reducer to the next state. /// /// Implement this requirement for "primitive" reducers, or reducers that work on leaf node - /// features. To define a reducer by combining the logic of other reducers together, implement - /// the ``body-swift.property-8lumc`` requirement instead. + /// features. To define a reducer by combining the logic of other reducers together, implement the + /// ``body-swift.property`` requirement instead. /// /// - Parameters: /// - state: The current state of the reducer. @@ -212,8 +211,8 @@ public protocol Reducer { /// /// Do not invoke this property directly. /// - /// > Important: if your reducer implements the ``reduce(into:action:)-4zl56`` method, it will - /// > take precedence over this property, and only ``reduce(into:action:)-4zl56`` will be called + /// > Important: if your reducer implements the ``reduce(into:action:)-1t2ri`` method, it will + /// > take precedence over this property, and only ``reduce(into:action:)-1t2ri`` will be called /// > by the ``Store``. If your reducer assembles a body from other reducers and has additional /// > business logic it needs to layer into the system, introduce this logic into the body /// > instead, either with ``Reduce``, or with a separate, dedicated conformance. @@ -239,7 +238,7 @@ extension Reducer where Body == Never { } extension Reducer where Body: Reducer, Body.State == State, Body.Action == Action { - /// Invokes the ``Body-40qdd``'s implementation of ``reduce(into:action:)-4zl56``. + /// Invokes the ``Body-40qdd``'s implementation of ``reduce(into:action:)-1t2ri``. @inlinable public func reduce( into state: inout Body.State, action: Body.Action diff --git a/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducer.swift b/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducer.swift deleted file mode 100644 index b4091f6408aa..000000000000 --- a/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducer.swift +++ /dev/null @@ -1,931 +0,0 @@ -import CasePaths -import Combine - -/// This API has been deprecated in favor of ``Reducer``. -/// Read for more information. -/// -/// A reducer describes how to evolve the current state of an application to the next state, given -/// an action, and describes what ``Effect``s should be executed later by the store, if any. -/// -/// Reducers have 3 generics: -/// -/// * `State`: A type that holds the current state of the application. -/// * `Action`: A type that holds all possible actions that cause the state of the application to -/// change. -/// * `Environment`: A type that holds all dependencies needed in order to produce -/// ``Effect``s, such as API clients, analytics clients, random number generators, etc. -/// -/// > Important: The thread on which effects output is important. An effect's output is immediately -/// > sent back into the store, and ``Store`` is not thread safe. This means all effects must -/// > receive values on the same thread, **and** if the ``Store`` is being used to drive UI then all -/// > output must be on the main thread. You can use the `Publisher` method `receive(on:)` for make -/// > the effect output its values on the thread of your choice. -/// > -/// > This is only an issue if using the Combine interface of ``EffectPublisher`` as mentioned -/// > above. If you are only using Swift's concurrency tools and the `.task`, `.run` and -/// > `.fireAndForget` functions on ``Effect``, then the threading is automatically handled for -/// > you. -@available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of 'Reducer'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ -) -public struct AnyReducer { - private let reducer: (inout State, Action, Environment) -> Effect - - /// > This API has been deprecated in favor of ``Reducer``. - /// Read for more information. - /// - /// Initializes a reducer from a simple reducer function signature. - /// - /// The reducer takes three arguments: state, action and environment. The state is `inout` so that - /// you can make any changes to it directly inline. The reducer must return an effect, which - /// typically would be constructed by using the dependencies inside the `environment` value. If - /// no effect needs to be executed, a ``EffectPublisher/none`` effect can be returned. - /// - /// For example: - /// - /// ```swift - /// struct MyState { var count = 0, text = "" } - /// enum MyAction { case buttonTapped, textChanged(String) } - /// struct MyEnvironment { var analyticsClient: AnalyticsClient } - /// - /// let myReducer = AnyReducer { state, action, environment in - /// switch action { - /// case .buttonTapped: - /// state.count += 1 - /// return environment.analyticsClient.track("Button Tapped") - /// - /// case .textChanged(let text): - /// state.text = text - /// return .none - /// } - /// } - /// ``` - /// - /// - Parameter reducer: A function signature that takes state, action and - /// environment. - @available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of 'Reducer'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public init(_ reducer: @escaping (inout State, Action, Environment) -> Effect) { - self.reducer = reducer - } - - /// This API has been deprecated in favor of ``EmptyReducer``. - /// Read for more information. - /// - /// A reducer that performs no state mutations and returns no effects. - @available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of 'EmptyReducer'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public static var empty: AnyReducer { - Self { _, _, _ in .none } - } - - /// This API has been deprecated in favor of combining reducers in a ``ReducerBuilder``. Read - /// for more information. - /// - /// Combines many reducers into a single one by running each one on state in order, and merging - /// all of the effects. - /// - /// It is important to note that the order of combining reducers matter. Combining `reducerA` with - /// `reducerB` is not necessarily the same as combining `reducerB` with `reducerA`. - /// - /// This can become an issue when working with reducers that have overlapping domains. For - /// example, if `reducerA` embeds the domain of `reducerB` and reacts to its actions or modifies - /// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state - /// _before_ or _after_ `reducerB` runs. - /// - /// This is perhaps most easily seen when working with ``optional(file:fileID:line:)`` reducers, - /// where the parent domain may listen to the child domain and `nil` out its state. If the parent - /// reducer runs before the child reducer, then the child reducer will not be able to react to its - /// own action. - /// - /// Similar can be said for a ``forEach(state:action:environment:file:fileID:line:)-2ypoa`` - /// reducer. If the parent domain modifies the child collection by moving, removing, or modifying - /// an element before the `forEach` reducer runs, the `forEach` reducer may perform its action - /// against the wrong element, an element that no longer exists, or an element in an unexpected - /// state. - /// - /// Running a parent reducer before a child reducer can be considered an application logic - /// error, and can produce assertion failures. So you should almost always combine reducers in - /// order from child to parent domain. - /// - /// Here is an example of how you should combine an ``optional(file:fileID:line:)`` reducer with a - /// parent domain: - /// - /// ```swift - /// let parentReducer = AnyReducer.combine( - /// // Combined before parent so that it can react to `.dismiss` while state is non-`nil`. - /// childReducer.optional().pullback( - /// state: \.child, - /// action: /ParentAction.child, - /// environment: { $0.child } - /// ), - /// // Combined after child so that it can `nil` out child state upon `.child(.dismiss)`. - /// AnyReducer { state, action, environment in - /// switch action - /// case .child(.dismiss): - /// state.child = nil - /// return .none - /// // ... - /// } - /// }, - /// ) - /// ``` - /// - /// - Parameter reducers: A list of reducers. - /// - Returns: A single reducer. - @available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of combining reducers in a 'ReducerBuilder'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public static func combine(_ reducers: Self...) -> Self { - .combine(reducers) - } - - /// This API has been deprecated in favor of combining reducers in a ``ReducerBuilder``. Read - /// for more information. - /// - /// Combines many reducers into a single one by running each one on state in order, and merging - /// all of the effects. - /// - /// This method is identical to ``AnyReducer/combine(_:)-94fzl`` except that it takes an array - /// of reducers instead of a variadic list. See the documentation on - /// ``AnyReducer/combine(_:)-94fzl`` for more information about what this method does. - /// - /// - Parameter reducers: An array of reducers. - /// - Returns: A single reducer. - @available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of combining reducers in a 'ReducerBuilder'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public static func combine(_ reducers: [Self]) -> Self { - Self { state, action, environment in - reducers.reduce(.none) { $0.merge(with: $1(&state, action, environment)) } - } - } - - /// This API has been deprecated in favor of combining reducers in a ``ReducerBuilder``. Read - /// for more information. - /// - /// Combines the receiving reducer with one other reducer, running the second after the first and - /// merging all of the effects. - /// - /// This method is identical to ``AnyReducer/combine(_:)-94fzl`` except that it combines the - /// receiver with a single other reducer rather than combining a whole list of reducers. See the - /// documentation on ``AnyReducer/combine(_:)-94fzl`` for more information about what this method - /// does. - /// - /// - Parameter other: Another reducer. - /// - Returns: A single reducer. - @available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of combining reducers in a 'ReducerBuilder'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public func combined(with other: Self) -> Self { - Self { state, action, environment in - self(&state, action, environment).merge(with: other(&state, action, environment)) - } - } - - /// This API has been deprecated in favor of ``Scope``. Read - /// for more information. - /// - /// Transforms a reducer that works on child state, action, and environment into one that works on - /// parent state, action and environment. It accomplishes this by providing 3 transformations to - /// the method: - /// - /// * A writable key path that can get/set a piece of child state from the parent state. - /// * A case path that can extract/embed a child action into a parent action. - /// * A function that can transform the parent environment into a child environment. - /// - /// This operation is important for breaking down large reducers into small ones. When used with - /// the ``combine(_:)-y8ee`` operator you can define many reducers that work on small pieces of - /// domain, and then _pull them back_ and _combine_ them into one big reducer that works on a - /// large domain. - /// - /// ```swift - /// // Global domain that holds a child domain: - /// struct AppState { var settings: SettingsState, /* rest of state */ } - /// enum AppAction { case settings(SettingsAction), /* other actions */ } - /// struct AppEnvironment { var settings: SettingsEnvironment, /* rest of dependencies */ } - /// - /// // A reducer that works on the child domain: - /// let settingsReducer = AnyReducer { ... } - /// - /// // Pullback the settings reducer so that it works on all of the app domain: - /// let appReducer = AnyReducer.combine( - /// settingsReducer.pullback( - /// state: \.settings, - /// action: /AppAction.settings, - /// environment: { $0.settings } - /// ), - /// - /// /* other reducers */ - /// ) - /// ``` - /// - /// - Parameters: - /// - toChildState: A key path that can get/set `State` inside `ParentState`. - /// - toChildAction: A case path that can extract/embed `Action` from `ParentAction`. - /// - toChildEnvironment: A function that transforms `ParentEnvironment` into `Environment`. - /// - Returns: A reducer that works on `ParentState`, `ParentAction`, `ParentEnvironment`. - @available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of 'Scope'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public func pullback( - state toChildState: WritableKeyPath, - action toChildAction: CasePath, - environment toChildEnvironment: @escaping (ParentEnvironment) -> Environment - ) -> AnyReducer { - .init { parentState, parentAction, parentEnvironment in - guard let childAction = toChildAction.extract(from: parentAction) else { return .none } - return self.reducer( - &parentState[keyPath: toChildState], - childAction, - toChildEnvironment(parentEnvironment) - ) - .map { toChildAction.embed($0) } - } - } - - /// This API has been soft-deprecated in favor of - /// ``Reducer/ifCaseLet(_:action:then:fileID:line:)`` and ``Scope/init(state:action:child:)``. - /// Read for more information. - /// - /// Transforms a reducer that works on child state, action, and environment into one that works on - /// parent state, action and environment. - /// - /// It accomplishes this by providing 3 transformations to the method: - /// - /// * A case path that can extract/embed a piece of child state from the parent state, which is - /// typically an enum. - /// * A case path that can extract/embed a child action into a parent action. - /// * A function that can transform the parent environment into a child environment. - /// - /// This overload of ``pullback(state:action:environment:)`` differs from the other in that it - /// takes a `CasePath` transformation for the state instead of a `WritableKeyPath`. This makes it - /// perfect for working on enum state as opposed to struct state. In particular, you can use this - /// operator to pullback a reducer that operates on a single case of some state enum to work on - /// the entire state enum. - /// - /// When used with the ``combine(_:)-94fzl`` operator you can define many reducers that work each - /// case of the state enum, and then _pull them back_ and _combine_ them into one big reducer that - /// works on a large domain. - /// - /// ```swift - /// // Parent domain that holds a child domain: - /// enum AppState { case loggedIn(LoggedInState), /* rest of state */ } - /// enum AppAction { case loggedIn(LoggedInAction), /* other actions */ } - /// struct AppEnvironment { var loggedIn: LoggedInEnvironment, /* rest of dependencies */ } - /// - /// // A reducer that works on the child domain: - /// let loggedInReducer = AnyReducer { ... } - /// - /// // Pullback the logged-in reducer so that it works on all of the app domain: - /// let appReducer: AnyReducer = .combine( - /// loggedInReducer.pullback( - /// state: /AppState.loggedIn, - /// action: /AppAction.loggedIn, - /// environment: { $0.loggedIn } - /// ), - /// - /// /* other reducers */ - /// ) - /// ``` - /// - /// Take care when combining a child reducer for a particular case of enum state into its parent - /// domain. A child reducer cannot process actions in its domain if it fails to extract its - /// corresponding state. If a child action is sent to a reducer when its state is unavailable, it - /// is generally considered a logic error, and a runtime warning will be logged. There are a few - /// ways in which these errors can sneak into a code base: - /// - /// * A parent reducer sets child state to a different case when processing a child action and - /// runs _before_ the child reducer: - /// - /// ```swift - /// let parentReducer = AnyReducer.combine( - /// // When combining reducers, the parent reducer runs first - /// AnyReducer { state, action, environment in - /// switch action { - /// case .child(.didDisappear): - /// // And `nil`s out child state when processing a child action - /// state.child = .anotherChild(AnotherChildState()) - /// return .none - /// // ... - /// } - /// }, - /// // Before the child reducer runs - /// childReducer.pullback(state: /ParentState.child, ...) - /// ) - /// - /// let childReducer = Reducer< - /// ChildState, ChildAction, ChildEnvironment - /// > { state, action environment in - /// case .didDisappear: - /// // This action is never received here because child state cannot be extracted - /// // ... - /// } - /// ``` - /// - /// To ensure that a child reducer can process any action that a parent may use to change its - /// state, combine it _before_ the parent: - /// - /// ```swift - /// let parentReducer = Reducer.combine( - /// // The child runs first - /// childReducer.pullback(state: /ParentState.child, ...), - /// // The parent runs after - /// Reducer { state, action, environment in - /// // ... - /// } - /// ) - /// ``` - /// - /// * A child effect feeds a child action back into the store when child state is unavailable: - /// - /// ```swift - /// let childReducer = Reducer< - /// ChildState, ChildAction, ChildEnvironment - /// > { state, action environment in - /// switch action { - /// case .onAppear: - /// // An effect may want to later feed a result back to the child domain in an action - /// return environment.apiClient - /// .request() - /// .map(ChildAction.response) - /// - /// case let .response(response): - /// // But the child cannot process this action if its state is unavailable - /// // ... - /// } - /// } - /// ``` - /// - /// It is perfectly reasonable to ignore the result of an effect when child state is `nil`, - /// for example one-off effects that you don't want to cancel. However, many long-living - /// effects _should_ be explicitly canceled when tearing down a child domain: - /// - /// ```swift - /// let childReducer = Reducer< - /// ChildState, ChildAction, ChildEnvironment - /// > { state, action environment in - /// enum CancelID { case motion } - /// - /// switch action { - /// case .onAppear: - /// // Mark long-living effects that shouldn't outlive their domain cancellable - /// return environment.motionClient - /// .start() - /// .map(ChildAction.motion) - /// .cancellable(id: CancelID.motion) - /// - /// case .onDisappear: - /// // And explicitly cancel them when the domain is torn down - /// return .cancel(id: CancelID.motion) - /// // ... - /// } - /// } - /// ``` - /// - /// * A view store sends a child action when child state is `nil`: - /// - /// ```swift - /// WithViewStore(self.parentStore) { parentViewStore in - /// // If child state is `nil`, it cannot process this action. - /// Button("Child Action") { parentViewStore.send(.child(.action)) } - /// // ... - /// } - /// ``` - /// - /// Use ``Store/scope(state:action:)-9iai9`` with ``SwitchStore`` to ensure that views can only send - /// child actions when the child domain is available. - /// - /// ```swift - /// SwitchStore(self.parentStore) { - /// CaseLet(/ParentState.child, action: ParentAction.child) { childStore in - /// // This destination only appears when child state matches - /// WithViewStore(childStore) { childViewStore in - /// // So this action can only be sent when child state is available - /// Button("Child Action") { childViewStore.send(.action) } - /// } - /// } - /// // ... - /// } - /// ``` - /// - /// - See also: ``SwitchStore``, a SwiftUI helper for transforming a store on enum state into - /// stores on each case of the enum. - /// - /// - Parameters: - /// - toChildState: A case path that can extract/embed `State` from `ParentState`. - /// - toChildAction: A case path that can extract/embed `Action` from `ParentAction`. - /// - toChildEnvironment: A function that transforms `ParentEnvironment` into `Environment`. - /// - Returns: A reducer that works on `ParentState`, `ParentAction`, `ParentEnvironment`. - @available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of 'Reducer.ifCaseLet' and 'Scope'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public func pullback( - state toChildState: CasePath, - action toChildAction: CasePath, - environment toChildEnvironment: @escaping (ParentEnvironment) -> Environment, - file: StaticString = #file, - fileID: StaticString = #fileID, - line: UInt = #line - ) -> AnyReducer { - .init { parentState, parentAction, parentEnvironment in - guard let childAction = toChildAction.extract(from: parentAction) else { return .none } - - guard var childState = toChildState.extract(from: parentState) else { - runtimeWarn( - """ - A reducer pulled back from "\(fileID):\(line)" received an action when child state was \ - unavailable. … - - Action: - \(debugCaseOutput(childAction)) - - This is generally considered an application logic error, and can happen for a few \ - reasons: - - • The reducer for a particular case of state was combined with or run from another \ - reducer that set "\(typeName(State.self))" to another case before the reducer ran. \ - Combine or run case-specific reducers before reducers that may set their state to \ - another case. This ensures that case-specific reducers can handle their actions while \ - their state is available. - - • An in-flight effect emitted this action when state was unavailable. While it may be \ - perfectly reasonable to ignore this action, you may want to cancel the associated \ - effect before state is set to another case, especially if it is a long-living effect. - - • This action was sent to the store while state was another case. Make sure that \ - actions for this reducer can only be sent to a view store when state is non-"nil". \ - In SwiftUI applications, use "SwitchStore". - """ - ) - return .none - } - defer { parentState = toChildState.embed(childState) } - - let effects = self.run( - &childState, - childAction, - toChildEnvironment(parentEnvironment) - ) - .map { toChildAction.embed($0) } - - return effects - } - } - - /// This API has been soft-deprecated in favor of ``Reducer/ifLet(_:action:then:fileID:line:)``. - /// Read for more information. - /// - /// Transforms a reducer that works on non-optional state into one that works on optional state by - /// only running the non-optional reducer when state is non-nil. - /// - /// Often used in tandem with ``pullback(state:action:environment:)`` to transform a reducer on a - /// non-optional child domain into a reducer that can be combined with a reducer on a parent - /// domain that contains some optional child domain: - /// - /// ```swift - /// // Parent domain that holds an optional child domain: - /// struct AppState { var modal: ModalState? } - /// enum AppAction { case modal(ModalAction) } - /// struct AppEnvironment { var mainQueue: AnySchedulerOf } - /// - /// // A reducer that works on the non-optional child domain: - /// let modalReducer = Reducer.combine( - /// modalReducer.optional().pullback( - /// state: \.modal, - /// action: /AppAction.modal, - /// environment: { ModalEnvironment(mainQueue: $0.mainQueue) } - /// ), - /// Reducer { state, action, environment in - /// // ... - /// } - /// ) - /// ``` - /// - /// Take care when combining optional reducers into parent domains. An optional reducer cannot - /// process actions in its domain when its state is `nil`. If a child action is sent to an - /// optional reducer when child state is `nil`, it is generally considered a logic error. There - /// are a few ways in which these errors can sneak into a code base: - /// - /// * A parent reducer sets child state to `nil` when processing a child action and runs - /// _before_ the child reducer: - /// - /// ```swift - /// let parentReducer = Reducer.combine( - /// // When combining reducers, the parent reducer runs first - /// Reducer { state, action, environment in - /// switch action { - /// case .child(.didDisappear): - /// // And `nil`s out child state when processing a child action - /// state.child = nil - /// return .none - /// // ... - /// } - /// }, - /// // Before the child reducer runs - /// childReducer.optional().pullback(...) - /// ) - /// - /// let childReducer = Reducer< - /// ChildState, ChildAction, ChildEnvironment - /// > { state, action environment in - /// case .didDisappear: - /// // This action is never received here because child state is `nil` in the parent - /// // ... - /// } - /// ``` - /// - /// To ensure that a child reducer can process any action that a parent may use to `nil` out - /// its state, combine it _before_ the parent: - /// - /// ```swift - /// let parentReducer = Reducer.combine( - /// // The child runs first - /// childReducer.optional().pullback(...), - /// // The parent runs after - /// Reducer { state, action, environment in - /// // ... - /// } - /// ) - /// ``` - /// - /// * A child effect feeds a child action back into the store when child state is `nil`: - /// - /// ```swift - /// let childReducer = Reducer< - /// ChildState, ChildAction, ChildEnvironment - /// > { state, action environment in - /// switch action { - /// case .onAppear: - /// // An effect may want to feed its result back to the child domain in an action - /// return environment.apiClient - /// .request() - /// .map(ChildAction.response) - /// - /// case let .response(response): - /// // But the child cannot process this action if its state is `nil` in the parent - /// // ... - /// } - /// } - /// ``` - /// - /// It is perfectly reasonable to ignore the result of an effect when child state is `nil`, - /// for example one-off effects that you don't want to cancel. However, many long-living - /// effects _should_ be explicitly canceled when tearing down a child domain: - /// - /// ```swift - /// let childReducer = Reducer< - /// ChildState, ChildAction, ChildEnvironment - /// > { state, action environment in - /// enum CancelID { case motion } - /// - /// switch action { - /// case .onAppear: - /// // Mark long-living effects that shouldn't outlive their domain cancellable - /// return environment.motionClient - /// .start() - /// .map(ChildAction.motion) - /// .cancellable(id: CancelID.motion) - /// - /// case .onDisappear: - /// // And explicitly cancel them when the domain is torn down - /// return .cancel(id: CancelID.motion) - /// // ... - /// } - /// } - /// ``` - /// - /// * A view store sends a child action when child state is `nil`: - /// - /// ```swift - /// WithViewStore(self.parentStore) { parentViewStore in - /// // If child state is `nil`, it cannot process this action. - /// Button("Child Action") { parentViewStore.send(.child(.action)) } - /// // ... - /// } - /// ``` - /// - /// Use ``Store/scope(state:action:)-9iai9`` with ``IfLetStore`` or ``Store/ifLet(then:else:)`` to - /// ensure that views can only send child actions when the child domain is non-`nil`. - /// - /// ```swift - /// IfLetStore( - /// self.parentStore.scope(state: { $0.child }, action: { .child($0) } - /// ) { childStore in - /// // This destination only appears when child state is non-`nil` - /// WithViewStore(childStore) { childViewStore in - /// // So this action can only be sent when child state is non-`nil` - /// Button("Child Action") { childViewStore.send(.action) } - /// } - /// // ... - /// } - /// ``` - /// - /// - See also: ``IfLetStore``, a SwiftUI helper for transforming a store on optional state into a - /// store on non-optional state. - /// - See also: ``Store/ifLet(then:else:)``, a UIKit helper for doing imperative work with a store - /// on optional state. - /// - /// - Returns: A reducer that works on optional state. - @available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of 'Reducer.ifLet'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public func optional( - file: StaticString = #file, - fileID: StaticString = #fileID, - line: UInt = #line - ) -> AnyReducer< - State?, Action, Environment - > { - .init { state, action, environment in - guard state != nil else { - runtimeWarn( - """ - An "optional" reducer at "\(fileID):\(line)" received an action when state was "nil". … - - Action: - \(debugCaseOutput(action)) - - This is generally considered an application logic error, and can happen for a few \ - reasons: - - • The optional reducer was combined with or run from another reducer that set \ - "\(typeName(State.self))" to "nil" before the optional reducer ran. Combine or run \ - optional reducers before reducers that can set their state to "nil". This ensures that \ - optional reducers can handle their actions while their state is still non-"nil". - - • An in-flight effect emitted this action while state was "nil". While it may be \ - perfectly reasonable to ignore this action, you may want to cancel the associated \ - effect before state is set to "nil", especially if it is a long-living effect. - - • This action was sent to the store while state was "nil". Make sure that actions for \ - this reducer can only be sent to a view store when state is non-"nil". In SwiftUI \ - applications, use "IfLetStore". - """ - ) - return .none - } - return self.reducer(&state!, action, environment) - } - } - - /// This API has been soft-deprecated in favor of - /// ``Reducer/forEach(_:action:element:fileID:line:)``. Read - /// for more information. - /// - /// A version of ``pullback(state:action:environment:)`` that transforms a reducer that works on - /// an element into one that works on an identified array of elements. - /// - /// ```swift - /// // Parent domain that holds a collection of child domains: - /// struct AppState { var todos: IdentifiedArrayOf } - /// enum AppAction { case todo(id: Todo.ID, action: TodoAction) } - /// struct AppEnvironment { var mainQueue: AnySchedulerOf } - /// - /// // A reducer that works on an element's domain: - /// let todoReducer = Reducer { ... } - /// - /// // Pullback the todo reducer so that it works on all of the app domain: - /// let appReducer = Reducer.combine( - /// todoReducer.forEach( - /// state: \.todos, - /// action: /AppAction.todo(id:action:), - /// environment: { _ in TodoEnvironment() } - /// ), - /// Reducer { state, action, environment in - /// // ... - /// } - /// ) - /// ``` - /// - /// Take care when combining `forEach` reducers into parent domains, as order matters. Always - /// combine `forEach` reducers _before_ parent reducers that can modify the collection. - /// - /// - Parameters: - /// - toElementsState: A key path that can get/set a collection of `State` elements inside - /// `ParentState`. - /// - toElementAction: A case path that can extract/embed `(ID, Action)` from `ParentAction`. - /// - toElementEnvironment: A function that transforms `ParentEnvironment` into `Environment`. - /// - Returns: A reducer that works on `ParentState`, `ParentAction`, `ParentEnvironment`. - @available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of 'Reducer.forEach'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public func forEach( - state toElementsState: WritableKeyPath>, - action toElementAction: CasePath, - environment toElementEnvironment: @escaping (ParentEnvironment) -> Environment, - file: StaticString = #file, - fileID: StaticString = #fileID, - line: UInt = #line - ) -> AnyReducer { - .init { parentState, parentAction, parentEnvironment in - guard let (id, action) = toElementAction.extract(from: parentAction) - else { return .none } - - if parentState[keyPath: toElementsState][id: id] == nil { - runtimeWarn( - """ - A "forEach" reducer at "\(fileID):\(line)" received an action when state contained no \ - element with that id. … - - Action: - \(debugCaseOutput(action)) - ID: - \(id) - - This is generally considered an application logic error, and can happen for a few \ - reasons: - - • This "forEach" reducer was combined with or run from another reducer that removed \ - the element at this id when it handled this action. To fix this make sure that this \ - "forEach" reducer is run before any other reducers that can move or remove elements \ - from state. This ensures that "forEach" reducers can handle their actions for the \ - element at the intended id. - - • An in-flight effect emitted this action while state contained no element at this id. \ - It may be perfectly reasonable to ignore this action, but you also may want to cancel \ - the effect it originated from when removing an element from the identified array, \ - especially if it is a long-living effect. - - • This action was sent to the store while its state contained no element at this id. \ - To fix this make sure that actions for this reducer can only be sent to a view store \ - when its state contains an element at this id. In SwiftUI applications, use \ - "ForEachStore". - """ - ) - return .none - } - return - self - .reducer( - &parentState[keyPath: toElementsState][id: id]!, - action, - toElementEnvironment(parentEnvironment) - ) - .map { toElementAction.embed((id, $0)) } - } - } - - /// A version of ``pullback(state:action:environment:)`` that transforms a reducer that works on - /// an element into one that works on a dictionary of element values. - /// - /// Take care when combining `forEach` reducers into parent domains, as order matters. Always - /// combine `forEach`` reducers _before_ parent reducers that can modify the dictionary. - /// - /// - Parameters: - /// - toDictionaryState: A key path that can get/set a dictionary of `State` values inside - /// `ParentState`. - /// - toKeyedAction: A case path that can extract/embed `(Key, Action)` from `ParentAction`. - /// - toValueEnvironment: A function that transforms `ParentEnvironment` into `Environment`. - /// - Returns: A reducer that works on `ParentState`, `ParentAction`, `ParentEnvironment`. - @available(*, deprecated) - public func forEach( - state toDictionaryState: WritableKeyPath, - action toKeyedAction: CasePath, - environment toValueEnvironment: @escaping (ParentEnvironment) -> Environment, - file: StaticString = #file, - fileID: StaticString = #fileID, - line: UInt = #line - ) -> AnyReducer { - .init { parentState, parentAction, parentEnvironment in - guard let (key, action) = toKeyedAction.extract(from: parentAction) else { return .none } - - if parentState[keyPath: toDictionaryState][key] == nil { - runtimeWarn( - """ - A "forEach" reducer at "\(fileID):\(line)" received an action when state contained no \ - value at that key. … - - Action: - \(debugCaseOutput(action)) - Key: - \(key) - - This is generally considered an application logic error, and can happen for a few \ - reasons: - - • This "forEach" reducer was combined with or run from another reducer that removed \ - the element at this key when it handled this action. To fix this make sure that this \ - "forEach" reducer is run before any other reducers that can move or remove elements \ - from state. This ensures that "forEach" reducers can handle their actions for the \ - element at the intended key. - - • An in-flight effect emitted this action while state contained no element at this \ - key. It may be perfectly reasonable to ignore this action, but you also may want to \ - cancel the effect it originated from when removing a value from the dictionary, \ - especially if it is a long-living effect. - - • This action was sent to the store while its state contained no element at this \ - key. To fix this make sure that actions for this reducer can only be sent to a view \ - store when its state contains an element at this key. - """ - ) - return .none - } - return self.reducer( - &parentState[keyPath: toDictionaryState][key]!, - action, - toValueEnvironment(parentEnvironment) - ) - .map { toKeyedAction.embed((key, $0)) } - } - } - - /// This API has been deprecated in favor of ``Reducer/reduce(into:action:)-4zl56``. - /// Read for more information. - /// - /// Runs the reducer. - /// - /// - Parameters: - /// - state: Mutable state. - /// - action: An action. - /// - environment: An environment. - /// - Returns: An effect that can emit zero or more actions. - @available( - *, - deprecated, - message: - """ - This API has been deprecated in favor of 'Reducer.reduce(into:action:)'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public func run( - _ state: inout State, - _ action: Action, - _ environment: Environment - ) -> Effect { - self.reducer(&state, action, environment) - } - - /// This API has been deprecated in favor of ``Reducer/reduce(into:action:)-4zl56``. - /// Read for more information. - @available( - *, deprecated, - message: - """ - This API has been deprecated in favor of 'Reducer.reduce(into:action:)'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ - ) - public func callAsFunction( - _ state: inout State, - _ action: Action, - _ environment: Environment - ) -> Effect { - self.reducer(&state, action, environment) - } -} diff --git a/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerBinding.swift b/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerBinding.swift deleted file mode 100644 index 0a847a41f76a..000000000000 --- a/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerBinding.swift +++ /dev/null @@ -1,48 +0,0 @@ -@available( - *, deprecated, - message: - """ - This API has been deprecated in favor of 'BindingReducer'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ -) -extension AnyReducer where Action: BindableAction, State == Action.State { - /// This API has been deprecated in favor of ``BindingReducer``. Read - /// for more information. - /// - /// Returns a reducer that applies ``BindingAction`` mutations to `State` before running this - /// reducer's logic. - /// - /// For example, a settings screen may gather its binding actions into a single - /// ``BindingAction`` case by conforming to ``BindableAction``: - /// - /// ```swift - /// enum SettingsAction: BindableAction { - /// // ... - /// case binding(BindingAction) - /// } - /// ``` - /// - /// The reducer can then be enhanced to automatically handle these mutations for you by tacking - /// on the ``binding()`` method: - /// - /// ```swift - /// let settingsReducer = AnyReducer { - /// // ... - /// } - /// .binding() - /// ``` - /// - /// - Returns: A reducer that applies ``BindingAction`` mutations to `State` before running this - /// reducer's logic. - public func binding() -> Self { - Self { state, action, environment in - guard let bindingAction = (/Action.binding).extract(from: action) - else { - return self.run(&state, action, environment) - } - - bindingAction.set(&state) - return self.run(&state, action, environment) - } - } -} diff --git a/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerCompatibility.swift b/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerCompatibility.swift deleted file mode 100644 index 7603f1dd641b..000000000000 --- a/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerCompatibility.swift +++ /dev/null @@ -1,70 +0,0 @@ -extension Reduce { - @available( - *, deprecated, - message: - """ - 'AnyReducer' has been deprecated in favor of 'Reducer'. - - See the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/reducerprotocol - """ - ) - public init( - _ reducer: AnyReducer, - environment: Environment - ) { - self.init(internal: { state, action in - reducer.run(&state, action, environment) - }) - } -} - -@available( - *, deprecated, - message: - """ - 'AnyReducer' has been deprecated in favor of 'Reducer'. - - See the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/reducerprotocol - """ -) -extension AnyReducer { - public init( - @ReducerBuilder _ build: @escaping (Environment) -> R - ) where R.State == State, R.Action == Action { - self.init { state, action, environment in - build(environment).reduce(into: &state, action: action) - } - } - - public init(_ reducer: R) where R.State == State, R.Action == Action { - self.init { _ in reducer } - } -} - -extension Store { - /// Initializes a store from an initial state, a reducer, and an environment. - /// - /// - Parameters: - /// - initialState: The state to start the application in. - /// - reducer: The reducer that powers the business logic of the application. - /// - environment: The environment of dependencies for the application. - @available( - *, deprecated, - message: - """ - 'AnyReducer' has been deprecated in favor of 'Reducer'. - - See the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/reducerprotocol - """ - ) - public convenience init( - initialState: State, - reducer: AnyReducer, - environment: Environment - ) { - self.init( - initialState: initialState, - reducer: Reduce(reducer, environment: environment) - ) - } -} diff --git a/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerDebug.swift b/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerDebug.swift deleted file mode 100644 index 2f56cbbbcdf4..000000000000 --- a/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerDebug.swift +++ /dev/null @@ -1,210 +0,0 @@ -import CasePaths -import Dispatch - -@available( - *, deprecated, - message: - """ - This API has been deprecated in favor of 'Reducer._printChanges'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ -) -extension AnyReducer { - /// This API has been deprecated in favor of ``Reducer/debug()``. Read - /// for more information. - /// - /// Prints debug messages describing all received actions and state mutations. - /// - /// Printing is only done in debug (`#if DEBUG`) builds. - /// - /// - Parameters: - /// - prefix: A string with which to prefix all debug messages. - /// - toDebugEnvironment: A function that transforms an environment into a debug environment by - /// describing a print function and a queue to print from. Defaults to a function that ignores - /// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print` - /// function and a background queue. - /// - Returns: A reducer that prints debug messages for all received actions. - public func debug( - _ prefix: String = "", - actionFormat: ActionFormat = .prettyPrint, - environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in - DebugEnvironment() - } - ) -> Self { - self.debug( - prefix, - state: { $0 }, - action: .self, - actionFormat: actionFormat, - environment: toDebugEnvironment - ) - } - - /// The API that used this type has been soft-deprecated in favor of ``Reducer/debug()``. - /// Read for more information. - /// - /// Prints debug messages describing all received actions. - /// - /// Printing is only done in debug (`#if DEBUG`) builds. - /// - /// - Parameters: - /// - prefix: A string with which to prefix all debug messages. - /// - toDebugEnvironment: A function that transforms an environment into a debug environment by - /// describing a print function and a queue to print from. Defaults to a function that ignores - /// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print` - /// function and a background queue. - /// - Returns: A reducer that prints debug messages for all received actions. - public func debugActions( - _ prefix: String = "", - actionFormat: ActionFormat = .prettyPrint, - environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in - DebugEnvironment() - } - ) -> Self { - self.debug( - prefix, - state: { _ in () }, - action: .self, - actionFormat: actionFormat, - environment: toDebugEnvironment - ) - } - - /// This API has been deprecated in favor of `Reducer._printChanges()`. Read - /// for more information. - /// - /// Prints debug messages describing all received actions and state mutations. - /// - /// Printing is only done in debug (`#if DEBUG`) builds. - /// - /// - Parameters: - /// - prefix: A string with which to prefix all debug messages. - /// - toDebugState: A function that filters state to be printed. - /// - toDebugAction: A case path that filters actions that are printed. - /// - toDebugEnvironment: A function that transforms an environment into a debug environment by - /// describing a print function and a queue to print from. Defaults to a function that ignores - /// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print` - /// function and a background queue. - /// - Returns: A reducer that prints debug messages for all received actions. - public func debug( - _ prefix: String = "", - state toDebugState: @escaping (State) -> DebugState, - action toDebugAction: CasePath, - actionFormat: ActionFormat = .prettyPrint, - environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in - DebugEnvironment() - } - ) -> Self { - #if DEBUG - return .init { state, action, environment in - let previousState = toDebugState(state) - let effects = self.run(&state, action, environment) - guard let debugAction = toDebugAction.extract(from: action) else { return effects } - let nextState = toDebugState(state) - let debugEnvironment = toDebugEnvironment(environment) - - @Sendable - func print() { - debugEnvironment.queue.async { - var actionOutput = "" - if actionFormat == .prettyPrint { - customDump(debugAction, to: &actionOutput, indent: 2) - } else { - actionOutput.write(debugCaseOutput(debugAction).indent(by: 2)) - } - let stateOutput = - DebugState.self == Void.self - ? "" - : diff(previousState, nextState).map { "\($0)\n" } ?? " (No state changes)\n" - debugEnvironment.printer( - """ - \(prefix.isEmpty ? "" : "\(prefix): ")received action: - \(actionOutput) - \(stateOutput) - """ - ) - } - } - - switch effects.operation { - case .none: - return .fireAndForget { print() } - case .publisher: - return .fireAndForget { print() }.merge(with: effects) - case .run: - return .run { _ in print() }.merge(with: effects) - } - } - #else - return self - #endif - } -} - -/// Determines how the string description of an action should be printed when using the -/// ``AnyReducer/debug(_:state:action:actionFormat:environment:)`` higher-order reducer. -@available( - *, deprecated, - message: - """ - This API that used this type has been deprecated in favor of 'Reducer._printChanges'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ -) -public enum ActionFormat: Sendable { - /// Prints the action in a single line by only specifying the labels of the associated values: - /// - /// ```swift - /// Action.screenA(.row(index:, action: .textChanged(query:))) - /// ``` - case labelsOnly - - /// Prints the action in a multiline, pretty-printed format, including all the labels of - /// any associated values, as well as the data held in the associated values: - /// - /// ```swift - /// Action.screenA( - /// ScreenA.row( - /// index: 1, - /// action: RowAction.textChanged( - /// query: "Hi" - /// ) - /// ) - /// ) - /// ``` - case prettyPrint -} - -/// The API that used this type has been soft-deprecated in favor of -/// ``Reducer/debug()`` Read for more -/// information. -/// -/// An environment for debug-printing reducers. -@available( - *, deprecated, - message: - """ - This API that used this type has been deprecated in favor of 'Reducer._printChanges'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ -) -public struct DebugEnvironment { - public var printer: (String) -> Void - public var queue: DispatchQueue - - public init( - printer: @escaping (String) -> Void = { print($0) }, - queue: DispatchQueue - ) { - self.printer = printer - self.queue = queue - } - - public init( - printer: @escaping (String) -> Void = { print($0) } - ) { - self.init(printer: printer, queue: _queue) - } -} - -private let _queue = DispatchQueue( - label: "co.pointfree.ComposableArchitecture.DebugEnvironment", - qos: .default -) diff --git a/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerSignpost.swift b/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerSignpost.swift deleted file mode 100644 index ef7911a09ae4..000000000000 --- a/Sources/ComposableArchitecture/Reducer/AnyReducer/AnyReducerSignpost.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Combine -import os.signpost - -@available( - *, deprecated, - message: - """ - This API has been deprecated in favor of 'Reducer.signpost'. Read the migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/Reducer - """ -) -extension AnyReducer { - /// Instruments the reducer with - /// [signposts](https://developer.apple.com/documentation/os/logging/recording_performance_data). - /// Each invocation of the reducer will be measured by an interval, and the lifecycle of its - /// effects will be measured with interval and event signposts. - /// - /// To use, build your app for Instruments (⌘I), create a blank instrument, and then use the "+" - /// icon at top right to add the signpost instrument. Start recording your app (red button at top - /// left) and then you should see timing information for every action sent to the store and every - /// effect executed. - /// - /// Effect instrumentation can be particularly useful for inspecting the lifecycle of long-living - /// effects. For example, if you start an effect (e.g. a location manager) in `onAppear` and - /// forget to tear down the effect in `onDisappear`, it will clearly show in Instruments that the - /// effect never completed. - /// - /// - Parameters: - /// - prefix: A string to print at the beginning of the formatted message for the signpost. - /// - log: An `OSLog` to use for signposts. - /// - Returns: A reducer that has been enhanced with instrumentation. - public func signpost( - _ prefix: String = "", - log: OSLog = OSLog( - subsystem: "co.pointfree.composable-architecture", - category: "Reducer Instrumentation" - ) - ) -> Self { - guard log.signpostsEnabled else { return self } - - // NB: Prevent rendering as "N/A" in Instruments - let zeroWidthSpace = "\u{200B}" - - let prefix = prefix.isEmpty ? zeroWidthSpace : "[\(prefix)] " - - return Self { state, action, environment in - var actionOutput: String! - if log.signpostsEnabled { - actionOutput = debugCaseOutput(action) - os_signpost(.begin, log: log, name: "Action", "%s%s", prefix, actionOutput) - } - let effects = self.run(&state, action, environment) - if log.signpostsEnabled { - os_signpost(.end, log: log, name: "Action") - return - effects - .effectSignpost(prefix, log: log, actionOutput: actionOutput) - } - return effects - } - } -} diff --git a/Sources/ComposableArchitecture/Reducer/ReducerBuilder.swift b/Sources/ComposableArchitecture/Reducer/ReducerBuilder.swift index cbf681f5dd5a..afbc3e6d8560 100644 --- a/Sources/ComposableArchitecture/Reducer/ReducerBuilder.swift +++ b/Sources/ComposableArchitecture/Reducer/ReducerBuilder.swift @@ -1,8 +1,8 @@ /// A result builder for combining reducers into a single reducer by running each, one after the /// other, and merging their effects. /// -/// It is most common to encounter a reducer builder context when conforming a type to -/// ``Reducer`` and implementing its ``Reducer/body-swift.property-8lumc`` property. +/// It is most common to encounter a reducer builder context when conforming a type to ``Reducer`` +/// and implementing its ``Reducer/body-swift.property`` property. /// /// See ``CombineReducers`` for an entry point into a reducer builder context. @resultBuilder diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/BindingReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/BindingReducer.swift index ba45c41a5fd6..e8b54403dae2 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/BindingReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/BindingReducer.swift @@ -2,8 +2,8 @@ import SwiftUI /// A reducer that updates bindable state when it receives binding actions. /// -/// This reducer should typically be composed into the ``Reducer/body-swift.property-8lumc`` -/// of your feature's reducer: +/// This reducer should typically be composed into the ``Reducer/body-swift.property`` of your +/// feature's reducer: /// /// ```swift /// struct Feature: Reducer { diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/OnChange.swift b/Sources/ComposableArchitecture/Reducer/Reducers/OnChange.swift index 6f82c04de392..f8dd5cbd4a19 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/OnChange.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/OnChange.swift @@ -75,7 +75,7 @@ where Base.State == Body.State, Base.Action == Body.Action { } @inlinable - public func reduce(into state: inout Base.State, action: Base.Action) -> EffectTask { + public func reduce(into state: inout Base.State, action: Base.Action) -> Effect { let oldValue = toValue(state) let baseEffects = self.base.reduce(into: &state, action: action) let newValue = toValue(state) diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift index bca9d2a02b30..5a9a79479cf8 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift @@ -412,11 +412,9 @@ public struct _PresentationReducer: Reducer let presentationDestinationID = self.navigationIDPath(for: presentationState) state[keyPath: self.toPresentationState].isPresented = true presentEffects = .concatenate( - Empty(completeImmediately: false) - .eraseToEffectPublisher() + .publisher { Empty(completeImmediately: false) } ._cancellable(id: PresentationDismissID(), navigationIDPath: presentationDestinationID), - Just(self.toPresentationAction.embed(.dismiss)) - .eraseToEffectPublisher() + .publisher { Just(self.toPresentationAction.embed(.dismiss)) } ) ._cancellable(navigationIDPath: presentationDestinationID) ._cancellable(id: OnFirstAppearID(), navigationIDPath: .init()) @@ -473,7 +471,8 @@ extension Task where Success == Never, Failure == Never { } } } -extension EffectPublisher { + +extension Effect { internal func _cancellable( id: ID = _PresentedID(), navigationIDPath: NavigationIDPath, diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/Scope.swift b/Sources/ComposableArchitecture/Reducer/Reducers/Scope.swift index f1abd1c05ef8..e94e4b6c7e55 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/Scope.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/Scope.swift @@ -29,8 +29,7 @@ /// ``` /// /// A parent reducer with a domain that holds onto the child domain can use -/// ``init(state:action:child:)`` to embed the child reducer in its -/// ``Reducer/body-swift.property-8lumc``: +/// ``init(state:action:child:)`` to embed the child reducer in its ``Reducer/body-swift.property``: /// /// ```swift /// struct Parent: Reducer { diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/SignpostReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/SignpostReducer.swift index e1db3a2c977b..2452ae8ffecc 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/SignpostReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/SignpostReducer.swift @@ -76,7 +76,7 @@ public struct _SignpostReducer: Reducer { } } -extension EffectPublisher where Failure == Never { +extension Effect { @usableFromInline func effectSignpost( _ prefix: String, diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/StackReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/StackReducer.swift index b5b86738dd30..e684f4cc22fc 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/StackReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/StackReducer.swift @@ -443,14 +443,12 @@ public struct _StackReducer: Reducer { let navigationDestinationID = self.navigationIDPath(for: elementID) state[keyPath: self.toStackState]._mounted.insert(elementID) return .concatenate( - Empty(completeImmediately: false) - .eraseToEffectPublisher() + .publisher { Empty(completeImmediately: false) } ._cancellable( id: NavigationDismissID(elementID: elementID), navigationIDPath: navigationDestinationID ), - Just(self.toStackAction.embed(.popFrom(id: elementID))) - .eraseToEffectPublisher() + .publisher { Just(self.toStackAction.embed(.popFrom(id: elementID))) } ) ._cancellable(navigationIDPath: navigationDestinationID) ._cancellable(id: OnFirstAppearID(), navigationIDPath: .init()) diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 86eff850aefd..41133fcdb81e 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -122,7 +122,7 @@ import SwiftUI /// #### Thread safety checks /// /// The store performs some basic thread safety checks in order to help catch mistakes. Stores -/// constructed via the initializer ``init(initialState:reducer:prepareDependencies:)`` are assumed +/// constructed via the initializer ``init(initialState:reducer:withDependencies:)`` are assumed /// to run only on the main thread, and so a check is executed immediately to make sure that is the /// case. Further, all actions sent to the store and all scopes (see ``scope(state:action:)-9iai9``) of /// the store are also checked to make sure that work is performed on the main thread. @@ -563,17 +563,6 @@ public final class Store { } } - @available(*, deprecated, message: "Send actions directly to 'store' instead.") - public var stateless: Store { - self.scope(state: { _ in () }, action: { $0 }) - } - - @available(*, deprecated, message: "Define a domain-specific, empty 'Action' enum instead.") - public var actionless: Store { - func absurd(_ never: Never) -> A {} - return self.scope(state: { $0 }, action: absurd) - } - private enum ThreadCheckStatus { case effectCompletion(Action) case `init` @@ -790,9 +779,10 @@ extension ScopedReducer: AnyScopedReducer { parentStores: self.parentStores + [store] ) let childStore = Store( - initialState: toRescopedState(store.state.value), - reducer: reducer - ) + initialState: toRescopedState(store.state.value) + ) { + reducer + } childStore._isInvalidated = store._isInvalidated childStore.parentCancellable = store.state .dropFirst() diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index 9c6ded2b3337..7a29c83346a6 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -61,82 +61,3 @@ extension View { } } } - -@available( - *, - deprecated, - message: - """ - Use 'View.alert(store:)' with 'PresentationState' and 'PresentationAction' instead, or use 'Alert.init(state:)' to create an alert in iOS 13. - """ -) -extension View { - /// Displays an alert when then store's state becomes non-`nil`, and dismisses it when it becomes - /// `nil`. - /// - /// - Parameters: - /// - store: A store that describes if the alert is shown or dismissed. - /// - dismiss: An action to send when the alert is dismissed through non-user actions, such - /// as when an alert is automatically dismissed by the system. Use this action to `nil` out - /// the associated alert state. - @ViewBuilder public func alert( - _ store: Store?, Action>, - dismiss: Action - ) -> some View { - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - self.modifier( - NewAlertModifier( - viewStore: ViewStore(store, observe: { $0 }, removeDuplicates: { $0?.id == $1?.id }), - dismiss: dismiss - ) - ) - } else { - self.modifier( - OldAlertModifier( - viewStore: ViewStore(store, observe: { $0 }, removeDuplicates: { $0?.id == $1?.id }), - dismiss: dismiss - ) - ) - } - } -} - -// NB: Workaround for iOS 14 runtime crashes during iOS 15 availability checks. -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -private struct NewAlertModifier: ViewModifier { - @StateObject var viewStore: ViewStore?, Action> - let dismiss: Action - - func body(content: Content) -> some View { - content.alert( - (viewStore.state?.title).map { Text($0) } ?? Text(""), - isPresented: viewStore.binding(send: dismiss).isPresent(), - presenting: viewStore.state, - actions: { - ForEach($0.buttons) { - Button($0) { action in - if let action = action { - viewStore.send(action) - } - } - } - }, - message: { $0.message.map { Text($0) } } - ) - } -} - -private struct OldAlertModifier: ViewModifier { - @ObservedObject var viewStore: ViewStore?, Action> - let dismiss: Action - - func body(content: Content) -> some View { - content.alert(item: viewStore.binding(send: dismiss)) { state in - Alert(state) { action in - if let action = action { - viewStore.send(action) - } - } - } - } -} diff --git a/Sources/ComposableArchitecture/SwiftUI/Binding.swift b/Sources/ComposableArchitecture/SwiftUI/Binding.swift index 7cf2ad00fedb..ccd453c4a583 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Binding.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Binding.swift @@ -15,7 +15,6 @@ import SwiftUI /// > SwiftUI components. /// /// Read for more information. -@dynamicMemberLookup @propertyWrapper public struct BindingState { /// The underlying value wrapped by the binding state. @@ -52,25 +51,6 @@ public struct BindingState { get { self } set { self = newValue } } - - /// Returns binding state to the resulting value of a given key path. - /// - /// - Parameter keyPath: A key path to a specific resulting value. - /// - Returns: A new bindable state. - @available( - *, - deprecated, - message: - """ - Chaining onto properties of binding state is deprecated. Instead of pattern matching into a deeper property of binding state, use 'Reducer.onChange(of:)' to detect changes to nested properties of binding state. Instead of using 'viewStore.binding(\\.$nested.property)', use dynamic member lookup ('viewStore.$nested.property'). - """ - ) - public subscript( - dynamicMember keyPath: WritableKeyPath - ) -> BindingState { - get { .init(wrappedValue: self.wrappedValue[keyPath: keyPath]) } - set { self.wrappedValue[keyPath: keyPath] = newValue.wrappedValue } - } } extension BindingState: Equatable where Value: Equatable { @@ -242,20 +222,6 @@ extension BindingAction: CustomDumpStringConvertible { } } -extension BindingAction { - @available(*, deprecated, message: "Use 'BindingViewState' instead.") - public func pullback( - _ keyPath: WritableKeyPath - ) -> BindingAction { - .init( - keyPath: (keyPath as AnyKeyPath).appending(path: self.keyPath) as! PartialKeyPath, - set: { self.set(&$0[keyPath: keyPath]) }, - value: self.value, - valueIsEqualTo: self.valueIsEqualTo - ) - } -} - /// An action type that exposes a `binding` case that holds a ``BindingAction``. /// /// Used in conjunction with ``BindingState`` to safely eliminate the boilerplate typically @@ -317,41 +283,6 @@ extension ViewStore where ViewAction: BindableAction, ViewAction.State == ViewSt } ) } - - @available(iOS, deprecated: 9999, message: "Use 'viewStore.$value' instead.") - @available(macOS, deprecated: 9999, message: "Use 'viewStore.$value' instead.") - @available(tvOS, deprecated: 9999, message: "Use 'viewStore.$value' instead.") - @available(watchOS, deprecated: 9999, message: "Use 'viewStore.$value' instead.") - public func binding( - _ keyPath: WritableKeyPath>, - fileID: StaticString = #fileID, - line: UInt = #line - ) -> Binding { - self.binding( - get: { $0[keyPath: keyPath].wrappedValue }, - send: { [isInvalidated = self._isInvalidated] value in - #if DEBUG - let debugger = BindableActionViewStoreDebugger( - value: value, - bindableActionType: ViewAction.self, - context: .viewStore, - isInvalidated: isInvalidated, - fileID: fileID, - line: line - ) - let set: @Sendable (inout ViewState) -> Void = { - $0[keyPath: keyPath].wrappedValue = value - debugger.wasCalled = true - } - #else - let set: @Sendable (inout ViewState) -> Void = { - $0[keyPath: keyPath].wrappedValue = value - } - #endif - return .binding(.init(keyPath: keyPath, set: set, value: value)) - } - ) - } } /// A property wrapper type that can designate properties of view state that can be directly @@ -509,9 +440,7 @@ extension ViewStore { _ store: Store, observe toViewState: @escaping (_ state: BindingViewStore) -> ViewState, send fromViewAction: @escaping (_ viewAction: ViewAction) -> Action, - removeDuplicates isDuplicate: @escaping (_ lhs: ViewState, _ rhs: ViewState) -> Bool, - file: StaticString = #fileID, - line: UInt = #line + removeDuplicates isDuplicate: @escaping (_ lhs: ViewState, _ rhs: ViewState) -> Bool ) where ViewAction: BindableAction, ViewAction.State == State { self.init( store, @@ -763,6 +692,8 @@ extension WithViewStore where ViewState: Equatable, Content: View { deinit { guard !self.isInvalidated() else { return } guard self.wasCalled else { + var value = "" + customDump(self.value, to: &value, maxDepth: 0) runtimeWarn( """ A binding action sent from a view store \ @@ -770,7 +701,7 @@ extension WithViewStore where ViewState: Equatable, Content: View { "\(self.fileID):\(self.line)" was not handled. … Action: - \(typeName(self.bindableActionType)).binding(.set(_, \(self.value))) + \(typeName(self.bindableActionType)).binding(.set(_, \(value))) To fix this, invoke "BindingReducer()" from your feature reducer's "body". """ diff --git a/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift b/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift index af7c47d362fe..dc1304445f30 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift @@ -66,97 +66,3 @@ extension View { } } } - -@available( - *, - deprecated, - message: - """ - Use 'View.confirmationDialog(store:)' with 'PresentationState' and 'PresentationAction' instead, or use 'ActionSheet.init(state:)' to create a confirmation dialog in iOS 13. - """ -) -extension View { - /// Displays a dialog when the store's state becomes non-`nil`, and dismisses it when it becomes - /// `nil`. - /// - /// - Parameters: - /// - store: A store that describes if the dialog is shown or dismissed. - /// - dismiss: An action to send when the dialog is dismissed through non-user actions, such - /// as when a dialog is automatically dismissed by the system. Use this action to `nil` out - /// the associated dialog state. - @available(iOS 13, *) - @available(macOS 12, *) - @available(tvOS 13, *) - @available(watchOS 6, *) - @ViewBuilder public func confirmationDialog( - _ store: Store?, Action>, - dismiss: Action - ) -> some View { - if #available(iOS 15, tvOS 15, watchOS 8, *) { - self.modifier( - NewConfirmationDialogModifier( - viewStore: ViewStore(store, observe: { $0 }, removeDuplicates: { $0?.id == $1?.id }), - dismiss: dismiss - ) - ) - } else { - #if !os(macOS) - self.modifier( - OldConfirmationDialogModifier( - viewStore: ViewStore(store, observe: { $0 }, removeDuplicates: { $0?.id == $1?.id }), - dismiss: dismiss - ) - ) - #endif - } - } -} - -// NB: Workaround for iOS 14 runtime crashes during iOS 15 availability checks. -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -private struct NewConfirmationDialogModifier: ViewModifier { - @StateObject var viewStore: ViewStore?, Action> - let dismiss: Action - - func body(content: Content) -> some View { - content.confirmationDialog( - (viewStore.state?.title).map { Text($0) } ?? Text(""), - isPresented: viewStore.binding(send: dismiss).isPresent(), - titleVisibility: viewStore.state.map { .init($0.titleVisibility) } ?? .automatic, - presenting: viewStore.state, - actions: { - ForEach($0.buttons) { - Button($0) { action in - if let action = action { - viewStore.send(action) - } - } - } - }, - message: { $0.message.map { Text($0) } } - ) - } -} - -@available(iOS 13, *) -@available(macOS 12, *) -@available(tvOS 13, *) -@available(watchOS 6, *) -private struct OldConfirmationDialogModifier: ViewModifier { - @ObservedObject var viewStore: ViewStore?, Action> - let dismiss: Action - - func body(content: Content) -> some View { - #if !os(macOS) - content.actionSheet(item: viewStore.binding(send: dismiss)) { - ActionSheet($0) { action in - if let action = action { - viewStore.send(action) - } - } - } - #else - EmptyView() - #endif - } -} diff --git a/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift b/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift index 53123b354a92..fc384833b6a5 100644 --- a/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift @@ -146,1119 +146,6 @@ extension CaseLet where EnumAction == CaseAction { } } -/// A view that covers any cases that aren't addressed in a ``SwitchStore``. -/// -/// If you wish to use ``SwitchStore`` in a non-exhaustive manner (i.e. you do not want to provide -/// a ``CaseLet`` for each case of the enum), then you must insert a ``Default`` view at the end of -/// the ``SwitchStore``'s body. -@available( - *, - deprecated, - message: - "Use the 'SwitchStore.init' that is passed state to 'switch' over, and use 'default' instead." -) -public struct Default: View { - private let content: Content - - /// Initializes a ``Default`` view that computes content depending on if a store of enum state - /// does not match a particular case. - /// - /// - Parameter content: A function that returns a view that is visible only when the switch - /// store's state does not match a preceding ``CaseLet`` view. - public init(@ViewBuilder content: () -> Content) { - self.content = content() - } - - public var body: some View { - self.content - } -} - -extension SwitchStore { - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init( - _ store: Store, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - CaseLet, - Default - > - { - let content = content().value - self.init(store) { state in - if content.0.toCaseState(state) != nil { - content.0 - } else { - content.1 - } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init( - _ store: Store, - fileID: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> CaseLet - ) - where - Content == _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - { - self.init(store) { - content() - Default { _ExhaustivityCheckView(fileID: fileID, line: line) } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init( - _ store: Store, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - Default - > - { - let content = content().value - self.init(store) { state in - if content.0.toCaseState(state) != nil { - content.0 - } else if content.1.toCaseState(state) != nil { - content.1 - } else { - content.2 - } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init( - _ store: Store, - fileID: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - Default<_ExhaustivityCheckView> - > - { - let content = content() - self.init(store) { - content.value.0 - content.value.1 - Default { _ExhaustivityCheckView(fileID: fileID, line: line) } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - Default - > - > - { - let content = content().value - self.init(store) { state in - if content.0.toCaseState(state) != nil { - content.0 - } else if content.1.toCaseState(state) != nil { - content.1 - } else if content.2.toCaseState(state) != nil { - content.2 - } else { - content.3 - } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init( - _ store: Store, - fileID: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - > - { - let content = content() - self.init(store) { - content.value.0 - content.value.1 - content.value.2 - Default { _ExhaustivityCheckView(fileID: fileID, line: line) } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - Default - > - { - let content = content().value - self.init(store) { state in - if content.0.toCaseState(state) != nil { - content.0 - } else if content.1.toCaseState(state) != nil { - content.1 - } else if content.2.toCaseState(state) != nil { - content.2 - } else if content.3.toCaseState(state) != nil { - content.3 - } else { - content.4 - } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4 - >( - _ store: Store, - fileID: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - Default<_ExhaustivityCheckView> - > - { - let content = content() - self.init(store) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - Default { _ExhaustivityCheckView(fileID: fileID, line: line) } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - State5, Action5, Content5, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - CaseLet, - Default - > - > - { - let content = content().value - self.init(store) { state in - if content.0.toCaseState(state) != nil { - content.0 - } else if content.1.toCaseState(state) != nil { - content.1 - } else if content.2.toCaseState(state) != nil { - content.2 - } else if content.3.toCaseState(state) != nil { - content.3 - } else if content.4.toCaseState(state) != nil { - content.4 - } else { - content.5 - } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - State5, Action5, Content5 - >( - _ store: Store, - fileID: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - > - { - let content = content() - self.init(store) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - Default { _ExhaustivityCheckView(fileID: fileID, line: line) } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - State5, Action5, Content5, - State6, Action6, Content6, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - Default - > - > - { - let content = content().value - self.init(store) { state in - if content.0.toCaseState(state) != nil { - content.0 - } else if content.1.toCaseState(state) != nil { - content.1 - } else if content.2.toCaseState(state) != nil { - content.2 - } else if content.3.toCaseState(state) != nil { - content.3 - } else if content.4.toCaseState(state) != nil { - content.4 - } else if content.5.toCaseState(state) != nil { - content.5 - } else { - content.6 - } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - State5, Action5, Content5, - State6, Action6, Content6 - >( - _ store: Store, - fileID: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - Default<_ExhaustivityCheckView> - > - > - { - let content = content() - self.init(store) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - Default { _ExhaustivityCheckView(fileID: fileID, line: line) } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - State5, Action5, Content5, - State6, Action6, Content6, - State7, Action7, Content7, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - Default - > - > - > - { - let content = content().value - self.init(store) { state in - if content.0.toCaseState(state) != nil { - content.0 - } else if content.1.toCaseState(state) != nil { - content.1 - } else if content.2.toCaseState(state) != nil { - content.2 - } else if content.3.toCaseState(state) != nil { - content.3 - } else if content.4.toCaseState(state) != nil { - content.4 - } else if content.5.toCaseState(state) != nil { - content.5 - } else if content.6.toCaseState(state) != nil { - content.6 - } else { - content.7 - } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - State5, Action5, Content5, - State6, Action6, Content6, - State7, Action7, Content7 - >( - _ store: Store, - fileID: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - > - > - { - let content = content() - self.init(store) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - content.value.6 - Default { _ExhaustivityCheckView(fileID: fileID, line: line) } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - State5, Action5, Content5, - State6, Action6, Content6, - State7, Action7, Content7, - State8, Action8, Content8, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - > - >, - Default - > - { - let content = content().value - self.init(store) { state in - if content.0.toCaseState(state) != nil { - content.0 - } else if content.1.toCaseState(state) != nil { - content.1 - } else if content.2.toCaseState(state) != nil { - content.2 - } else if content.3.toCaseState(state) != nil { - content.3 - } else if content.4.toCaseState(state) != nil { - content.4 - } else if content.5.toCaseState(state) != nil { - content.5 - } else if content.6.toCaseState(state) != nil { - content.6 - } else if content.7.toCaseState(state) != nil { - content.7 - } else { - content.8 - } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - State5, Action5, Content5, - State6, Action6, Content6, - State7, Action7, Content7, - State8, Action8, Content8 - >( - _ store: Store, - fileID: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - > - >, - Default<_ExhaustivityCheckView> - > - { - let content = content() - self.init(store) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - content.value.6 - content.value.7 - Default { _ExhaustivityCheckView(fileID: fileID, line: line) } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - State5, Action5, Content5, - State6, Action6, Content6, - State7, Action7, Content7, - State8, Action8, Content8, - State9, Action9, Content9, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - > - >, - _ConditionalContent< - CaseLet, - Default - > - > - { - let content = content().value - self.init(store) { state in - if content.0.toCaseState(state) != nil { - content.0 - } else if content.1.toCaseState(state) != nil { - content.1 - } else if content.2.toCaseState(state) != nil { - content.2 - } else if content.3.toCaseState(state) != nil { - content.3 - } else if content.4.toCaseState(state) != nil { - content.4 - } else if content.5.toCaseState(state) != nil { - content.5 - } else if content.6.toCaseState(state) != nil { - content.6 - } else if content.7.toCaseState(state) != nil { - content.7 - } else if content.8.toCaseState(state) != nil { - content.8 - } else { - content.9 - } - } - } - - @available( - *, - deprecated, - message: "Use the 'SwitchStore.init' that is passed state to explicitly 'switch' over instead." - ) - public init< - State1, Action1, Content1, - State2, Action2, Content2, - State3, Action3, Content3, - State4, Action4, Content4, - State5, Action5, Content5, - State6, Action6, Content6, - State7, Action7, Content7, - State8, Action8, Content8, - State9, Action9, Content9 - >( - _ store: Store, - fileID: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - > - >, - _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - > - { - let content = content() - self.init(store) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - content.value.6 - content.value.7 - content.value.8 - Default { _ExhaustivityCheckView(fileID: fileID, line: line) } - } - } -} - -@available( - iOS, - deprecated: 9999, - message: "Use the 'SwitchStore.init' that can 'switch' over a given 'state' instead." -) -@available( - macOS, - deprecated: 9999, - message: "Use the 'SwitchStore.init' that can 'switch' over a given 'state' instead." -) -@available( - tvOS, - deprecated: 9999, - message: "Use the 'SwitchStore.init' that can 'switch' over a given 'state' instead." -) -@available( - watchOS, - deprecated: 9999, - message: "Use the 'SwitchStore.init' that can 'switch' over a given 'state' instead." -) -public struct _ExhaustivityCheckView: View { - @EnvironmentObject private var store: StoreObservableObject - let fileID: StaticString - let line: UInt - - public var body: some View { - #if DEBUG - let message = """ - Warning: SwitchStore.body@\(self.fileID):\(self.line) - - "\(debugCaseOutput(self.store.wrappedValue.state.value))" was encountered by a \ - "SwitchStore" that does not handle this case. - - Make sure that you exhaustively provide a "CaseLet" view for each case in "\(State.self)", \ - or provide a "Default" view at the end of the "SwitchStore". - """ - return VStack(spacing: 17) { - #if os(macOS) - Text("⚠️") - #else - Image(systemName: "exclamationmark.triangle.fill") - .font(.largeTitle) - #endif - - Text(message) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .foregroundColor(.white) - .padding() - .background(Color.red.edgesIgnoringSafeArea(.all)) - .onAppear { - runtimeWarn( - """ - A "SwitchStore" at "\(self.fileID):\(self.line)" does not handle the current case. … - - Unhandled case: - \(debugCaseOutput(self.store.wrappedValue.state.value)) - - Make sure that you exhaustively provide a "CaseLet" view for each case in your state, \ - or provide a "Default" view at the end of the "SwitchStore". - """ - ) - } - #else - return EmptyView() - #endif - } -} - public struct _CaseLetMismatchView: View { @EnvironmentObject private var store: StoreObservableObject let fileID: StaticString diff --git a/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift b/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift index f41c6c4b0454..4de995e089e8 100644 --- a/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift @@ -139,17 +139,28 @@ public struct WithViewStore: View { self.viewStore = ViewStore(store, observe: { $0 }, removeDuplicates: isDuplicate) } - /// Prints debug information to the console whenever the view is computed. - /// - /// - Parameter prefix: A string with which to prefix all debug messages. - /// - Returns: A structure that prints debug messages for all computations. - public func _printChanges(_ prefix: String = "") -> Self { - var view = self - #if DEBUG - view.prefix = prefix - #endif - return view - } + #if swift(>=5.8) + /// Prints debug information to the console whenever the view is computed. + /// + /// - Parameter prefix: A string with which to prefix all debug messages. + /// - Returns: A structure that prints debug messages for all computations. + @_documentation(visibility:public) + public func _printChanges(_ prefix: String = "") -> Self { + var view = self + #if DEBUG + view.prefix = prefix + #endif + return view + } + #else + public func _printChanges(_ prefix: String = "") -> Self { + var view = self + #if DEBUG + view.prefix = prefix + #endif + return view + } + #endif public var body: Content { #if DEBUG @@ -352,49 +363,6 @@ public struct WithViewStore: View { line: line ) } - - /// Initializes a structure that transforms a store into an observable view store in order to - /// compute views from store state. - /// - /// > Warning: This initializer is deprecated. Use - /// ``WithViewStore/init(_:observe:removeDuplicates:content:file:line:)`` to make state - /// observation explicit. - /// > - /// > When using ``WithViewStore`` you should take care to observe only the pieces of state that - /// your view needs to do its job, especially towards the root of the application. See - /// for more details. - /// - /// - Parameters: - /// - store: A store. - /// - isDuplicate: A function to determine when two `ViewState` values are equal. When values - /// are equal, repeat view computations are removed. - /// - content: A function that can generate content from a view store. - @available( - *, deprecated, - message: - """ - Use 'init(_:observe:removeDuplicates:content:)' to make state observation explicit. - - When using WithViewStore you should take care to observe only the pieces of state that your view needs to do its job, especially towards the root of the application. See the performance article for more details: - - https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/performance#View-stores - """ - ) - public init( - _ store: Store, - removeDuplicates isDuplicate: @escaping (_ lhs: ViewState, _ rhs: ViewState) -> Bool, - @ViewBuilder content: @escaping (_ viewStore: ViewStore) -> Content, - file: StaticString = #fileID, - line: UInt = #line - ) { - self.init( - store: store, - removeDuplicates: isDuplicate, - content: content, - file: file, - line: line - ) - } } extension WithViewStore where ViewState: Equatable, Content: View { @@ -573,40 +541,6 @@ extension WithViewStore where ViewState: Equatable, Content: View { line: line ) } - - /// Initializes a structure that transforms a store into an observable view store in order to - /// compute views from equatable store state. - /// - /// > Warning: This initializer is deprecated. Use - /// ``WithViewStore/init(_:observe:content:file:line:)`` to make state - /// observation explicit. - /// > - /// > When using ``WithViewStore`` you should take care to observe only the pieces of state that - /// your view needs to do its job, especially towards the root of the application. See - /// for more details. - /// - /// - Parameters: - /// - store: A store of equatable state. - /// - content: A function that can generate content from a view store. - @available( - *, deprecated, - message: - """ - Use 'init(_:observe:content:)' to make state observation explicit. - - When using WithViewStore you should take care to observe only the pieces of state that your view needs to do its job, especially towards the root of the application. See the performance article for more details: - - https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/performance#View-stores - """ - ) - public init( - _ store: Store, - @ViewBuilder content: @escaping (_ viewStore: ViewStore) -> Content, - file: StaticString = #fileID, - line: UInt = #line - ) { - self.init(store, removeDuplicates: ==, content: content, file: file, line: line) - } } extension WithViewStore: DynamicViewContent diff --git a/Sources/ComposableArchitecture/TestStore.swift b/Sources/ComposableArchitecture/TestStore.swift index b7e84ce30f55..e2c577641f59 100644 --- a/Sources/ComposableArchitecture/TestStore.swift +++ b/Sources/ComposableArchitecture/TestStore.swift @@ -263,11 +263,11 @@ import XCTestDynamicOverlay /// complete before the test is finished. To turn off exhaustivity you can set ``exhaustivity`` /// to ``Exhaustivity/off``. When that is done the ``TestStore``'s behavior changes: /// -/// * The trailing closures of ``send(_:assert:file:line:)-1ax61`` and -/// ``receive(_:timeout:assert:file:line:)-1rwdd`` no longer need to assert on all state +/// * The trailing closures of ``send(_:assert:file:line:)`` and +/// ``receive(_:timeout:assert:file:line:)-5awso`` no longer need to assert on all state /// changes. They can assert on any subset of changes, and only if they make an incorrect /// mutation will a test failure be reported. -/// * The ``send(_:assert:file:line:)-1ax61`` and ``receive(_:timeout:assert:file:line:)-1rwdd`` +/// * The ``send(_:assert:file:line:)`` and ``receive(_:timeout:assert:file:line:)-5awso`` /// methods are allowed to be called even when actions have been received from effects that have /// not been asserted on yet. Any pending actions will be cleared. /// * Tests are allowed to finish with unasserted, received actions and in-flight effects. No test @@ -423,7 +423,7 @@ import XCTestDynamicOverlay /// [merowing.info]: https://www.merowing.info /// [exhaustive-testing-in-tca]: https://www.merowing.info/exhaustive-testing-in-tca/ /// [Composable-Architecture-at-Scale]: https://vimeo.com/751173570 -public final class TestStore { +public final class TestStore { /// The current dependencies of the test store. /// @@ -473,45 +473,10 @@ public final class TestStore private let file: StaticString - private let fromScopedAction: (ScopedAction) -> Action private var line: UInt let reducer: TestReducer private let store: Store.TestAction> - private let toScopedState: (State) -> ScopedState /// Creates a test store with an initial state and a reducer powering its runtime. /// @@ -553,93 +515,16 @@ public final class TestStore( - initialState: @autoclosure () -> State, - @ReducerBuilder reducer: () -> R, - observe toScopedState: @escaping (State) -> ScopedState, - withDependencies prepareDependencies: (inout DependencyValues) -> Void = { _ in }, - file: StaticString = #file, - line: UInt = #line - ) - where - R.State == State, - R.Action == Action, - ScopedState: Equatable, - Action == ScopedAction, - Environment == Void - { - self.init( - initialState: initialState(), - reducer: reducer, - observe: toScopedState, - send: { $0 }, - withDependencies: prepareDependencies, - file: file, - line: line - ) - } - - @available( - *, - deprecated, - message: - """ - Test the reducer domain directly. To test view state and actions, write a unit test. - """ - ) - public init( - initialState: @autoclosure () -> State, - @ReducerBuilder reducer: () -> R, - observe toScopedState: @escaping (State) -> ScopedState, - send fromScopedAction: @escaping (ScopedAction) -> Action, - withDependencies prepareDependencies: (inout DependencyValues) -> Void = { _ in }, - file: StaticString = #file, - line: UInt = #line - ) - where - R.State == State, - R.Action == Action, - ScopedState: Equatable, - Environment == Void - { - let reducer = Dependencies.withDependencies(prepareDependencies) { - TestReducer(Reduce(reducer()), initialState: initialState()) - } - self._environment = .init(wrappedValue: ()) - self.file = file - self.fromScopedAction = fromScopedAction - self.line = line - self.reducer = reducer - self.store = Store(initialState: reducer.state, reducer: reducer) - self.timeout = 1 * NSEC_PER_SEC - self.toScopedState = toScopedState self.useMainSerialExecutor = true } @@ -665,80 +550,19 @@ public final class TestStore, - environment: Environment, - file: StaticString = #file, - line: UInt = #line - ) - where State == ScopedState, Action == ScopedAction { - let environment = Box(wrappedValue: environment) - let reducer = TestReducer( - Reduce( - reducer.pullback(state: \.self, action: .self, environment: { $0.wrappedValue }), - environment: environment - ), - initialState: initialState - ) - self._environment = environment - self.file = file - self.fromScopedAction = { $0 } - self.line = line - self.reducer = reducer - self.store = Store(initialState: initialState, reducer: reducer) - self.timeout = 1 * NSEC_PER_SEC - self.toScopedState = { $0 } - } - - init( - _environment: Box, - file: StaticString, - fromScopedAction: @escaping (ScopedAction) -> Action, - line: UInt, - reducer: TestReducer, - store: Store.Action>, - timeout: UInt64 = 1 * NSEC_PER_SEC, - toScopedState: @escaping (State) -> ScopedState - ) { - self._environment = _environment - self.file = file - self.fromScopedAction = fromScopedAction - self.line = line - self.reducer = reducer - self.store = store - self.timeout = timeout - self.toScopedState = toScopedState - } - // NB: Only needed until Xcode ships a macOS SDK that uses the 5.7 standard library. // See: https://forums.swift.org/t/xcode-14-rc-cannot-specialize-protocol-type/60171/15 #if (canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst)) @@ -782,7 +606,7 @@ public final class TestStore +/// let testStore: TestStore /// ``` /// /// You can specify a single generic: @@ -942,9 +766,9 @@ public final class TestStore /// ``` -public typealias TestStoreOf = TestStore +public typealias TestStoreOf = TestStore -extension TestStore where ScopedState: Equatable { +extension TestStore where State: Equatable { /// Sends an action to the store and asserts when state changes. /// /// To assert on how state changes you can provide a trailing closure, and that closure is handed @@ -1023,76 +847,78 @@ extension TestStore where ScopedState: Equatable { @MainActor @discardableResult public func send( - _ action: ScopedAction, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, + _ action: Action, + assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) async -> TestStoreTask { - if !self.reducer.receivedActions.isEmpty { - var actions = "" - customDump(self.reducer.receivedActions.map(\.action), to: &actions) - XCTFailHelper( - """ - Must handle \(self.reducer.receivedActions.count) received \ - action\(self.reducer.receivedActions.count == 1 ? "" : "s") before sending an action: … - - Unhandled actions: \(actions) - """, - file: file, - line: line - ) - } + await XCTFailContext.$current.withValue(XCTFailContext(file: file, line: line)) { + if !self.reducer.receivedActions.isEmpty { + var actions = "" + customDump(self.reducer.receivedActions.map(\.action), to: &actions) + XCTFailHelper( + """ + Must handle \(self.reducer.receivedActions.count) received \ + action\(self.reducer.receivedActions.count == 1 ? "" : "s") before sending an action: … - switch self.exhaustivity { - case .on: - break - case .off(showSkippedAssertions: true): - await self.skipReceivedActions(strict: false) - case .off(showSkippedAssertions: false): - self.reducer.receivedActions = [] - } + Unhandled actions: \(actions) + """, + file: file, + line: line + ) + } - let expectedState = self.toScopedState(self.state) - let previousState = self.reducer.state - let previousStackElementID = self.reducer.dependencies.stackElementID.incrementingCopy() - let task = self.store.send( - .init(origin: .send(self.fromScopedAction(action)), file: file, line: line), - originatingFrom: nil - ) - if uncheckedUseMainSerialExecutor { - await Task.yield() - } else { - for await _ in self.reducer.effectDidSubscribe.stream { + switch self.exhaustivity { + case .on: break - } - } - do { - let currentState = self.state - let currentStackElementID = self.reducer.dependencies.stackElementID - self.reducer.state = previousState - self.reducer.dependencies.stackElementID = previousStackElementID - defer { - self.reducer.state = currentState - self.reducer.dependencies.stackElementID = currentStackElementID + case .off(showSkippedAssertions: true): + await self.skipReceivedActions(strict: false) + case .off(showSkippedAssertions: false): + self.reducer.receivedActions = [] } - try self.expectedStateShouldMatch( - expected: expectedState, - actual: self.toScopedState(currentState), - updateStateToExpectedResult: updateStateToExpectedResult, - file: file, - line: line + let expectedState = self.state + let previousState = self.reducer.state + let previousStackElementID = self.reducer.dependencies.stackElementID.incrementingCopy() + let task = self.store.send( + .init(origin: .send(action), file: file, line: line), + originatingFrom: nil ) - } catch { - XCTFail("Threw error: \(error)", file: file, line: line) - } - if "\(self.file)" == "\(file)" { - self.line = line + if uncheckedUseMainSerialExecutor { + await Task.yield() + } else { + for await _ in self.reducer.effectDidSubscribe.stream { + break + } + } + do { + let currentState = self.state + let currentStackElementID = self.reducer.dependencies.stackElementID + self.reducer.state = previousState + self.reducer.dependencies.stackElementID = previousStackElementID + defer { + self.reducer.state = currentState + self.reducer.dependencies.stackElementID = currentStackElementID + } + + try self.expectedStateShouldMatch( + expected: expectedState, + actual: currentState, + updateStateToExpectedResult: updateStateToExpectedResult, + file: file, + line: line + ) + } catch { + XCTFail("Threw error: \(error)", file: file, line: line) + } + if "\(self.file)" == "\(file)" { + self.line = line + } + // NB: Give concurrency runtime more time to kick off effects so users don't need to manually + // instrument their effects. + await Task.megaYield(count: 20) + return .init(rawValue: task, timeout: self.timeout) } - // NB: Give concurrency runtime more time to kick off effects so users don't need to manually - // instrument their effects. - await Task.megaYield(count: 20) - return .init(rawValue: task, timeout: self.timeout) } /// Assert against the current state of the store. @@ -1126,121 +952,32 @@ extension TestStore where ScopedState: Equatable { /// store. @MainActor public func assert( - _ updateStateToExpectedResult: @escaping (_ state: inout ScopedState) throws -> Void, + _ updateStateToExpectedResult: @escaping (_ state: inout State) throws -> Void, file: StaticString = #file, line: UInt = #line ) { - let expectedState = self.toScopedState(self.state) - let currentState = self.reducer.state - do { - try self.expectedStateShouldMatch( - expected: expectedState, - actual: self.toScopedState(currentState), - updateStateToExpectedResult: updateStateToExpectedResult, - skipUnnecessaryModifyFailure: true, - file: file, - line: line - ) - } catch { - XCTFail("Threw error: \(error)", file: file, line: line) - } - } - - /// Sends an action to the store and asserts when state changes. - /// - /// This method returns a ``TestStoreTask``, which represents the lifecycle of the effect started - /// from sending an action. You can use this value to force the cancellation of the effect, which - /// is helpful for effects that are tied to a view's lifecycle and not torn down when an action is - /// sent, such as actions sent in SwiftUI's `task` view modifier. - /// - /// For example, if your feature kicks off a long-living effect when the view appears by using - /// SwiftUI's `task` view modifier, then you can write a test for such a feature by explicitly - /// canceling the effect's task after you make all assertions: - /// - /// ```swift - /// let store = TestStore(/* ... */) - /// - /// // emulate the view appearing - /// let task = await store.send(.task) - /// - /// // assertions - /// - /// // emulate the view disappearing - /// await task.cancel() - /// ``` - /// - /// - Parameters: - /// - action: An action. - /// - updateStateToExpectedResult: A closure that asserts state changed by sending the action to - /// the store. The mutable state sent to this closure must be modified to match the state of - /// the store after processing the given action. Do not provide a closure if no change is - /// expected. - /// - Returns: A ``TestStoreTask`` that represents the lifecycle of the effect executed when - /// sending the action. - @available(*, deprecated, message: "Call the async-friendly 'send' instead.") - @discardableResult - public func send( - _ action: ScopedAction, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, - file: StaticString = #file, - line: UInt = #line - ) -> TestStoreTask { - if !self.reducer.receivedActions.isEmpty { - var actions = "" - customDump(self.reducer.receivedActions.map(\.action), to: &actions) - XCTFailHelper( - """ - Must handle \(self.reducer.receivedActions.count) received \ - action\(self.reducer.receivedActions.count == 1 ? "" : "s") before sending an action: … - - Unhandled actions: \(actions) - """, - file: file, - line: line - ) - } - - switch self.exhaustivity { - case .on: - break - case .off(showSkippedAssertions: true): - self.skipReceivedActions(strict: false) - case .off(showSkippedAssertions: false): - self.reducer.receivedActions = [] - } - - let expectedState = self.toScopedState(self.state) - let previousState = self.state - let task = self.store.send( - .init(origin: .send(self.fromScopedAction(action)), file: file, line: line), - originatingFrom: nil - ) - do { - let currentState = self.state - self.reducer.state = previousState - defer { self.reducer.state = currentState } - - try self.expectedStateShouldMatch( - expected: expectedState, - actual: self.toScopedState(currentState), - updateStateToExpectedResult: updateStateToExpectedResult, - file: file, - line: line - ) - } catch { - XCTFail("Threw error: \(error)", file: file, line: line) - } - if "\(self.file)" == "\(file)" { - self.line = line + XCTFailContext.$current.withValue(XCTFailContext(file: file, line: line)) { + let expectedState = self.state + let currentState = self.reducer.state + do { + try self.expectedStateShouldMatch( + expected: expectedState, + actual: currentState, + updateStateToExpectedResult: updateStateToExpectedResult, + skipUnnecessaryModifyFailure: true, + file: file, + line: line + ) + } catch { + XCTFail("Threw error: \(error)", file: file, line: line) + } } - - return .init(rawValue: task, timeout: self.timeout) } private func expectedStateShouldMatch( - expected: ScopedState, - actual: ScopedState, - updateStateToExpectedResult: ((inout ScopedState) throws -> Void)? = nil, + expected: State, + actual: State, + updateStateToExpectedResult: ((inout State) throws -> Void)? = nil, skipUnnecessaryModifyFailure: Bool = false, file: StaticString, line: UInt @@ -1256,7 +993,7 @@ extension TestStore where ScopedState: Equatable { } let updateStateToExpectedResult = updateStateToExpectedResult.map { original in - { (state: inout ScopedState) in + { (state: inout State) in try XCTModifyLocals.$isExhaustive.withValue(self.exhaustivity == .on) { try original(&state) } @@ -1332,7 +1069,7 @@ extension TestStore where ScopedState: Equatable { } } - func expectationFailure(expected: ScopedState) { + func expectationFailure(expected: State) { let difference = diff(expected, actual, format: .proportional) .map { "\($0.indent(by: 4))\n\n(Expected: −, Actual: +)" } @@ -1379,31 +1116,10 @@ extension TestStore where ScopedState: Equatable { } } -extension TestStore where ScopedState: Equatable, Action: Equatable { - /// Asserts an action was received from an effect and asserts when state changes. - /// - /// See ``receive(_:timeout:assert:file:line:)-1rwdd`` for more information of how to use this - /// method. - /// - /// - Parameters: - /// - expectedAction: An action expected from an effect. - /// - updateStateToExpectedResult: A closure that asserts state changed by sending the action to - /// the store. The mutable state sent to this closure must be modified to match the state of - /// the store after processing the given action. Do not provide a closure if no change is - /// expected. - @available(*, deprecated, message: "Call the async-friendly 'receive' instead.") - public func receive( - _ expectedAction: Action, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, - file: StaticString = #file, - line: UInt = #line - ) { - self._receive(expectedAction, assert: updateStateToExpectedResult, file: file, line: line) - } - +extension TestStore where State: Equatable, Action: Equatable { private func _receive( _ expectedAction: Action, - assert updateStateToExpectedResult: ((inout ScopedState) throws -> Void)? = nil, + assert updateStateToExpectedResult: ((inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) { @@ -1472,7 +1188,7 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { public func receive( _ expectedAction: Action, timeout duration: Duration, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, + assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) async { @@ -1520,56 +1236,36 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { public func receive( _ expectedAction: Action, timeout nanoseconds: UInt64? = nil, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, + assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) async { - guard !self.reducer.inFlightEffects.isEmpty - else { + await XCTFailContext.$current.withValue(XCTFailContext(file: file, line: line)) { + guard !self.reducer.inFlightEffects.isEmpty + else { + _ = { + self._receive(expectedAction, assert: updateStateToExpectedResult, file: file, line: line) + }() + return + } + await self.receiveAction( + matching: { expectedAction == $0 }, + timeout: nanoseconds, + file: file, + line: line + ) _ = { self._receive(expectedAction, assert: updateStateToExpectedResult, file: file, line: line) }() - return + await Task.megaYield() } - await self.receiveAction( - matching: { expectedAction == $0 }, - timeout: nanoseconds, - file: file, - line: line - ) - _ = { - self._receive(expectedAction, assert: updateStateToExpectedResult, file: file, line: line) - }() - await Task.megaYield() } } -extension TestStore where ScopedState: Equatable { - /// Asserts a matching action was received from an effect and asserts how the state changes. - /// - /// See ``receive(_:timeout:assert:file:line:)-2ju31`` for more information of how to use this - /// method. - /// - /// - Parameters: - /// - isMatching: A closure that attempts to match an action. If it returns `false`, a test - /// failure is reported. - /// - updateStateToExpectedResult: A closure that asserts state changed by sending the action to - /// the store. The mutable state sent to this closure must be modified to match the state of - /// the store after processing the given action. Do not provide a closure if no change is - /// expected. - @available(*, deprecated, message: "Call the async-friendly 'receive' instead.") - public func receive( - _ isMatching: (_ action: Action) -> Bool, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, - file: StaticString = #file, - line: UInt = #line - ) { - self._receive(isMatching, assert: updateStateToExpectedResult, file: file, line: line) - } - +extension TestStore where State: Equatable { private func _receive( _ isMatching: (Action) -> Bool, - assert updateStateToExpectedResult: ((inout ScopedState) throws -> Void)? = nil, + assert updateStateToExpectedResult: ((inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) { @@ -1587,30 +1283,9 @@ extension TestStore where ScopedState: Equatable { ) } - /// Asserts an action was received matching a case path and asserts how the state changes. - /// - /// See ``receive(_:timeout:assert:file:line:)-8xkqt`` for more information of how to use this - /// method. - /// - /// - Parameters: - /// - actionCase: A case path identifying the case of an action enum to receive. - /// - updateStateToExpectedResult: A closure that asserts state changed by sending the action to - /// the store. The mutable state sent to this closure must be modified to match the state of - /// the store after processing the given action. Do not provide a closure if no change is - /// expected. - @available(*, deprecated, message: "Call the async-friendly 'receive' instead.") - public func receive( - _ actionCase: CasePath, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, - file: StaticString = #file, - line: UInt = #line - ) { - self._receive(actionCase, assert: updateStateToExpectedResult, file: file, line: line) - } - private func _receive( _ actionCase: CasePath, - assert updateStateToExpectedResult: ((inout ScopedState) throws -> Void)? = nil, + assert updateStateToExpectedResult: ((inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) { @@ -1634,7 +1309,7 @@ extension TestStore where ScopedState: Equatable { /// Asserts an action was received from an effect that matches a predicate, and asserts how the /// state changes. /// - /// This method is similar to ``receive(_:timeout:assert:file:line:)-4he05``, except it allows + /// This method is similar to ``receive(_:timeout:assert:file:line:)-5awso``, except it allows /// you to assert that an action was received that matches a predicate without asserting on all /// the data in the action: /// @@ -1653,7 +1328,7 @@ extension TestStore where ScopedState: Equatable { /// data was in the effect that you chose not to assert on. /// /// If you only want to check that a particular action case was received, then you might find - /// the ``receive(_:timeout:assert:file:line:)-4he05`` overload of this method more useful. + /// the ``receive(_:timeout:assert:file:line:)-5awso`` overload of this method more useful. /// /// - Parameters: /// - isMatching: A closure that attempts to match an action. If it returns `false`, a test @@ -1669,7 +1344,7 @@ extension TestStore where ScopedState: Equatable { public func receive( _ isMatching: (_ action: Action) -> Bool, timeout duration: Duration, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, + assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) async { @@ -1686,7 +1361,7 @@ extension TestStore where ScopedState: Equatable { /// Asserts an action was received from an effect that matches a predicate, and asserts how the /// state changes. /// - /// This method is similar to ``receive(_:timeout:assert:file:line:)-1rwdd``, except it allows you + /// This method is similar to ``receive(_:timeout:assert:file:line:)-5awso``, except it allows you /// to assert that an action was received that matches a predicate without asserting on all the /// data in the action: /// @@ -1705,7 +1380,7 @@ extension TestStore where ScopedState: Equatable { /// was in the effect that you chose not to assert on. /// /// If you only want to check that a particular action case was received, then you might find the - /// ``receive(_:timeout:assert:file:line:)-8xkqt`` overload of this method more useful. + /// ``receive(_:timeout:assert:file:line:)-6m8t6`` overload of this method more useful. /// /// - Parameters: /// - isMatching: A closure that attempts to match an action. If it returns `false`, a test @@ -1720,27 +1395,29 @@ extension TestStore where ScopedState: Equatable { public func receive( _ isMatching: (_ action: Action) -> Bool, timeout nanoseconds: UInt64? = nil, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, + assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) async { - guard !self.reducer.inFlightEffects.isEmpty - else { + await XCTFailContext.$current.withValue(XCTFailContext(file: file, line: line)) { + guard !self.reducer.inFlightEffects.isEmpty + else { + _ = { + self._receive(isMatching, assert: updateStateToExpectedResult, file: file, line: line) + }() + return + } + await self.receiveAction(matching: isMatching, timeout: nanoseconds, file: file, line: line) _ = { self._receive(isMatching, assert: updateStateToExpectedResult, file: file, line: line) }() - return + await Task.megaYield() } - await self.receiveAction(matching: isMatching, timeout: nanoseconds, file: file, line: line) - _ = { - self._receive(isMatching, assert: updateStateToExpectedResult, file: file, line: line) - }() - await Task.megaYield() } /// Asserts an action was received matching a case path and asserts how the state changes. /// - /// This method is similar to ``receive(_:timeout:assert:file:line:)-1rwdd``, except it allows you + /// This method is similar to ``receive(_:timeout:assert:file:line:)-5awso``, except it allows you /// to assert that an action was received that matches a particular case of the action enum /// without asserting on all the data in the action. /// @@ -1774,33 +1451,35 @@ extension TestStore where ScopedState: Equatable { public func receive( _ actionCase: CasePath, timeout nanoseconds: UInt64? = nil, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, + assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) async { - guard !self.reducer.inFlightEffects.isEmpty - else { + await XCTFailContext.$current.withValue(XCTFailContext(file: file, line: line)) { + guard !self.reducer.inFlightEffects.isEmpty + else { + _ = { + self._receive(actionCase, assert: updateStateToExpectedResult, file: file, line: line) + }() + return + } + await self.receiveAction( + matching: { actionCase.extract(from: $0) != nil }, + timeout: nanoseconds, + file: file, + line: line + ) _ = { self._receive(actionCase, assert: updateStateToExpectedResult, file: file, line: line) }() - return + await Task.megaYield() } - await self.receiveAction( - matching: { actionCase.extract(from: $0) != nil }, - timeout: nanoseconds, - file: file, - line: line - ) - _ = { - self._receive(actionCase, assert: updateStateToExpectedResult, file: file, line: line) - }() - await Task.megaYield() } #if (canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst)) /// Asserts an action was received matching a case path and asserts how the state changes. /// - /// This method is similar to ``receive(_:timeout:assert:file:line:)-4he05``, except it allows + /// This method is similar to ``receive(_:timeout:assert:file:line:)-5awso``, except it allows /// you to assert that an action was received that matches a particular case of the action enum /// without asserting on all the data in the action. /// @@ -1835,27 +1514,29 @@ extension TestStore where ScopedState: Equatable { public func receive( _ actionCase: CasePath, timeout duration: Duration, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, + assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) async { - guard !self.reducer.inFlightEffects.isEmpty - else { + await XCTFailContext.$current.withValue(XCTFailContext(file: file, line: line)) { + guard !self.reducer.inFlightEffects.isEmpty + else { + _ = { + self._receive(actionCase, assert: updateStateToExpectedResult, file: file, line: line) + }() + return + } + await self.receiveAction( + matching: { actionCase.extract(from: $0) != nil }, + timeout: duration.nanoseconds, + file: file, + line: line + ) _ = { self._receive(actionCase, assert: updateStateToExpectedResult, file: file, line: line) }() - return + await Task.megaYield() } - await self.receiveAction( - matching: { actionCase.extract(from: $0) != nil }, - timeout: duration.nanoseconds, - file: file, - line: line - ) - _ = { - self._receive(actionCase, assert: updateStateToExpectedResult, file: file, line: line) - }() - await Task.megaYield() } #endif @@ -1863,12 +1544,12 @@ extension TestStore where ScopedState: Equatable { matching predicate: (Action) -> Bool, failureMessage: @autoclosure () -> String, unexpectedActionDescription: (Action) -> String, - _ updateStateToExpectedResult: ((inout ScopedState) throws -> Void)?, + _ updateStateToExpectedResult: ((inout State) throws -> Void)?, file: StaticString, line: UInt ) { let updateStateToExpectedResult = updateStateToExpectedResult.map { original in - { (state: inout ScopedState) in + { (state: inout State) in try XCTModifyLocals.$isExhaustive.withValue(self.exhaustivity == .on) { try original(&state) } @@ -1933,11 +1614,11 @@ extension TestStore where ScopedState: Equatable { line: line ) } else { - let expectedState = self.toScopedState(self.state) + let expectedState = self.state do { try self.expectedStateShouldMatch( expected: expectedState, - actual: self.toScopedState(state), + actual: state, updateStateToExpectedResult: updateStateToExpectedResult, file: file, line: line @@ -2017,62 +1698,6 @@ extension TestStore where ScopedState: Equatable { } extension TestStore { - /// Scopes a store to assert against scoped state and actions. - /// - /// Useful for testing view store-specific state and actions. - /// - /// - Parameters: - /// - toScopedState: A function that transforms the reducer's state into scoped state. This - /// state will be asserted against as it is mutated by the reducer. Useful for testing view - /// store state transformations. - /// - fromScopedAction: A function that wraps a more scoped action in the reducer's action. - /// Scoped actions can be "sent" to the store, while any reducer action may be received. - /// Useful for testing view store action transformations. - @available( - *, - deprecated, - message: - """ - Use 'TestStore.init(initialState:reducer:observe:send:)' to scope a test store's state and actions. - """ - ) - public func scope( - state toScopedState: @escaping (ScopedState) -> S, - action fromScopedAction: @escaping (A) -> ScopedAction - ) -> TestStore { - .init( - _environment: self._environment, - file: self.file, - fromScopedAction: { self.fromScopedAction(fromScopedAction($0)) }, - line: self.line, - reducer: self.reducer, - store: self.store, - timeout: self.timeout, - toScopedState: { toScopedState(self.toScopedState($0)) } - ) - } - - /// Scopes a store to assert against scoped state. - /// - /// Useful for testing view store-specific state. - /// - /// - Parameter toScopedState: A function that transforms the reducer's state into scoped state. - /// This state will be asserted against as it is mutated by the reducer. Useful for testing view - /// store state transformations. - @available( - *, - deprecated, - message: - """ - Use 'TestStore.init(initialState:reducer:observe:)' to scope a test store's state. - """ - ) - public func scope( - state toScopedState: @escaping (ScopedState) -> S - ) -> TestStore { - self.scope(state: toScopedState, action: { $0 }) - } - /// Clears the queue of received actions from effects. /// /// Can be handy if you are writing an exhaustive test for a particular part of your feature, but @@ -2104,21 +1729,6 @@ extension TestStore { _ = { self._skipReceivedActions(strict: strict, file: file, line: line) }() } - /// Clears the queue of received actions from effects. - /// - /// The synchronous version of ``skipReceivedActions(strict:file:line:)-a4ri``. - /// - /// - Parameter strict: When `true` and there are no in-flight actions to cancel, a test failure - /// will be reported. - @available(*, deprecated, message: "Call the async-friendly 'skipReceivedActions' instead.") - public func skipReceivedActions( - strict: Bool = true, - file: StaticString = #file, - line: UInt = #line - ) { - self._skipReceivedActions(strict: strict, file: file, line: line) - } - private func _skipReceivedActions( strict: Bool = true, file: StaticString = #file, @@ -2184,21 +1794,6 @@ extension TestStore { _ = { self._skipInFlightEffects(strict: strict, file: file, line: line) }() } - /// Cancels any currently in-flight effects. - /// - /// The synchronous version of ``skipInFlightEffects(strict:file:line:)-5hbsk``. - /// - /// - Parameter strict: When `true` and there are no in-flight actions to cancel, a test failure - /// will be reported. - @available(*, deprecated, message: "Call the async-friendly 'skipInFlightEffects' instead.") - public func skipInFlightEffects( - strict: Bool = true, - file: StaticString = #file, - line: UInt = #line - ) { - self._skipInFlightEffects(strict: strict, file: file, line: line) - } - private func _skipInFlightEffects( strict: Bool = true, file: StaticString = #file, @@ -2262,8 +1857,8 @@ extension TestStore { } } -/// The type returned from ``TestStore/send(_:assert:file:line:)-1ax61`` that represents the -/// lifecycle of the effect started from sending an action. +/// The type returned from ``TestStore/send(_:assert:file:line:)`` that represents the lifecycle of +/// the effect started from sending an action. /// /// You can use this value in tests to cancel the effect started from sending an action: /// @@ -2290,7 +1885,7 @@ extension TestStore { /// See ``TestStore/finish(timeout:file:line:)-53gi5`` for the ability to await all in-flight /// effects in the test store. /// -/// See ``ViewStoreTask`` for the analog provided to ``ViewStore``. +/// See ``StoreTask`` for the analog provided to ``Store``. public struct TestStoreTask: Hashable, Sendable { fileprivate let rawValue: Task? fileprivate let timeout: UInt64 @@ -2434,25 +2029,25 @@ class TestReducer: Reducer { case .publisher, .run: let effect = LongLivingEffect(action: action) - return - EffectPublisherWrapper(effects) - .handleEvents( - receiveSubscription: { [effectDidSubscribe, weak self] _ in - self?.inFlightEffects.insert(effect) - Task { - await Task.megaYield() - effectDidSubscribe.continuation.yield() + return .publisher { [effectDidSubscribe, weak self] in + _EffectPublisher(effects) + .handleEvents( + receiveSubscription: { _ in + self?.inFlightEffects.insert(effect) + Task { + await Task.megaYield() + effectDidSubscribe.continuation.yield() + } + }, + receiveCompletion: { [weak self] _ in + self?.inFlightEffects.remove(effect) + }, + receiveCancel: { [weak self] in + self?.inFlightEffects.remove(effect) } - }, - receiveCompletion: { [weak self] _ in - self?.inFlightEffects.remove(effect) - }, - receiveCancel: { [weak self] in - self?.inFlightEffects.remove(effect) - } - ) - .map { .init(origin: .receive($0), file: action.file, line: action.line) } - .eraseToEffectPublisher() + ) + .map { .init(origin: .receive($0), file: action.file, line: action.line) } + } } } @@ -2512,8 +2107,8 @@ public enum Exhaustivity: Equatable, Sendable { /// ``TestStore/skipInFlightEffects(strict:file:line:)-5hbsk``. /// /// To partially match an action received from an effect, use - /// ``TestStore/receive(_:timeout:assert:file:line:)-8xkqt`` or - /// ``TestStore/receive(_:timeout:assert:file:line:)-2ju31``. + /// ``TestStore/receive(_:timeout:assert:file:line:)-6m8t6`` or + /// ``TestStore/receive(_:timeout:assert:file:line:)-7md3m``. case on @@ -2571,7 +2166,7 @@ extension TestStore { ) public func receive( _ expectedAction: Action, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, + assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) async { @@ -2583,8 +2178,8 @@ extension TestStore { *, unavailable, message: "'State' must conform to 'Equatable' to assert against sent actions." ) public func send( - _ action: ScopedAction, - assert updateStateToExpectedResult: ((_ state: inout ScopedState) throws -> Void)? = nil, + _ action: Action, + assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil, file: StaticString = #file, line: UInt = #line ) async -> TestStoreTask { diff --git a/Sources/ComposableArchitecture/ViewStore.swift b/Sources/ComposableArchitecture/ViewStore.swift index d67ab6b87cc8..60a99f6e3d5d 100644 --- a/Sources/ComposableArchitecture/ViewStore.swift +++ b/Sources/ComposableArchitecture/ViewStore.swift @@ -138,46 +138,6 @@ public final class ViewStore: ObservableObject { } } - /// Initializes a view store from a store. - /// - /// > Warning: This initializer is deprecated. Use - /// ``ViewStore/init(_:observe:removeDuplicates:)`` to make state observation explicit. - /// > - /// > When using ``ViewStore`` you should take care to observe only the pieces of state that - /// your view needs to do its job, especially towards the root of the application. See - /// for more details. - /// - /// - Parameters: - /// - store: A store. - /// - isDuplicate: A function to determine when two `State` values are equal. When values are - /// equal, repeat view computations are removed. - @available( - *, deprecated, - message: - """ - Use 'init(_:observe:removeDuplicates:)' to make state observation explicit. - - When using ViewStore you should take care to observe only the pieces of state that your view needs to do its job, especially towards the root of the application. See the performance article for more details: - - https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/performance#View-stores - """ - ) - public init( - _ store: Store, - removeDuplicates isDuplicate: @escaping (_ lhs: ViewState, _ rhs: ViewState) -> Bool - ) { - self._send = { store.send($0, originatingFrom: nil) } - self._state = CurrentValueRelay(store.state.value) - self._isInvalidated = store._isInvalidated - self.viewCancellable = store.state - .removeDuplicates(by: isDuplicate) - .sink { [weak objectWillChange = self.objectWillChange, weak _state = self._state] in - guard let objectWillChange = objectWillChange, let _state = _state else { return } - objectWillChange.send() - _state.value = $0 - } - } - init(_ viewStore: ViewStore) { self._send = viewStore._send self._state = viewStore._state @@ -623,44 +583,8 @@ extension ViewStore where ViewState: Equatable { ) { self.init(store, observe: toViewState, send: fromViewAction, removeDuplicates: ==) } - - /// Initializes a view store from a store. - /// - /// > Warning: This initializer is deprecated. Use - /// ``ViewStore/init(_:observe:)`` to make state observation explicit. - /// > - /// > When using ``ViewStore`` you should take care to observe only the pieces of state that - /// your view needs to do its job, especially towards the root of the application. See - /// for more details. - /// - /// - Parameters: - /// - store: A store. - @available( - *, deprecated, - message: - """ - Use 'init(_:observe:)' to make state observation explicit. - - When using ViewStore you should take care to observe only the pieces of state that your view needs to do its job, especially towards the root of the application. See the performance article for more details: - - https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/performance#View-stores - """ - ) - public convenience init(_ store: Store) { - self.init(store, removeDuplicates: ==) - } } -extension ViewStore where ViewState == Void { - @available(*, deprecated, message: "Send actions directly to 'store' instead.") - public convenience init(_ store: Store) { - self.init(store, observe: {}, removeDuplicates: ==) - } -} - -@available(*, deprecated, renamed: "StoreTask") -public typealias ViewStoreTask = StoreTask - private struct HashableWrapper: Hashable { let rawValue: Value static func == (lhs: Self, rhs: Self) -> Bool { false } diff --git a/Sources/swift-composable-architecture-benchmark/Effects.swift b/Sources/swift-composable-architecture-benchmark/Effects.swift index ce4560367634..72c9c2a1a577 100644 --- a/Sources/swift-composable-architecture-benchmark/Effects.swift +++ b/Sources/swift-composable-architecture-benchmark/Effects.swift @@ -20,7 +20,8 @@ let effectSuite = BenchmarkSuite(name: "Effects") { var didComplete = false $0.benchmark("Merged Effect.none (sink)") { doNotOptimizeAway( - effect.sink(receiveCompletion: { _ in didComplete = true }, receiveValue: { _ in }) + _EffectPublisher(effect) + .sink(receiveCompletion: { _ in didComplete = true }, receiveValue: { _ in }) ) } tearDown: { precondition(didComplete) diff --git a/Tests/ComposableArchitectureTests/DeprecatedTests.swift b/Tests/ComposableArchitectureTests/DeprecatedTests.swift deleted file mode 100644 index 5c98323c667d..000000000000 --- a/Tests/ComposableArchitectureTests/DeprecatedTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -import ComposableArchitecture -import XCTest - -@available(*, deprecated) -final class DeprecatedTests: BaseTCATestCase { - func testUncheckedStore() { - var expectations: [XCTestExpectation] = [] - for n in 1...100 { - let expectation = XCTestExpectation(description: "\(n)th iteration is complete") - expectations.append(expectation) - DispatchQueue.global().async { - let viewStore = ViewStore( - Store.unchecked( - initialState: 0, - reducer: AnyReducer { state, _, expectation in - state += 1 - if state == 2 { - return .fireAndForget { expectation.fulfill() } - } - return .none - }, - environment: expectation - ) - ) - viewStore.send(()) - DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { - viewStore.send(()) - } - } - } - - wait(for: expectations, timeout: 1) - } -} diff --git a/Tests/ComposableArchitectureTests/EffectCancellationTests.swift b/Tests/ComposableArchitectureTests/EffectCancellationTests.swift index 2bf5842e700f..7eaa957ced6b 100644 --- a/Tests/ComposableArchitectureTests/EffectCancellationTests.swift +++ b/Tests/ComposableArchitectureTests/EffectCancellationTests.swift @@ -267,7 +267,7 @@ final class EffectCancellationTests: BaseTCATestCase { } func testNestedMergeCancellation() async { - let effect = EffectPublisher.merge( + let effect = Effect.merge( .publisher { (1...2).publisher } .cancellable(id: 1) ) @@ -279,41 +279,6 @@ final class EffectCancellationTests: BaseTCATestCase { } XCTAssertEqual(output, [1, 2]) } - - @available(*, deprecated) - func testMultipleCancellations() async { - let mainQueue = DispatchQueue.test - let output = LockIsolated<[AnyHashable]>([]) - - struct A: Hashable {} - struct B: Hashable {} - struct C: Hashable {} - - let ids: [AnyHashable] = [A(), B(), C()] - let effects = Effect.merge( - ids.map { id in - .publisher { - Just(id) - .delay(for: 1, scheduler: mainQueue) - } - .cancellable(id: id) - } - ) - - let task = Task { - for await n in effects.actions { - output.withValue { $0.append(n) } - } - } - await Task.megaYield() // TODO: Does a yield have to be necessary here for cancellation? - - for await _ in Effect.cancel(ids: [A(), C()]).actions {} - - await mainQueue.advance(by: 1) - - await task.value - XCTAssertEqual(output.value, [B()]) - } } #if DEBUG @@ -353,7 +318,6 @@ final class EffectCancellationTests: BaseTCATestCase { XCTAssertEqual(_cancellationCancellables.exists(at: id, path: NavigationIDPath()), false) } - @available(*, deprecated) func testConcurrentCancels() { let queues = [ DispatchQueue.main, @@ -366,11 +330,11 @@ final class EffectCancellationTests: BaseTCATestCase { ] let ids = (1...10).map { _ in UUID() } - let effect = EffectPublisher.merge( - (1...1_000).map { idx -> EffectPublisher in + let effect = Effect.merge( + (1...1_000).map { idx -> Effect in let id = ids[idx % 10] - return EffectPublisher.merge( + return .merge( .publisher { Just(idx) .delay( @@ -392,7 +356,7 @@ final class EffectCancellationTests: BaseTCATestCase { let expectation = self.expectation(description: "wait") // NB: `for await _ in effect.actions` blows the stack with 1,000 merged publishers - effect + _EffectPublisher(effect) .sink(receiveCompletion: { _ in expectation.fulfill() }, receiveValue: { _ in }) .store(in: &self.cancellables) self.wait(for: [expectation], timeout: 999) @@ -439,29 +403,6 @@ final class EffectCancellationTests: BaseTCATestCase { } } - @available(*, deprecated) - func testNestedCancels() { - let id = UUID() - - var effect = Effect.publisher { - Empty(completeImmediately: false) - } - .cancellable(id: id) - - for _ in 1...1_000 { - effect = effect.cancellable(id: id) - } - - // NB: `for await _ in effect.actions` blows the stack with 1,000 chained publishers - effect - .sink(receiveValue: { _ in }) - .store(in: &cancellables) - - cancellables.removeAll() - - XCTAssertEqual(_cancellationCancellables.exists(at: id, path: NavigationIDPath()), false) - } - func testCancelIDHash() { struct CancelID1: Hashable {} struct CancelID2: Hashable {} diff --git a/Tests/ComposableArchitectureTests/EffectDebounceTests.swift b/Tests/ComposableArchitectureTests/EffectDebounceTests.swift deleted file mode 100644 index d5e6837e3a65..000000000000 --- a/Tests/ComposableArchitectureTests/EffectDebounceTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Combine -import ComposableArchitecture -import XCTest - -@MainActor -@available(*, deprecated) -final class EffectDebounceTests: BaseTCATestCase { - var cancellables: Set = [] - - func testDebounce() async { - let mainQueue = DispatchQueue.test - var values: [Int] = [] - - func runDebouncedEffect(value: Int) { - struct CancelToken: Hashable {} - Effect.send(value) - .debounce(id: CancelToken(), for: 1, scheduler: mainQueue) - .sink { values.append($0) } - .store(in: &self.cancellables) - } - - runDebouncedEffect(value: 1) - - // Nothing emits right away. - XCTAssertEqual(values, []) - - // Waiting half the time also emits nothing - await mainQueue.advance(by: 0.5) - XCTAssertEqual(values, []) - - // Run another debounced effect. - runDebouncedEffect(value: 2) - - // Waiting half the time emits nothing because the first debounced effect has been canceled. - await mainQueue.advance(by: 0.5) - XCTAssertEqual(values, []) - - // Run another debounced effect. - runDebouncedEffect(value: 3) - - // Waiting half the time emits nothing because the second debounced effect has been canceled. - await mainQueue.advance(by: 0.5) - XCTAssertEqual(values, []) - - // Waiting the rest of the time emits the final effect value. - await mainQueue.advance(by: 0.5) - XCTAssertEqual(values, [3]) - - // Running out the scheduler - await mainQueue.run() - XCTAssertEqual(values, [3]) - } - - func testDebounceIsLazy() async { - let mainQueue = DispatchQueue.test - var values: [Int] = [] - var effectRuns = 0 - - func runDebouncedEffect(value: Int) { - struct CancelToken: Hashable {} - - Deferred { () -> Just in - effectRuns += 1 - return Just(value) - } - .eraseToEffect() - .debounce(id: CancelToken(), for: 1, scheduler: mainQueue) - .sink { values.append($0) } - .store(in: &self.cancellables) - } - - runDebouncedEffect(value: 1) - - XCTAssertEqual(values, []) - XCTAssertEqual(effectRuns, 0) - - await mainQueue.advance(by: 0.5) - - XCTAssertEqual(values, []) - XCTAssertEqual(effectRuns, 0) - - await mainQueue.advance(by: 0.5) - - XCTAssertEqual(values, [1]) - XCTAssertEqual(effectRuns, 1) - } -} diff --git a/Tests/ComposableArchitectureTests/EffectDeferredTests.swift b/Tests/ComposableArchitectureTests/EffectDeferredTests.swift deleted file mode 100644 index f32c7d4b26f1..000000000000 --- a/Tests/ComposableArchitectureTests/EffectDeferredTests.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Combine -import ComposableArchitecture -import XCTest - -@available(*, deprecated) -final class EffectDeferredTests: BaseTCATestCase { - var cancellables: Set = [] - - func testDeferred() { - let mainQueue = DispatchQueue.test - var values: [Int] = [] - - func runDeferredEffect(value: Int) { - Just(value) - .eraseToEffect() - .deferred(for: 1, scheduler: mainQueue) - .sink { values.append($0) } - .store(in: &self.cancellables) - } - - runDeferredEffect(value: 1) - - // Nothing emits right away. - XCTAssertEqual(values, []) - - // Waiting half the time also emits nothing - mainQueue.advance(by: 0.5) - XCTAssertEqual(values, []) - - // Run another deferred effect. - runDeferredEffect(value: 2) - - // Waiting half the time emits first deferred effect received. - mainQueue.advance(by: 0.5) - XCTAssertEqual(values, [1]) - - // Run another deferred effect. - runDeferredEffect(value: 3) - - // Waiting half the time emits second deferred effect received. - mainQueue.advance(by: 0.5) - XCTAssertEqual(values, [1, 2]) - - // Waiting the rest of the time emits the final effect value. - mainQueue.advance(by: 0.5) - XCTAssertEqual(values, [1, 2, 3]) - - // Running out the scheduler - mainQueue.run() - XCTAssertEqual(values, [1, 2, 3]) - } - - func testDeferredIsLazy() { - let mainQueue = DispatchQueue.test - var values: [Int] = [] - var effectRuns = 0 - - func runDeferredEffect(value: Int) { - Deferred { () -> Just in - effectRuns += 1 - return Just(value) - } - .eraseToEffect() - .deferred(for: 1, scheduler: mainQueue) - .sink { values.append($0) } - .store(in: &self.cancellables) - } - - runDeferredEffect(value: 1) - - XCTAssertEqual(values, []) - XCTAssertEqual(effectRuns, 0) - - mainQueue.advance(by: 0.5) - - XCTAssertEqual(values, []) - XCTAssertEqual(effectRuns, 0) - - mainQueue.advance(by: 0.5) - - XCTAssertEqual(values, [1]) - XCTAssertEqual(effectRuns, 1) - } -} diff --git a/Tests/ComposableArchitectureTests/EffectFailureTests.swift b/Tests/ComposableArchitectureTests/EffectFailureTests.swift index 19a7a30d29c1..1e6a9136cd99 100644 --- a/Tests/ComposableArchitectureTests/EffectFailureTests.swift +++ b/Tests/ComposableArchitectureTests/EffectFailureTests.swift @@ -7,30 +7,6 @@ final class EffectFailureTests: BaseTCATestCase { var cancellables: Set = [] - func testTaskUnexpectedThrows() async { - guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { return } - - var line: UInt! - XCTExpectFailure { - $0.compactDescription == """ - An "Effect.task" returned from "\(#fileID):\(line+1)" threw an unhandled error. … - - EffectFailureTests.Unexpected() - - All non-cancellation errors must be explicitly handled via the "catch" parameter on \ - "Effect.task", or via a "do" block. - """ - } - - line = #line - let effect = Effect.task { - struct Unexpected: Error {} - throw Unexpected() - } - - for await _ in effect.actions {} - } - func testRunUnexpectedThrows() async { guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { return } diff --git a/Tests/ComposableArchitectureTests/EffectOperationTests.swift b/Tests/ComposableArchitectureTests/EffectOperationTests.swift index c49a7461aad9..c9b5f5eb93ca 100644 --- a/Tests/ComposableArchitectureTests/EffectOperationTests.swift +++ b/Tests/ComposableArchitectureTests/EffectOperationTests.swift @@ -15,7 +15,7 @@ XCTFail() } - effect = Effect.task { 42 } + effect = Effect.run { send in await send(42) } .merge(with: .none) switch effect.operation { case let .run(_, send): @@ -25,7 +25,7 @@ } effect = Effect.none - .merge(with: .task { 42 }) + .merge(with: .run { send in await send(42) }) switch effect.operation { case let .run(_, send): await send(.init(send: { XCTAssertEqual($0, 42) })) @@ -62,7 +62,7 @@ XCTFail() } - effect = Effect.task { 42 } + effect = Effect.run { send in await send(42) } .concatenate(with: .none) switch effect.operation { case let .run(_, send): @@ -72,7 +72,7 @@ } effect = Effect.none - .concatenate(with: .task { 42 }) + .concatenate(with: .run { send in await send(42) }) switch effect.operation { case let .run(_, send): await send(.init(send: { XCTAssertEqual($0, 42) })) @@ -80,7 +80,7 @@ XCTFail() } - effect = Effect.run { await $0(42) } + effect = Effect.run { send in await send(42) } .concatenate(with: .none) switch effect.operation { case let .run(_, send): @@ -90,7 +90,7 @@ } effect = Effect.none - .concatenate(with: .run { await $0(42) }) + .concatenate(with: .run { send in await send(42) }) switch effect.operation { case let .run(_, send): await send(.init(send: { XCTAssertEqual($0, 42) })) @@ -102,19 +102,19 @@ func testMergeFuses() async { var values = [Int]() - let effect = Effect.task { + let effect = Effect.run { send in try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10) - return 42 + await send(42) } .merge( - with: .task { + with: .run { send in try await Task.sleep(nanoseconds: NSEC_PER_SEC / 2) - return 1729 + await send(1729) } ) switch effect.operation { case let .run(_, send): - await send(.init(send: { values.append($0) })) + await send(.init { values.append($0) }) default: XCTFail() } @@ -125,8 +125,8 @@ func testConcatenateFuses() async { var values = [Int]() - let effect = Effect.task { 42 } - .concatenate(with: .task { 1729 }) + let effect = Effect.run { send in await send(42) } + .concatenate(with: .run { send in await send(1729) }) switch effect.operation { case let .run(_, send): await send(.init(send: { values.append($0) })) @@ -138,7 +138,7 @@ } func testMap() async { - let effect = Effect.task { 42 } + let effect = Effect.run { send in await send(42) } .map { "\($0)" } switch effect.operation { diff --git a/Tests/ComposableArchitectureTests/EffectPublisherTests.swift b/Tests/ComposableArchitectureTests/EffectPublisherTests.swift deleted file mode 100644 index bc78b0a5ba1a..000000000000 --- a/Tests/ComposableArchitectureTests/EffectPublisherTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Combine -import ComposableArchitecture -import XCTest - -@MainActor -@available(*, deprecated) -final class EffectPublisherTests: BaseTCATestCase { - var cancellables: Set = [] - - func testEscapedDependencies() { - @Dependency(\.date.now) var now - - let effect = withDependencies { - $0.date.now = Date(timeIntervalSince1970: 1_234_567_890) - } operation: { - Effect.publisher { - Just(now) - } - } - - var value: Date? - effect.sink { value = $0 }.store(in: &self.cancellables) - XCTAssertEqual(value, Date(timeIntervalSince1970: 1_234_567_890)) - } -} diff --git a/Tests/ComposableArchitectureTests/EffectTaskTests.swift b/Tests/ComposableArchitectureTests/EffectTaskTests.swift deleted file mode 100644 index 85556bf23bc1..000000000000 --- a/Tests/ComposableArchitectureTests/EffectTaskTests.swift +++ /dev/null @@ -1,125 +0,0 @@ -import Combine -import ComposableArchitecture -import XCTest - -@MainActor -final class EffectTaskTests: BaseTCATestCase { - func testTask() async { - struct State: Equatable {} - enum Action: Equatable { case tapped, response } - let store = TestStore(initialState: State()) { - Reduce { state, action in - switch action { - case .tapped: - return .task { .response } - case .response: - return .none - } - } - } - await store.send(.tapped) - await store.receive(.response) - } - - func testTaskCatch() async { - struct State: Equatable {} - enum Action: Equatable, Sendable { case tapped, response } - let store = TestStore(initialState: State()) { - Reduce { state, action in - switch action { - case .tapped: - return .task { - struct Failure: Error {} - throw Failure() - } catch: { _ in - .response - } - case .response: - return .none - } - } - } - await store.send(.tapped) - await store.receive(.response) - } - - #if DEBUG - func testTaskUnhandledFailure() async { - var line: UInt! - XCTExpectFailure(nil, enabled: nil, strict: nil) { - $0.compactDescription == """ - An "Effect.task" returned from "\(#fileID):\(line+1)" threw an unhandled error. … - - EffectTaskTests.Failure() - - All non-cancellation errors must be explicitly handled via the "catch" parameter on \ - "Effect.task", or via a "do" block. - """ - } - struct State: Equatable {} - enum Action: Equatable { case tapped, response } - let store = TestStore(initialState: State()) { - Reduce { state, action in - switch action { - case .tapped: - line = #line - return .task { - struct Failure: Error {} - throw Failure() - } - case .response: - return .none - } - } - } - // NB: We wait a long time here because XCTest failures take a long time to generate - await store.send(.tapped).finish(timeout: 5 * NSEC_PER_SEC) - } - #endif - - func testTaskCancellation() async { - enum CancelID { case response } - struct State: Equatable {} - enum Action: Equatable { case tapped, response } - let store = TestStore(initialState: State()) { - Reduce { state, action in - switch action { - case .tapped: - return .task { - Task.cancel(id: CancelID.response) - try Task.checkCancellation() - return .response - } - .cancellable(id: CancelID.response) - case .response: - return .none - } - } - } - await store.send(.tapped).finish() - } - - func testTaskCancellationCatch() async { - enum CancelID { case responseA } - struct State: Equatable {} - enum Action: Equatable { case tapped, responseA, responseB } - let store = TestStore(initialState: State()) { - Reduce { state, action in - switch action { - case .tapped: - return .task { - Task.cancel(id: CancelID.responseA) - try Task.checkCancellation() - return .responseA - } catch: { _ in - .responseB - } - .cancellable(id: CancelID.responseA) - case .responseA, .responseB: - return .none - } - } - } - await store.send(.tapped).finish() - } -} diff --git a/Tests/ComposableArchitectureTests/EffectTests.swift b/Tests/ComposableArchitectureTests/EffectTests.swift index b0c8a03e7f86..eef002ebcb00 100644 --- a/Tests/ComposableArchitectureTests/EffectTests.swift +++ b/Tests/ComposableArchitectureTests/EffectTests.swift @@ -7,61 +7,17 @@ final class EffectTests: BaseTCATestCase { var cancellables: Set = [] let mainQueue = DispatchQueue.test - @available(*, deprecated) - func testCatchToEffect() { - struct Error: Swift.Error, Equatable {} - - Future { $0(.success(42)) } - .catchToEffect() - .sink { XCTAssertEqual($0, .success(42)) } - .store(in: &self.cancellables) - - Future { $0(.failure(Error())) } - .catchToEffect() - .sink { XCTAssertEqual($0, .failure(Error())) } - .store(in: &self.cancellables) - - Future { $0(.success(42)) } - .eraseToEffect() - .sink { XCTAssertEqual($0, 42) } - .store(in: &self.cancellables) - - Future { $0(.success(42)) } - .catchToEffect { - switch $0 { - case let .success(val): - return val - case .failure: - return -1 - } - } - .sink { XCTAssertEqual($0, 42) } - .store(in: &self.cancellables) - - Future { $0(.failure(Error())) } - .catchToEffect { - switch $0 { - case let .success(val): - return val - case .failure: - return -1 - } - } - .sink { XCTAssertEqual($0, -1) } - .store(in: &self.cancellables) - } - #if (canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst)) func testConcatenate() async { if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { let clock = TestClock() let values = LockIsolated<[Int]>([]) - let effect = EffectTask.concatenate( + let effect = Effect.concatenate( (1...3).map { count in - .task { + .run { send in try await clock.sleep(for: .seconds(count)) - return count + await send(count) } } ) @@ -120,11 +76,11 @@ final class EffectTests: BaseTCATestCase { if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { let clock = TestClock() - let effect = EffectPublisher.merge( + let effect = Effect.merge( (1...3).map { count in - .task { + .run { send in try await clock.sleep(for: .seconds(count)) - return count + await send(count) } } ) @@ -153,92 +109,6 @@ final class EffectTests: BaseTCATestCase { } #endif - @available(*, deprecated) - func testEffectSubscriberInitializer() { - let effect = Effect.run { subscriber in - subscriber.send(1) - subscriber.send(2) - self.mainQueue.schedule(after: self.mainQueue.now.advanced(by: .seconds(1))) { - subscriber.send(3) - } - self.mainQueue.schedule(after: self.mainQueue.now.advanced(by: .seconds(2))) { - subscriber.send(4) - subscriber.send(completion: .finished) - } - - return AnyCancellable {} - } - - var values: [Int] = [] - var isComplete = false - effect - .sink(receiveCompletion: { _ in isComplete = true }, receiveValue: { values.append($0) }) - .store(in: &self.cancellables) - - XCTAssertEqual(values, [1, 2]) - XCTAssertEqual(isComplete, false) - - self.mainQueue.advance(by: 1) - - XCTAssertEqual(values, [1, 2, 3]) - XCTAssertEqual(isComplete, false) - - self.mainQueue.advance(by: 1) - - XCTAssertEqual(values, [1, 2, 3, 4]) - XCTAssertEqual(isComplete, true) - } - - @available(*, deprecated) - func testEffectSubscriberInitializer_WithCancellation() { - enum CancelID { case delay } - - let effect = Effect.run { subscriber in - subscriber.send(1) - self.mainQueue.schedule(after: self.mainQueue.now.advanced(by: .seconds(1))) { - subscriber.send(2) - } - - return AnyCancellable {} - } - .cancellable(id: CancelID.delay) - - var values: [Int] = [] - var isComplete = false - effect - .sink(receiveCompletion: { _ in isComplete = true }, receiveValue: { values.append($0) }) - .store(in: &self.cancellables) - - XCTAssertEqual(values, [1]) - XCTAssertEqual(isComplete, false) - - Effect.cancel(id: CancelID.delay) - .sink(receiveValue: { _ in }) - .store(in: &self.cancellables) - - self.mainQueue.advance(by: 1) - - XCTAssertEqual(values, [1]) - XCTAssertEqual(isComplete, true) - } - - @available(*, deprecated) - func testEffectErrorCrash() { - let expectation = self.expectation(description: "Complete") - - // This crashes on iOS 13 if Effect.init(error:) is implemented using the Fail publisher. - EffectPublisher(error: NSError(domain: "", code: 1)) - .retry(3) - .catch { _ in Fail(error: NSError(domain: "", code: 1)) } - .sink( - receiveCompletion: { _ in expectation.fulfill() }, - receiveValue: { _ in } - ) - .store(in: &self.cancellables) - - self.wait(for: [expectation], timeout: 0) - } - func testDoubleCancelInFlight() async { var result: Int? @@ -254,52 +124,6 @@ final class EffectTests: BaseTCATestCase { XCTAssertEqual(result, 42) } - #if DEBUG - @available(*, deprecated) - func testUnimplemented() { - let effect = Effect.unimplemented("unimplemented") - XCTExpectFailure { - effect - .sink(receiveValue: { _ in }) - .store(in: &self.cancellables) - } issueMatcher: { issue in - issue.compactDescription == "unimplemented - An unimplemented effect ran." - } - } - #endif - - @available(*, deprecated) - func testTask() async { - guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { return } - let effect = Effect.task { 42 } - for await result in effect.actions { - XCTAssertEqual(result, 42) - } - } - - @available(*, deprecated) - func testCancellingTask_Infallible() { - @Sendable func work() async -> Int { - do { - try await Task.sleep(nanoseconds: NSEC_PER_MSEC) - XCTFail() - } catch { - } - return 42 - } - - Effect.task { await work() } - .sink( - receiveCompletion: { _ in XCTFail() }, - receiveValue: { _ in XCTFail() } - ) - .store(in: &self.cancellables) - - self.cancellables = [] - - _ = XCTWaiter.wait(for: [.init()], timeout: 1.1) - } - func testDependenciesTransferredToEffects_Task() async { struct Feature: Reducer { enum Action: Equatable { @@ -310,8 +134,8 @@ final class EffectTests: BaseTCATestCase { func reduce(into state: inout Int, action: Action) -> Effect { switch action { case .tap: - return .task { - .response(Int(self.date.now.timeIntervalSinceReferenceDate)) + return .run { send in + await send(.response(Int(self.date.now.timeIntervalSinceReferenceDate))) } case let .response(value): state = value @@ -378,7 +202,7 @@ final class EffectTests: BaseTCATestCase { let effect = withDependencies { $0.date.now = Date(timeIntervalSince1970: 1_234_567_890) } operation: { - Effect.task {}.map { date() } + Effect.run { send in await send(()) }.map { date() } } output = nil for await date in effect.actions { diff --git a/Tests/ComposableArchitectureTests/EffectThrottleTests.swift b/Tests/ComposableArchitectureTests/EffectThrottleTests.swift deleted file mode 100644 index 158cb3f8af0c..000000000000 --- a/Tests/ComposableArchitectureTests/EffectThrottleTests.swift +++ /dev/null @@ -1,218 +0,0 @@ -import Combine -import ComposableArchitecture -import XCTest - -@MainActor -@available(*, deprecated) -final class EffectThrottleTests: BaseTCATestCase { - var cancellables: Set = [] - let mainQueue = DispatchQueue.test - - func testThrottleLatest() async { - struct CancelID: Hashable {} - defer { Task.cancel(id: CancelID()) } - - var values: [Int] = [] - var effectRuns = 0 - - func runThrottledEffect(value: Int) { - Deferred { () -> Just in - effectRuns += 1 - return Just(value) - } - .eraseToEffect() - .throttle( - id: CancelID(), for: 1, scheduler: mainQueue.eraseToAnyScheduler(), latest: true - ) - .sink { values.append($0) } - .store(in: &self.cancellables) - } - - runThrottledEffect(value: 1) - - await mainQueue.advance() - - // A value emits right away. - XCTAssertEqual(values, [1]) - - runThrottledEffect(value: 2) - - await mainQueue.advance() - - // A second value is throttled. - XCTAssertEqual(values, [1]) - - await mainQueue.advance(by: 0.25) - - runThrottledEffect(value: 3) - - await mainQueue.advance(by: 0.25) - - runThrottledEffect(value: 4) - - await mainQueue.advance(by: 0.25) - - runThrottledEffect(value: 5) - - // A third value is throttled. - XCTAssertEqual(values, [1]) - - await mainQueue.advance(by: 0.25) - - // The latest value emits. - XCTAssertEqual(values, [1, 5]) - } - - func testThrottleFirst() async { - struct CancelID: Hashable {} - defer { Task.cancel(id: CancelID()) } - - var values: [Int] = [] - var effectRuns = 0 - - func runThrottledEffect(value: Int) { - Deferred { () -> Just in - effectRuns += 1 - return Just(value) - } - .eraseToEffect() - .throttle( - id: CancelID(), for: 1, scheduler: mainQueue.eraseToAnyScheduler(), latest: false - ) - .sink { values.append($0) } - .store(in: &self.cancellables) - } - - runThrottledEffect(value: 1) - - await mainQueue.advance() - - // A value emits right away. - XCTAssertEqual(values, [1]) - - runThrottledEffect(value: 2) - - await mainQueue.advance() - - // A second value is throttled. - XCTAssertEqual(values, [1]) - - await mainQueue.advance(by: 0.25) - - runThrottledEffect(value: 3) - - await mainQueue.advance(by: 0.25) - - runThrottledEffect(value: 4) - - await mainQueue.advance(by: 0.25) - - runThrottledEffect(value: 5) - - await mainQueue.advance(by: 0.25) - - // The second (throttled) value emits. - XCTAssertEqual(values, [1, 2]) - - await mainQueue.advance(by: 0.25) - - runThrottledEffect(value: 6) - - await mainQueue.advance(by: 0.50) - - // A third value is throttled. - XCTAssertEqual(values, [1, 2]) - - runThrottledEffect(value: 7) - - await mainQueue.advance(by: 0.25) - - // The third (throttled) value emits. - XCTAssertEqual(values, [1, 2, 6]) - } - - func testThrottleAfterInterval() async { - struct CancelID: Hashable {} - - var values: [Int] = [] - var effectRuns = 0 - - func runThrottledEffect(value: Int) { - - Deferred { () -> Just in - effectRuns += 1 - return Just(value) - } - .eraseToEffect() - .throttle( - id: CancelID(), for: 1, scheduler: mainQueue.eraseToAnyScheduler(), latest: true - ) - .sink { values.append($0) } - .store(in: &self.cancellables) - } - - runThrottledEffect(value: 1) - - await mainQueue.advance() - - // A value emits right away. - XCTAssertEqual(values, [1]) - - await mainQueue.advance(by: 2) - - runThrottledEffect(value: 2) - - await mainQueue.advance() - - // A second value is emitted right away. - XCTAssertEqual(values, [1, 2]) - - await mainQueue.advance(by: 2) - - runThrottledEffect(value: 3) - - await mainQueue.advance() - - // A third value is emitted right away. - XCTAssertEqual(values, [1, 2, 3]) - } - - func testThrottleEmitsFirstValueOnce() async { - struct CancelID: Hashable {} - defer { Task.cancel(id: CancelID()) } - - var values: [Int] = [] - var effectRuns = 0 - - func runThrottledEffect(value: Int) { - Deferred { () -> Just in - effectRuns += 1 - return Just(value) - } - .eraseToEffect() - .throttle( - id: CancelID(), for: 1, scheduler: mainQueue.eraseToAnyScheduler(), latest: false - ) - .sink { values.append($0) } - .store(in: &self.cancellables) - } - - runThrottledEffect(value: 1) - - await mainQueue.advance() - - // A value emits right away. - XCTAssertEqual(values, [1]) - - await mainQueue.advance(by: 0.5) - - runThrottledEffect(value: 2) - - await mainQueue.advance(by: 0.5) - - runThrottledEffect(value: 3) - - // A second value is emitted right away. - XCTAssertEqual(values, [1, 2]) - } -} diff --git a/Tests/ComposableArchitectureTests/ReducerTests.swift b/Tests/ComposableArchitectureTests/ReducerTests.swift index 5732d2c92841..9e37f71f1013 100644 --- a/Tests/ComposableArchitectureTests/ReducerTests.swift +++ b/Tests/ComposableArchitectureTests/ReducerTests.swift @@ -36,7 +36,7 @@ final class ReducerTests: BaseTCATestCase { func reduce(into state: inout State, action: Action) -> Effect { state += 1 - return .fireAndForget { + return .run { _ in try await self.clock.sleep(for: self.delay) await self.setValue() } @@ -83,7 +83,7 @@ final class ReducerTests: BaseTCATestCase { let effect: @Sendable () async -> Void func reduce(into state: inout State, action: Action) -> Effect { state += 1 - return .fireAndForget { + return .run { _ in await self.effect() } } diff --git a/Tests/ComposableArchitectureTests/Reducers/ForEachReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/ForEachReducerTests.swift index 5196c12649c3..fc3330404d83 100644 --- a/Tests/ComposableArchitectureTests/Reducers/ForEachReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/ForEachReducerTests.swift @@ -81,7 +81,7 @@ final class ForEachReducerTests: BaseTCATestCase { case tick } @Dependency(\.continuousClock) var clock - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .startButtonTapped: return .run { send in diff --git a/Tests/ComposableArchitectureTests/Reducers/IfCaseLetReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/IfCaseLetReducerTests.swift index 9cb0df007059..24821c175b7f 100644 --- a/Tests/ComposableArchitectureTests/Reducers/IfCaseLetReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/IfCaseLetReducerTests.swift @@ -181,7 +181,7 @@ final class IfCaseLetReducerTests: BaseTCATestCase { case response(Int) } @Dependency(\.mainQueue) var mainQueue - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .tap: diff --git a/Tests/ComposableArchitectureTests/Reducers/PresentationReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/PresentationReducerTests.swift index 9955d260a27b..af68708a82eb 100644 --- a/Tests/ComposableArchitectureTests/Reducers/PresentationReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/PresentationReducerTests.swift @@ -197,7 +197,7 @@ final class PresentationReducerTests: BaseTCATestCase { func reduce(into state: inout State, action: Action) -> Effect { switch action { case .closeButtonTapped: - return .fireAndForget { + return .run { _ in await self.dismiss() } case .decrementButtonTapped: @@ -351,7 +351,7 @@ final class PresentationReducerTests: BaseTCATestCase { func reduce(into state: inout State, action: Action) -> Effect { switch action { case .closeButtonTapped: - return .fireAndForget { + return .run { _ in await self.dismiss() } @@ -742,7 +742,7 @@ final class PresentationReducerTests: BaseTCATestCase { func reduce(into state: inout State, action: Action) -> Effect { switch action { case .closeButtonTapped: - return .fireAndForget { + return .run { _ in await self.dismiss() } case .decrementButtonTapped: @@ -806,7 +806,7 @@ final class PresentationReducerTests: BaseTCATestCase { func reduce(into state: inout State, action: Action) -> Effect { switch action { case .closeButtonTapped: - return .fireAndForget { + return .run { _ in await self.dismiss() } case .startButtonTapped: @@ -976,7 +976,7 @@ final class PresentationReducerTests: BaseTCATestCase { func reduce(into state: inout State, action: Action) -> Effect { switch action { case .startButtonTapped: - return .fireAndForget { + return .run { _ in try await Task.never() } .cancellable(id: 42) @@ -1032,7 +1032,7 @@ final class PresentationReducerTests: BaseTCATestCase { func reduce(into state: inout State, action: Action) -> Effect { switch action { case .startButtonTapped: - return .fireAndForget { + return .run { _ in try await Task.never() } .cancellable(id: CancelID.effect) @@ -1059,7 +1059,7 @@ final class PresentationReducerTests: BaseTCATestCase { state.grandchild = Grandchild.State() return .none case .startButtonTapped: - return .fireAndForget { + return .run { _ in try await Task.never() } .cancellable(id: CancelID.effect) @@ -1454,9 +1454,9 @@ final class PresentationReducerTests: BaseTCATestCase { state.count = value return .none case .startButtonTapped: - return .task { + return .run { send in try await self.clock.sleep(for: .seconds(1)) - return .response(42) + await send(.response(42)) } .cancellable(id: CancelID.effect) case .stopButtonTapped: @@ -1506,9 +1506,9 @@ final class PresentationReducerTests: BaseTCATestCase { case .response: return .none case .startButtonTapped: - return .task { + return .run { send in try await clock.sleep(for: .seconds(0)) - return .response(42) + await send(.response(42)) } .cancellable(id: CancelID.effect) } @@ -1820,7 +1820,7 @@ final class PresentationReducerTests: BaseTCATestCase { case .dismissMe: return .none case .task: - return .fireAndForget { + return .run { _ in try await Task.never() } } diff --git a/Tests/ComposableArchitectureTests/Reducers/StackReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/StackReducerTests.swift index dcad7af3c7fd..a7dab23c4b59 100644 --- a/Tests/ComposableArchitectureTests/Reducers/StackReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/StackReducerTests.swift @@ -70,7 +70,7 @@ final class StackReducerTests: BaseTCATestCase { case decrementButtonTapped case incrementButtonTapped } - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .decrementButtonTapped: state.count -= 1 @@ -120,10 +120,10 @@ final class StackReducerTests: BaseTCATestCase { enum Action: Equatable { case onAppear } - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .onAppear: - return .fireAndForget { + return .run { _ in try await Task.never() } } @@ -178,14 +178,14 @@ final class StackReducerTests: BaseTCATestCase { case onAppear } @Dependency(\.dismiss) var dismiss - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .closeButtonTapped: - return .fireAndForget { + return .run { _ in await self.dismiss() } case .onAppear: - return .fireAndForget { + return .run { _ in try await Task.never() } } @@ -235,8 +235,8 @@ final class StackReducerTests: BaseTCATestCase { struct State: Equatable {} enum Action: Equatable { case tap } @Dependency(\.dismiss) var dismiss - func reduce(into state: inout State, action: Action) -> EffectTask { - .fireAndForget { await self.dismiss() } + func reduce(into state: inout State, action: Action) -> Effect { + .run { _ in await self.dismiss() } } } struct Parent: Reducer { @@ -283,10 +283,10 @@ final class StackReducerTests: BaseTCATestCase { } @Dependency(\.dismiss) var dismiss @Dependency(\.mainQueue) var mainQueue - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .onAppear: - return .fireAndForget { [count = state.count] in + return .run { [count = state.count] _ in try await self.mainQueue.sleep(for: .seconds(count)) await self.dismiss() } @@ -344,10 +344,10 @@ final class StackReducerTests: BaseTCATestCase { case closeButtonTapped } @Dependency(\.dismiss) var dismiss - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .closeButtonTapped: - return .fireAndForget { + return .run { _ in await self.dismiss() } } @@ -400,17 +400,17 @@ final class StackReducerTests: BaseTCATestCase { case onAppear } @Dependency(\.dismiss) var dismiss - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .closeButtonTapped: - return .fireAndForget { + return .run { _ in await self.dismiss() } case .incrementButtonTapped: state.count += 1 return .none case .onAppear: - return .fireAndForget { + return .run { _ in try await Task.never() } } @@ -483,8 +483,8 @@ final class StackReducerTests: BaseTCATestCase { struct State: Equatable {} enum Action { case tap } @Dependency(\.dismiss) var dismiss - func reduce(into state: inout State, action: Action) -> EffectTask { - .fireAndForget { try await Task.never() } + func reduce(into state: inout State, action: Action) -> Effect { + .run { _ in try await Task.never() } } } struct Parent: Reducer { @@ -543,7 +543,7 @@ final class StackReducerTests: BaseTCATestCase { } @Dependency(\.mainQueue) var mainQueue enum CancelID: Hashable { case cancel } - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .cancel: return .cancel(id: CancelID.cancel) @@ -551,9 +551,9 @@ final class StackReducerTests: BaseTCATestCase { state.count = value return .none case .tap: - return .task { + return .run { send in try await self.mainQueue.sleep(for: .seconds(1)) - return .response(42) + await send(.response(42)) } .cancellable(id: CancelID.cancel) } @@ -639,15 +639,15 @@ final class StackReducerTests: BaseTCATestCase { case tap } @Dependency(\.mainQueue) var mainQueue - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case let .response(value): state.count += value return .none case .tap: - return .task { + return .run { send in try await self.mainQueue.sleep(for: .seconds(self.id)) - return .response(self.id) + await send(.response(self.id)) } } } @@ -822,8 +822,8 @@ final class StackReducerTests: BaseTCATestCase { struct Child: Reducer { struct State: Equatable {} enum Action { case tap } - func reduce(into state: inout State, action: Action) -> EffectTask { - .fireAndForget { try await Task.never() } + func reduce(into state: inout State, action: Action) -> Effect { + .run { _ in try await Task.never() } } } struct Parent: Reducer { @@ -882,7 +882,7 @@ final class StackReducerTests: BaseTCATestCase { case response(Int) } @Dependency(\.mainQueue) var mainQueue - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .tap: return .run { [count = state.count] send in @@ -938,7 +938,7 @@ final class StackReducerTests: BaseTCATestCase { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable { case tap } - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { .run { _ in try await Task.never() } } } @@ -975,7 +975,7 @@ final class StackReducerTests: BaseTCATestCase { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable {} - func reduce(into state: inout State, action: Action) -> EffectTask {} + func reduce(into state: inout State, action: Action) -> Effect {} } struct Parent: Reducer { struct State: Equatable { @@ -1072,7 +1072,7 @@ final class StackReducerTests: BaseTCATestCase { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable {} - func reduce(into state: inout State, action: Action) -> EffectTask {} + func reduce(into state: inout State, action: Action) -> Effect {} } struct Parent: Reducer { struct State: Equatable { @@ -1113,7 +1113,7 @@ final class StackReducerTests: BaseTCATestCase { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable {} - func reduce(into state: inout State, action: Action) -> EffectTask {} + func reduce(into state: inout State, action: Action) -> Effect {} } struct Parent: Reducer { struct State: Equatable { diff --git a/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift b/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift index 7bda820dd207..8aa75cbbd9a6 100644 --- a/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift +++ b/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift @@ -191,6 +191,7 @@ @MainActor func testBindingUnhandledAction() { + let line = #line + 2 struct State: Equatable { @BindingState var value = 0 } @@ -199,13 +200,12 @@ } let store = Store(initialState: State()) {} - var line: UInt = 0 XCTExpectFailure { - line = #line - ViewStore(store, observe: { $0 }).binding(\.$value).wrappedValue = 42 + ViewStore(store, observe: { $0 }).$value.wrappedValue = 42 } issueMatcher: { $0.compactDescription == """ - A binding action sent from a view store at "\(#fileID):\(line + 1)" was not handled. … + A binding action sent from a view store for binding state defined at \ + "\(#fileID):\(line)" was not handled. … Action: RuntimeWarningTests.Action.binding(.set(_, 42)) diff --git a/Tests/ComposableArchitectureTests/StoreTests.swift b/Tests/ComposableArchitectureTests/StoreTests.swift index 7a636acb5ae5..b5cebc3288d6 100644 --- a/Tests/ComposableArchitectureTests/StoreTests.swift +++ b/Tests/ComposableArchitectureTests/StoreTests.swift @@ -309,7 +309,7 @@ final class StoreTests: BaseTCATestCase { state? += 1 return .none } else { - return .task { true } + return .run { send in await send(true) } } } } @@ -469,16 +469,16 @@ final class StoreTests: BaseTCATestCase { Reduce { state, action in switch action { case .task: - return .task { .response } + return .run { send in await send(.response) } case .response: return .merge( .run { _ in try await Task.never() }, - .run { await $0(.response1) } + .run { send in await send(.response1) } ) case .response1: return .merge( .run { _ in try await Task.never() }, - .run { await $0(.response2) } + .run { send in await send(.response2) } ) case .response2: return .run { _ in try await Task.never() } @@ -500,7 +500,7 @@ final class StoreTests: BaseTCATestCase { Reduce { state, action in switch action { case .task: - return .fireAndForget { try await Task.never() } + return .run { _ in try await Task.never() } } } } @@ -513,7 +513,7 @@ final class StoreTests: BaseTCATestCase { let store = Store(initialState: ()) { Reduce { _, _ in - .fireAndForget { + .run { _ in try await neverEndingTask.value } } @@ -596,21 +596,21 @@ final class StoreTests: BaseTCATestCase { return withDependencies { $0.count.value += 1 } operation: { - .task { .response1(self.count.value) } + .run { send in await send(.response1(self.count.value)) } } case let .response1(count): state.count = count return withDependencies { $0.count.value += 1 } operation: { - .task { .response2(self.count.value) } + .run { send in await send(.response2(self.count.value)) } } case let .response2(count): state.count = count return withDependencies { $0.count.value += 1 } operation: { - .task { .response3(self.count.value) } + .run { send in await send(.response3(self.count.value)) } } case let .response3(count): state.count = count @@ -654,21 +654,21 @@ final class StoreTests: BaseTCATestCase { return withDependencies { $0.count.value += 1 } operation: { - Effect.task { .response1(self.count.value) } + .run { send in await send(.response1(self.count.value)) } } case let .response1(count): state.count = count return withDependencies { $0.count.value += 1 } operation: { - Effect.task { .response2(self.count.value) } + .run { send in await send(.response2(self.count.value)) } } case let .response2(count): state.count = count return withDependencies { $0.count.value += 1 } operation: { - Effect.task { .response3(self.count.value) } + .run { send in await send(.response3(self.count.value)) } } case let .response3(count): state.count = count @@ -702,7 +702,7 @@ final class StoreTests: BaseTCATestCase { case didFinish } - func reduce(into state: inout State, action: Action) -> EffectTask { + func reduce(into state: inout State, action: Action) -> Effect { switch action { case .task: return .run { send in await send(.didFinish) } @@ -726,9 +726,9 @@ final class StoreTests: BaseTCATestCase { switch action { case .child(.didFinish): state.child = nil - return .task { + return .run { send in try await self.mainQueue.sleep(for: .seconds(1)) - return .delay + await send(.delay) } case .child: return .none diff --git a/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift b/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift index f84991f9d9bd..ee49e741e186 100644 --- a/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift +++ b/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift @@ -443,7 +443,7 @@ switch action { case .tap: state += 1 - return .task { [state] in .response(state + 42) } + return .run { [state] send in await send(.response(state + 42)) } case let .response(number): state = number return .none @@ -709,7 +709,7 @@ } } - func testReceiveNonExhuastiveWithTimeout() async { + func testReceiveNonExhaustiveWithTimeout() async { struct Feature: Reducer { struct State: Equatable {} enum Action: Equatable { case tap, response1, response2 } @@ -738,7 +738,7 @@ await store.receive(.response2, timeout: 1_000_000_000) } - func testReceiveNonExhuastiveWithTimeoutMultipleNonMatching() async { + func testReceiveNonExhaustiveWithTimeoutMultipleNonMatching() async { struct Feature: Reducer { struct State: Equatable {} enum Action: Equatable { case tap, response1, response2 } @@ -778,7 +778,7 @@ await store.receive(.response2, timeout: 1_000_000_000) } - func testReceiveNonExhuastiveWithTimeoutMultipleMatching() async { + func testReceiveNonExhaustiveWithTimeoutMultipleMatching() async { struct Feature: Reducer { struct State: Equatable {} enum Action: Equatable { case tap, response1, response2 } diff --git a/Tests/ComposableArchitectureTests/TestStoreTests.swift b/Tests/ComposableArchitectureTests/TestStoreTests.swift index bdfe157a279d..949d72a9a860 100644 --- a/Tests/ComposableArchitectureTests/TestStoreTests.swift +++ b/Tests/ComposableArchitectureTests/TestStoreTests.swift @@ -61,7 +61,7 @@ final class TestStoreTests: BaseTCATestCase { Reduce { state, action in switch action { case .tap: - return .task { .response(42) } + return .run { send in await send(.response(42)) } case let .response(number): state = number return .none @@ -359,7 +359,7 @@ final class TestStoreTests: BaseTCATestCase { switch action { case .tap: state.count += 1 - return .task { .response(42) } + return .run { send in await send(.response(42)) } case let .response(number): state.count = number state.date = now diff --git a/Tests/ComposableArchitectureTests/TimerTests.swift b/Tests/ComposableArchitectureTests/TimerTests.swift deleted file mode 100644 index 58083da807b9..000000000000 --- a/Tests/ComposableArchitectureTests/TimerTests.swift +++ /dev/null @@ -1,131 +0,0 @@ -import Combine -import ComposableArchitecture -import XCTest - -@MainActor -@available(*, deprecated) -final class TimerTests: BaseTCATestCase { - var cancellables: Set = [] - - func testTimer() async { - let mainQueue = DispatchQueue.test - - var count = 0 - - defer { Task.cancel(id: 1) } - EffectPublisher.timer(id: 1, every: .seconds(1), on: mainQueue) - .sink { _ in count += 1 } - .store(in: &self.cancellables) - - await mainQueue.advance(by: 1) - XCTAssertEqual(count, 1) - - await mainQueue.advance(by: 1) - XCTAssertEqual(count, 2) - - await mainQueue.advance(by: 1) - XCTAssertEqual(count, 3) - - await mainQueue.advance(by: 3) - XCTAssertEqual(count, 6) - } - - func testInterleavingTimer() async { - let mainQueue = DispatchQueue.test - - var count2 = 0 - var count3 = 0 - - defer { - Task.cancel(id: 1) - Task.cancel(id: 2) - } - EffectPublisher.merge( - EffectPublisher.timer(id: 1, every: .seconds(2), on: mainQueue) - .handleEvents(receiveOutput: { _ in count2 += 1 }) - .eraseToEffect(), - EffectPublisher.timer(id: 2, every: .seconds(3), on: mainQueue) - .handleEvents(receiveOutput: { _ in count3 += 1 }) - .eraseToEffect() - ) - .sink { _ in } - .store(in: &self.cancellables) - - await mainQueue.advance(by: 1) - XCTAssertEqual(count2, 0) - XCTAssertEqual(count3, 0) - await mainQueue.advance(by: 1) - XCTAssertEqual(count2, 1) - XCTAssertEqual(count3, 0) - await mainQueue.advance(by: 1) - XCTAssertEqual(count2, 1) - XCTAssertEqual(count3, 1) - await mainQueue.advance(by: 1) - XCTAssertEqual(count2, 2) - XCTAssertEqual(count3, 1) - } - - func testTimerCancellation() async { - let mainQueue = DispatchQueue.test - - var firstCount = 0 - var secondCount = 0 - - struct CancelToken: Hashable {} - - defer { Task.cancel(id: CancelToken()) } - EffectPublisher.timer(id: CancelToken(), every: .seconds(2), on: mainQueue) - .handleEvents(receiveOutput: { _ in firstCount += 1 }) - .eraseToEffect() - .sink { _ in } - .store(in: &self.cancellables) - - await mainQueue.advance(by: 2) - - XCTAssertEqual(firstCount, 1) - - await mainQueue.advance(by: 2) - - XCTAssertEqual(firstCount, 2) - - EffectPublisher.timer(id: CancelToken(), every: .seconds(2), on: mainQueue) - .handleEvents(receiveOutput: { _ in secondCount += 1 }) - .eraseToEffect() - .sink { _ in } - .store(in: &self.cancellables) - - await mainQueue.advance(by: 2) - - XCTAssertEqual(firstCount, 2) - XCTAssertEqual(secondCount, 1) - - await mainQueue.advance(by: 2) - - XCTAssertEqual(firstCount, 2) - XCTAssertEqual(secondCount, 2) - } - - func testTimerCompletion() async { - let mainQueue = DispatchQueue.test - - var count = 0 - - defer { Task.cancel(id: 1) } - EffectPublisher.timer(id: 1, every: .seconds(1), on: mainQueue) - .prefix(3) - .sink { _ in count += 1 } - .store(in: &self.cancellables) - - await mainQueue.advance(by: 1) - XCTAssertEqual(count, 1) - - await mainQueue.advance(by: 1) - XCTAssertEqual(count, 2) - - await mainQueue.advance(by: 1) - XCTAssertEqual(count, 3) - - await mainQueue.run() - XCTAssertEqual(count, 3) - } -} diff --git a/Tests/ComposableArchitectureTests/ViewStoreTests.swift b/Tests/ComposableArchitectureTests/ViewStoreTests.swift index 4a0635f90721..71271e439d93 100644 --- a/Tests/ComposableArchitectureTests/ViewStoreTests.swift +++ b/Tests/ComposableArchitectureTests/ViewStoreTests.swift @@ -172,7 +172,7 @@ final class ViewStoreTests: BaseTCATestCase { return .none case .tapped: state = true - return .task { .response } + return .run { send in await send(.response) } } } @@ -198,7 +198,7 @@ final class ViewStoreTests: BaseTCATestCase { return .none case .tapped: state = true - return .task { .response } + return .run { send in await send(.response) } } } @@ -224,8 +224,8 @@ final class ViewStoreTests: BaseTCATestCase { Reduce { state, action in switch action { case .tap: - return .task { - return .response(42) + return .run { send in + await send(.response(42)) } case let .response(value): state = value @@ -250,9 +250,9 @@ final class ViewStoreTests: BaseTCATestCase { Reduce { state, action in switch action { case .tap: - return .task { + return .run { send in try await Task.sleep(nanoseconds: NSEC_PER_SEC) - return .response(42) + await send(.response(42)) } case let .response(value): state = value