├── .github
└── workflows
│ ├── documentation.yml
│ ├── format.yml
│ └── swift.yml
├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ ├── ComposableEnvironment.xcscheme
│ ├── GlobalEnvironment.xcscheme
│ └── swift-composable-environment-Package.xcscheme
├── ComposableEnvironment.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Example
├── Example.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Example (macOS).xcscheme
├── Package.swift
├── Shared
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── ExampleApp.swift
│ ├── Level0Feature.swift
│ ├── Level1Feature.swift
│ ├── Level2Feature.swift
│ └── SharedDependencies.swift
├── iOS
│ └── Info.plist
└── macOS
│ ├── Info.plist
│ └── macOS.entitlements
├── LICENSE
├── Makefile
├── Package.swift
├── README.md
├── Sources
├── ComposableDependencies
│ └── ComposableDependencies.swift
├── ComposableEnvironment
│ ├── ComposableEnvironment.swift
│ ├── Dependency.swift
│ ├── Deprecations
│ │ ├── GlobalEnvironment+Migration.swift
│ │ └── Reducers+Deprecations.swift
│ ├── DerivedEnvironment.swift
│ └── Reducer+ComposableEnvironment.swift
├── GlobalEnvironment
│ ├── Dependency.swift
│ ├── Deprecations
│ │ ├── ComposableEnvironment+Migration.swift
│ │ └── Reducers+Deprecations.swift
│ ├── DerivedEnvironment.swift
│ ├── GlobalEnvironment.swift
│ └── Reducer+GlobalEnvironment.swift
├── _Dependencies
│ ├── Dependencies.swift
│ └── DependencyKey.swift
└── _DependencyAliases
│ └── DependencyAliases.swift
└── Tests
├── ComposableEnvironmentTests
├── ComposableEnvironmentTests.swift
└── ReducerComposableEnvironmentTests.swift
├── DependencyAliasesTests
└── DependencyAliasesTests.swift
└── GlobalEnvironmentTests
├── GlobalEnvironmentTests.swift
└── ReducerGlobalEnvironmentTests.swift
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
1 | # .github/workflows/documentation.yml
2 | name: Documentation
3 |
4 | on:
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v1
15 | - name: Generate Documentation
16 | uses: SwiftDocOrg/swift-doc@master
17 | with:
18 | inputs: "Sources"
19 | module-name: ComposableEnvironment
20 | output: "Documentation"
21 | - name: Upload Documentation to Wiki
22 | uses: SwiftDocOrg/github-wiki-publish-action@v1
23 | with:
24 | path: "Documentation"
25 | env:
26 | GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
27 |
--------------------------------------------------------------------------------
/.github/workflows/format.yml:
--------------------------------------------------------------------------------
1 | name: Format
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | swift_format:
10 | name: swift-format
11 | runs-on: macOS-11
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Xcode Select
15 | run: sudo xcode-select -s /Applications/Xcode_13.0.app
16 | - name: Tap
17 | run: brew tap tgrapperon/formulae
18 | - name: Install
19 | run: brew install Formulae/swift-format@5.5
20 | - name: Format
21 | run: make format
22 | - uses: stefanzweifel/git-auto-commit-action@v4
23 | with:
24 | commit_message: Run swift-format
25 | branch: 'main'
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | name: Swift
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: macos-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Build
17 | run: swift build -v
18 | - name: Run tests
19 | run: swift test -v
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | Package.resolved
7 | .DS_Store
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/ComposableEnvironment.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/GlobalEnvironment.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/swift-composable-environment-Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
57 |
63 |
64 |
65 |
71 |
77 |
78 |
79 |
85 |
91 |
92 |
93 |
99 |
105 |
106 |
107 |
113 |
119 |
120 |
121 |
127 |
133 |
134 |
135 |
136 |
137 |
142 |
143 |
145 |
151 |
152 |
153 |
155 |
161 |
162 |
163 |
165 |
171 |
172 |
173 |
174 |
175 |
185 |
186 |
192 |
193 |
199 |
200 |
201 |
202 |
204 |
205 |
208 |
209 |
210 |
--------------------------------------------------------------------------------
/ComposableEnvironment.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ComposableEnvironment.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | E972F1FD26A2B74A00504D39 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972F1EA26A2B74A00504D39 /* ExampleApp.swift */; };
11 | E972F1FE26A2B74B00504D39 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972F1EA26A2B74A00504D39 /* ExampleApp.swift */; };
12 | E972F20126A2B74B00504D39 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E972F1EC26A2B74A00504D39 /* Assets.xcassets */; };
13 | E972F20226A2B74B00504D39 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E972F1EC26A2B74A00504D39 /* Assets.xcassets */; };
14 | E972F21026A2B7FE00504D39 /* ComposableEnvironment in Frameworks */ = {isa = PBXBuildFile; productRef = E972F20F26A2B7FE00504D39 /* ComposableEnvironment */; };
15 | E972F21226A2B80600504D39 /* ComposableEnvironment in Frameworks */ = {isa = PBXBuildFile; productRef = E972F21126A2B80600504D39 /* ComposableEnvironment */; };
16 | E972F21926A2B9A700504D39 /* Level0Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972F21826A2B9A700504D39 /* Level0Feature.swift */; };
17 | E972F21A26A2B9A700504D39 /* Level0Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972F21826A2B9A700504D39 /* Level0Feature.swift */; };
18 | E972F21C26A2B9B900504D39 /* Level1Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972F21B26A2B9B900504D39 /* Level1Feature.swift */; };
19 | E972F21D26A2B9B900504D39 /* Level1Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972F21B26A2B9B900504D39 /* Level1Feature.swift */; };
20 | E972F21F26A2BA8400504D39 /* Level2Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972F21E26A2BA8400504D39 /* Level2Feature.swift */; };
21 | E972F22026A2BA8400504D39 /* Level2Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972F21E26A2BA8400504D39 /* Level2Feature.swift */; };
22 | E972F22226A2C72200504D39 /* SharedDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972F22126A2C72200504D39 /* SharedDependencies.swift */; };
23 | E972F22326A2C72200504D39 /* SharedDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972F22126A2C72200504D39 /* SharedDependencies.swift */; };
24 | /* End PBXBuildFile section */
25 |
26 | /* Begin PBXFileReference section */
27 | E972F1EA26A2B74A00504D39 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; };
28 | E972F1EC26A2B74A00504D39 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
29 | E972F1F126A2B74A00504D39 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
30 | E972F1F426A2B74A00504D39 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
31 | E972F1F926A2B74A00504D39 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
32 | E972F1FB26A2B74A00504D39 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
33 | E972F1FC26A2B74A00504D39 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; };
34 | E972F21826A2B9A700504D39 /* Level0Feature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Level0Feature.swift; sourceTree = ""; };
35 | E972F21B26A2B9B900504D39 /* Level1Feature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Level1Feature.swift; sourceTree = ""; };
36 | E972F21E26A2BA8400504D39 /* Level2Feature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Level2Feature.swift; sourceTree = ""; };
37 | E972F22126A2C72200504D39 /* SharedDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedDependencies.swift; sourceTree = ""; };
38 | /* End PBXFileReference section */
39 |
40 | /* Begin PBXFrameworksBuildPhase section */
41 | E972F1EE26A2B74A00504D39 /* Frameworks */ = {
42 | isa = PBXFrameworksBuildPhase;
43 | buildActionMask = 2147483647;
44 | files = (
45 | E972F21026A2B7FE00504D39 /* ComposableEnvironment in Frameworks */,
46 | );
47 | runOnlyForDeploymentPostprocessing = 0;
48 | };
49 | E972F1F626A2B74A00504D39 /* Frameworks */ = {
50 | isa = PBXFrameworksBuildPhase;
51 | buildActionMask = 2147483647;
52 | files = (
53 | E972F21226A2B80600504D39 /* ComposableEnvironment in Frameworks */,
54 | );
55 | runOnlyForDeploymentPostprocessing = 0;
56 | };
57 | /* End PBXFrameworksBuildPhase section */
58 |
59 | /* Begin PBXGroup section */
60 | E972F1E426A2B74900504D39 = {
61 | isa = PBXGroup;
62 | children = (
63 | E972F1E926A2B74A00504D39 /* Shared */,
64 | E972F1F326A2B74A00504D39 /* iOS */,
65 | E972F1FA26A2B74A00504D39 /* macOS */,
66 | E972F1F226A2B74A00504D39 /* Products */,
67 | E972F20E26A2B7FE00504D39 /* Frameworks */,
68 | );
69 | sourceTree = "";
70 | };
71 | E972F1E926A2B74A00504D39 /* Shared */ = {
72 | isa = PBXGroup;
73 | children = (
74 | E972F21826A2B9A700504D39 /* Level0Feature.swift */,
75 | E972F21B26A2B9B900504D39 /* Level1Feature.swift */,
76 | E972F21E26A2BA8400504D39 /* Level2Feature.swift */,
77 | E972F22126A2C72200504D39 /* SharedDependencies.swift */,
78 | E972F1EA26A2B74A00504D39 /* ExampleApp.swift */,
79 | E972F1EC26A2B74A00504D39 /* Assets.xcassets */,
80 | );
81 | path = Shared;
82 | sourceTree = "";
83 | };
84 | E972F1F226A2B74A00504D39 /* Products */ = {
85 | isa = PBXGroup;
86 | children = (
87 | E972F1F126A2B74A00504D39 /* Example.app */,
88 | E972F1F926A2B74A00504D39 /* Example.app */,
89 | );
90 | name = Products;
91 | sourceTree = "";
92 | };
93 | E972F1F326A2B74A00504D39 /* iOS */ = {
94 | isa = PBXGroup;
95 | children = (
96 | E972F1F426A2B74A00504D39 /* Info.plist */,
97 | );
98 | path = iOS;
99 | sourceTree = "";
100 | };
101 | E972F1FA26A2B74A00504D39 /* macOS */ = {
102 | isa = PBXGroup;
103 | children = (
104 | E972F1FB26A2B74A00504D39 /* Info.plist */,
105 | E972F1FC26A2B74A00504D39 /* macOS.entitlements */,
106 | );
107 | path = macOS;
108 | sourceTree = "";
109 | };
110 | E972F20E26A2B7FE00504D39 /* Frameworks */ = {
111 | isa = PBXGroup;
112 | children = (
113 | );
114 | name = Frameworks;
115 | sourceTree = "";
116 | };
117 | /* End PBXGroup section */
118 |
119 | /* Begin PBXNativeTarget section */
120 | E972F1F026A2B74A00504D39 /* Example (iOS) */ = {
121 | isa = PBXNativeTarget;
122 | buildConfigurationList = E972F20526A2B74B00504D39 /* Build configuration list for PBXNativeTarget "Example (iOS)" */;
123 | buildPhases = (
124 | E972F1ED26A2B74A00504D39 /* Sources */,
125 | E972F1EE26A2B74A00504D39 /* Frameworks */,
126 | E972F1EF26A2B74A00504D39 /* Resources */,
127 | );
128 | buildRules = (
129 | );
130 | dependencies = (
131 | );
132 | name = "Example (iOS)";
133 | packageProductDependencies = (
134 | E972F20F26A2B7FE00504D39 /* ComposableEnvironment */,
135 | );
136 | productName = "Example (iOS)";
137 | productReference = E972F1F126A2B74A00504D39 /* Example.app */;
138 | productType = "com.apple.product-type.application";
139 | };
140 | E972F1F826A2B74A00504D39 /* Example (macOS) */ = {
141 | isa = PBXNativeTarget;
142 | buildConfigurationList = E972F20826A2B74B00504D39 /* Build configuration list for PBXNativeTarget "Example (macOS)" */;
143 | buildPhases = (
144 | E972F1F526A2B74A00504D39 /* Sources */,
145 | E972F1F626A2B74A00504D39 /* Frameworks */,
146 | E972F1F726A2B74A00504D39 /* Resources */,
147 | );
148 | buildRules = (
149 | );
150 | dependencies = (
151 | );
152 | name = "Example (macOS)";
153 | packageProductDependencies = (
154 | E972F21126A2B80600504D39 /* ComposableEnvironment */,
155 | );
156 | productName = "Example (macOS)";
157 | productReference = E972F1F926A2B74A00504D39 /* Example.app */;
158 | productType = "com.apple.product-type.application";
159 | };
160 | /* End PBXNativeTarget section */
161 |
162 | /* Begin PBXProject section */
163 | E972F1E526A2B74900504D39 /* Project object */ = {
164 | isa = PBXProject;
165 | attributes = {
166 | LastSwiftUpdateCheck = 1250;
167 | LastUpgradeCheck = 1250;
168 | TargetAttributes = {
169 | E972F1F026A2B74A00504D39 = {
170 | CreatedOnToolsVersion = 12.5.1;
171 | };
172 | E972F1F826A2B74A00504D39 = {
173 | CreatedOnToolsVersion = 12.5.1;
174 | };
175 | };
176 | };
177 | buildConfigurationList = E972F1E826A2B74900504D39 /* Build configuration list for PBXProject "Example" */;
178 | compatibilityVersion = "Xcode 9.3";
179 | developmentRegion = en;
180 | hasScannedForEncodings = 0;
181 | knownRegions = (
182 | en,
183 | Base,
184 | );
185 | mainGroup = E972F1E426A2B74900504D39;
186 | packageReferences = (
187 | );
188 | productRefGroup = E972F1F226A2B74A00504D39 /* Products */;
189 | projectDirPath = "";
190 | projectRoot = "";
191 | targets = (
192 | E972F1F026A2B74A00504D39 /* Example (iOS) */,
193 | E972F1F826A2B74A00504D39 /* Example (macOS) */,
194 | );
195 | };
196 | /* End PBXProject section */
197 |
198 | /* Begin PBXResourcesBuildPhase section */
199 | E972F1EF26A2B74A00504D39 /* Resources */ = {
200 | isa = PBXResourcesBuildPhase;
201 | buildActionMask = 2147483647;
202 | files = (
203 | E972F20126A2B74B00504D39 /* Assets.xcassets in Resources */,
204 | );
205 | runOnlyForDeploymentPostprocessing = 0;
206 | };
207 | E972F1F726A2B74A00504D39 /* Resources */ = {
208 | isa = PBXResourcesBuildPhase;
209 | buildActionMask = 2147483647;
210 | files = (
211 | E972F20226A2B74B00504D39 /* Assets.xcassets in Resources */,
212 | );
213 | runOnlyForDeploymentPostprocessing = 0;
214 | };
215 | /* End PBXResourcesBuildPhase section */
216 |
217 | /* Begin PBXSourcesBuildPhase section */
218 | E972F1ED26A2B74A00504D39 /* Sources */ = {
219 | isa = PBXSourcesBuildPhase;
220 | buildActionMask = 2147483647;
221 | files = (
222 | E972F21C26A2B9B900504D39 /* Level1Feature.swift in Sources */,
223 | E972F21F26A2BA8400504D39 /* Level2Feature.swift in Sources */,
224 | E972F21926A2B9A700504D39 /* Level0Feature.swift in Sources */,
225 | E972F22226A2C72200504D39 /* SharedDependencies.swift in Sources */,
226 | E972F1FD26A2B74A00504D39 /* ExampleApp.swift in Sources */,
227 | );
228 | runOnlyForDeploymentPostprocessing = 0;
229 | };
230 | E972F1F526A2B74A00504D39 /* Sources */ = {
231 | isa = PBXSourcesBuildPhase;
232 | buildActionMask = 2147483647;
233 | files = (
234 | E972F21D26A2B9B900504D39 /* Level1Feature.swift in Sources */,
235 | E972F22026A2BA8400504D39 /* Level2Feature.swift in Sources */,
236 | E972F21A26A2B9A700504D39 /* Level0Feature.swift in Sources */,
237 | E972F22326A2C72200504D39 /* SharedDependencies.swift in Sources */,
238 | E972F1FE26A2B74B00504D39 /* ExampleApp.swift in Sources */,
239 | );
240 | runOnlyForDeploymentPostprocessing = 0;
241 | };
242 | /* End PBXSourcesBuildPhase section */
243 |
244 | /* Begin XCBuildConfiguration section */
245 | E972F20326A2B74B00504D39 /* Debug */ = {
246 | isa = XCBuildConfiguration;
247 | buildSettings = {
248 | ALWAYS_SEARCH_USER_PATHS = NO;
249 | CLANG_ANALYZER_NONNULL = YES;
250 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
251 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
252 | CLANG_CXX_LIBRARY = "libc++";
253 | CLANG_ENABLE_MODULES = YES;
254 | CLANG_ENABLE_OBJC_ARC = YES;
255 | CLANG_ENABLE_OBJC_WEAK = YES;
256 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
257 | CLANG_WARN_BOOL_CONVERSION = YES;
258 | CLANG_WARN_COMMA = YES;
259 | CLANG_WARN_CONSTANT_CONVERSION = YES;
260 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
261 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
262 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
263 | CLANG_WARN_EMPTY_BODY = YES;
264 | CLANG_WARN_ENUM_CONVERSION = YES;
265 | CLANG_WARN_INFINITE_RECURSION = YES;
266 | CLANG_WARN_INT_CONVERSION = YES;
267 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
268 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
269 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
270 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
271 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
272 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
273 | CLANG_WARN_STRICT_PROTOTYPES = YES;
274 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
275 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
276 | CLANG_WARN_UNREACHABLE_CODE = YES;
277 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
278 | COPY_PHASE_STRIP = NO;
279 | DEBUG_INFORMATION_FORMAT = dwarf;
280 | ENABLE_STRICT_OBJC_MSGSEND = YES;
281 | ENABLE_TESTABILITY = YES;
282 | GCC_C_LANGUAGE_STANDARD = gnu11;
283 | GCC_DYNAMIC_NO_PIC = NO;
284 | GCC_NO_COMMON_BLOCKS = YES;
285 | GCC_OPTIMIZATION_LEVEL = 0;
286 | GCC_PREPROCESSOR_DEFINITIONS = (
287 | "DEBUG=1",
288 | "$(inherited)",
289 | );
290 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
291 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
292 | GCC_WARN_UNDECLARED_SELECTOR = YES;
293 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
294 | GCC_WARN_UNUSED_FUNCTION = YES;
295 | GCC_WARN_UNUSED_VARIABLE = YES;
296 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
297 | MTL_FAST_MATH = YES;
298 | ONLY_ACTIVE_ARCH = YES;
299 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
300 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
301 | };
302 | name = Debug;
303 | };
304 | E972F20426A2B74B00504D39 /* Release */ = {
305 | isa = XCBuildConfiguration;
306 | buildSettings = {
307 | ALWAYS_SEARCH_USER_PATHS = NO;
308 | CLANG_ANALYZER_NONNULL = YES;
309 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
310 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
311 | CLANG_CXX_LIBRARY = "libc++";
312 | CLANG_ENABLE_MODULES = YES;
313 | CLANG_ENABLE_OBJC_ARC = YES;
314 | CLANG_ENABLE_OBJC_WEAK = YES;
315 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
316 | CLANG_WARN_BOOL_CONVERSION = YES;
317 | CLANG_WARN_COMMA = YES;
318 | CLANG_WARN_CONSTANT_CONVERSION = YES;
319 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
320 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
321 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
322 | CLANG_WARN_EMPTY_BODY = YES;
323 | CLANG_WARN_ENUM_CONVERSION = YES;
324 | CLANG_WARN_INFINITE_RECURSION = YES;
325 | CLANG_WARN_INT_CONVERSION = YES;
326 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
327 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
328 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
329 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
330 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
331 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
332 | CLANG_WARN_STRICT_PROTOTYPES = YES;
333 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
334 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
335 | CLANG_WARN_UNREACHABLE_CODE = YES;
336 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
337 | COPY_PHASE_STRIP = NO;
338 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
339 | ENABLE_NS_ASSERTIONS = NO;
340 | ENABLE_STRICT_OBJC_MSGSEND = YES;
341 | GCC_C_LANGUAGE_STANDARD = gnu11;
342 | GCC_NO_COMMON_BLOCKS = YES;
343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
345 | GCC_WARN_UNDECLARED_SELECTOR = YES;
346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
347 | GCC_WARN_UNUSED_FUNCTION = YES;
348 | GCC_WARN_UNUSED_VARIABLE = YES;
349 | MTL_ENABLE_DEBUG_INFO = NO;
350 | MTL_FAST_MATH = YES;
351 | SWIFT_COMPILATION_MODE = wholemodule;
352 | SWIFT_OPTIMIZATION_LEVEL = "-O";
353 | };
354 | name = Release;
355 | };
356 | E972F20626A2B74B00504D39 /* Debug */ = {
357 | isa = XCBuildConfiguration;
358 | buildSettings = {
359 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
360 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
361 | CODE_SIGN_STYLE = Automatic;
362 | ENABLE_PREVIEWS = YES;
363 | INFOPLIST_FILE = iOS/Info.plist;
364 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
365 | LD_RUNPATH_SEARCH_PATHS = (
366 | "$(inherited)",
367 | "@executable_path/Frameworks",
368 | );
369 | PRODUCT_BUNDLE_IDENTIFIER = "swift-composable-environment.Example";
370 | PRODUCT_NAME = Example;
371 | SDKROOT = iphoneos;
372 | SWIFT_VERSION = 5.0;
373 | TARGETED_DEVICE_FAMILY = "1,2";
374 | };
375 | name = Debug;
376 | };
377 | E972F20726A2B74B00504D39 /* Release */ = {
378 | isa = XCBuildConfiguration;
379 | buildSettings = {
380 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
381 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
382 | CODE_SIGN_STYLE = Automatic;
383 | ENABLE_PREVIEWS = YES;
384 | INFOPLIST_FILE = iOS/Info.plist;
385 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
386 | LD_RUNPATH_SEARCH_PATHS = (
387 | "$(inherited)",
388 | "@executable_path/Frameworks",
389 | );
390 | PRODUCT_BUNDLE_IDENTIFIER = "swift-composable-environment.Example";
391 | PRODUCT_NAME = Example;
392 | SDKROOT = iphoneos;
393 | SWIFT_VERSION = 5.0;
394 | TARGETED_DEVICE_FAMILY = "1,2";
395 | VALIDATE_PRODUCT = YES;
396 | };
397 | name = Release;
398 | };
399 | E972F20926A2B74B00504D39 /* Debug */ = {
400 | isa = XCBuildConfiguration;
401 | buildSettings = {
402 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
403 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
404 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
405 | CODE_SIGN_IDENTITY = "-";
406 | CODE_SIGN_STYLE = Automatic;
407 | COMBINE_HIDPI_IMAGES = YES;
408 | ENABLE_PREVIEWS = YES;
409 | INFOPLIST_FILE = macOS/Info.plist;
410 | LD_RUNPATH_SEARCH_PATHS = (
411 | "$(inherited)",
412 | "@executable_path/../Frameworks",
413 | );
414 | MACOSX_DEPLOYMENT_TARGET = 11.0;
415 | PRODUCT_BUNDLE_IDENTIFIER = "swift-composable-environment.Example";
416 | PRODUCT_NAME = Example;
417 | SDKROOT = macosx;
418 | SWIFT_VERSION = 5.0;
419 | };
420 | name = Debug;
421 | };
422 | E972F20A26A2B74B00504D39 /* Release */ = {
423 | isa = XCBuildConfiguration;
424 | buildSettings = {
425 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
426 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
427 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
428 | CODE_SIGN_IDENTITY = "-";
429 | CODE_SIGN_STYLE = Automatic;
430 | COMBINE_HIDPI_IMAGES = YES;
431 | ENABLE_PREVIEWS = YES;
432 | INFOPLIST_FILE = macOS/Info.plist;
433 | LD_RUNPATH_SEARCH_PATHS = (
434 | "$(inherited)",
435 | "@executable_path/../Frameworks",
436 | );
437 | MACOSX_DEPLOYMENT_TARGET = 11.0;
438 | PRODUCT_BUNDLE_IDENTIFIER = "swift-composable-environment.Example";
439 | PRODUCT_NAME = Example;
440 | SDKROOT = macosx;
441 | SWIFT_VERSION = 5.0;
442 | };
443 | name = Release;
444 | };
445 | /* End XCBuildConfiguration section */
446 |
447 | /* Begin XCConfigurationList section */
448 | E972F1E826A2B74900504D39 /* Build configuration list for PBXProject "Example" */ = {
449 | isa = XCConfigurationList;
450 | buildConfigurations = (
451 | E972F20326A2B74B00504D39 /* Debug */,
452 | E972F20426A2B74B00504D39 /* Release */,
453 | );
454 | defaultConfigurationIsVisible = 0;
455 | defaultConfigurationName = Release;
456 | };
457 | E972F20526A2B74B00504D39 /* Build configuration list for PBXNativeTarget "Example (iOS)" */ = {
458 | isa = XCConfigurationList;
459 | buildConfigurations = (
460 | E972F20626A2B74B00504D39 /* Debug */,
461 | E972F20726A2B74B00504D39 /* Release */,
462 | );
463 | defaultConfigurationIsVisible = 0;
464 | defaultConfigurationName = Release;
465 | };
466 | E972F20826A2B74B00504D39 /* Build configuration list for PBXNativeTarget "Example (macOS)" */ = {
467 | isa = XCConfigurationList;
468 | buildConfigurations = (
469 | E972F20926A2B74B00504D39 /* Debug */,
470 | E972F20A26A2B74B00504D39 /* Release */,
471 | );
472 | defaultConfigurationIsVisible = 0;
473 | defaultConfigurationName = Release;
474 | };
475 | /* End XCConfigurationList section */
476 |
477 | /* Begin XCSwiftPackageProductDependency section */
478 | E972F20F26A2B7FE00504D39 /* ComposableEnvironment */ = {
479 | isa = XCSwiftPackageProductDependency;
480 | productName = ComposableEnvironment;
481 | };
482 | E972F21126A2B80600504D39 /* ComposableEnvironment */ = {
483 | isa = XCSwiftPackageProductDependency;
484 | productName = ComposableEnvironment;
485 | };
486 | /* End XCSwiftPackageProductDependency section */
487 | };
488 | rootObject = E972F1E526A2B74900504D39 /* Project object */;
489 | }
490 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/xcshareddata/xcschemes/Example (macOS).xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Example/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "Example",
7 | products: [])
8 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | },
93 | {
94 | "idiom" : "mac",
95 | "scale" : "1x",
96 | "size" : "16x16"
97 | },
98 | {
99 | "idiom" : "mac",
100 | "scale" : "2x",
101 | "size" : "16x16"
102 | },
103 | {
104 | "idiom" : "mac",
105 | "scale" : "1x",
106 | "size" : "32x32"
107 | },
108 | {
109 | "idiom" : "mac",
110 | "scale" : "2x",
111 | "size" : "32x32"
112 | },
113 | {
114 | "idiom" : "mac",
115 | "scale" : "1x",
116 | "size" : "128x128"
117 | },
118 | {
119 | "idiom" : "mac",
120 | "scale" : "2x",
121 | "size" : "128x128"
122 | },
123 | {
124 | "idiom" : "mac",
125 | "scale" : "1x",
126 | "size" : "256x256"
127 | },
128 | {
129 | "idiom" : "mac",
130 | "scale" : "2x",
131 | "size" : "256x256"
132 | },
133 | {
134 | "idiom" : "mac",
135 | "scale" : "1x",
136 | "size" : "512x512"
137 | },
138 | {
139 | "idiom" : "mac",
140 | "scale" : "2x",
141 | "size" : "512x512"
142 | }
143 | ],
144 | "info" : {
145 | "author" : "xcode",
146 | "version" : 1
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Shared/ExampleApp.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import ComposableEnvironment
3 | import SwiftUI
4 |
5 | let store = Store(
6 | initialState:
7 | .init(
8 | level1: .init(
9 | first: .init(randomNumber: nil),
10 | second: .init(randomNumber: nil)
11 | )),
12 | reducer: level0Reducer,
13 | environment: .init()
14 | )
15 |
16 | @main
17 | struct ExampleApp: App {
18 | var body: some Scene {
19 | WindowGroup {
20 | Level0View(store: store)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Example/Shared/Level0Feature.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import ComposableArchitecture
3 | import ComposableEnvironment
4 | import SwiftUI
5 |
6 | // "Level0" Feature, which embeds a `Level1` feature
7 |
8 | struct Level0State: Equatable {
9 | var level1: Level1State
10 | var isReady: Bool = false
11 | }
12 |
13 | enum Level0Action {
14 | case isReady
15 | case level1(Level1Action)
16 | case onAppear
17 | }
18 |
19 | class Level0Environment: ComposableEnvironment {
20 | // Dependencies
21 | @Dependency(\.mainQueue) var main
22 | @Dependency(\.backgroundQueue) var background
23 |
24 | // Derived Environments
25 | @DerivedEnvironment var level1
26 | }
27 |
28 | let level0Reducer = Reducer.combine(
29 | level1Reducer.pullback(
30 | state: \.level1,
31 | action: /Level0Action.level1,
32 | environment: \.level1),
33 | Reducer {
34 | state, action, environment in
35 | switch action {
36 | case .isReady:
37 | state.isReady = true
38 | return .none
39 | case .level1:
40 | return .none
41 | case .onAppear:
42 | return Effect(value: .isReady)
43 | .delay(for: 1, scheduler: environment.background) // Simulate something lengthy…
44 | .receive(on: environment.main)
45 | .eraseToEffect()
46 |
47 | // Alternatively, we can directly tap into the environment's dependepencies using
48 | // their global property name, meaning that we can even bypass declarations like
49 | // `@Dependency(\.mainQueue) var main` in the environment to write:
50 | //
51 | // return Effect(value: .isReady)
52 | // .delay(for: 1, scheduler: environment.backgroundQueue)
53 | // .receive(on: environment.mainQueue)
54 | // .eraseToEffect()
55 | }
56 | }
57 | )
58 |
59 | struct Level0View: View {
60 | let store: Store
61 | init(store: Store) {
62 | self.store = store
63 | }
64 |
65 | var body: some View {
66 | WithViewStore(store) { viewStore in
67 | VStack {
68 | Text("Random numbers")
69 | .font(.title)
70 | Level1View(store: store.scope(state: \.level1, action: Level0Action.level1))
71 | .padding()
72 | .disabled(!viewStore.isReady)
73 | }
74 | .onAppear { viewStore.send(.onAppear) }
75 | .fixedSize()
76 | }
77 | }
78 | }
79 |
80 | struct Level0View_Preview: PreviewProvider {
81 | static var previews: some View {
82 | Level0View(
83 | store:
84 | .init(
85 | initialState:
86 | .init(
87 | level1: .init(
88 | first: .init(randomNumber: 6),
89 | second: .init(randomNumber: nil)
90 | )
91 | ),
92 | reducer: level0Reducer,
93 | environment: Level0Environment() // Swift ≥ 5.4 can use .init()
94 | .with(\.mainQueue, .immediate)
95 | .with(\.backgroundQueue, .immediate)
96 | // We can set the value of `rng` even if Level0Environment doesn't have a `rng` property:
97 | .with(\.rng) { 4 })
98 | )
99 | Level0View(
100 | store:
101 | .init(
102 | initialState:
103 | .init(
104 | level1: .init(
105 | first: .init(randomNumber: nil),
106 | second: .init(randomNumber: nil)
107 | )),
108 | reducer: level0Reducer,
109 | // An environment default dependencies:
110 | environment: .init())
111 | )
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Example/Shared/Level1Feature.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import ComposableEnvironment
3 | import SwiftUI
4 |
5 | // "Level1" Feature, which embeds two similar `Level2` features
6 |
7 | struct Level1State: Equatable {
8 | var first: Level2State
9 | var second: Level2State
10 | }
11 |
12 | enum Level1Action {
13 | case first(Level2Action)
14 | case second(Level2Action)
15 | }
16 |
17 | class Level1Environment: ComposableEnvironment {
18 | @DerivedEnvironment var first
19 | @DerivedEnvironment var second
20 |
21 | // In this case, we could have used a shared `@DerivedEnvironment` property instead:
22 | // @DerivedEnvironment var level2
23 | // And used its `KeyPath` `\.level2` twice when pulling-back in `level1Reducer`
24 |
25 | // This environment doesn't have exposed dependencies, but this doesn't prevent derived
26 | // environments to inherit dependencies that were set higher in the parents' chain, nor
27 | // to access them using their global KeyPath.
28 | }
29 |
30 | // Alternatively, if we plan to use environment-less pullback variants, we can only declare an
31 | // empty environment:
32 | // class Level1Environment: ComposableEnvironment { }
33 |
34 | let level1Reducer = Reducer.combine(
35 | level2Reducer.pullback(
36 | state: \.first,
37 | action: /Level1Action.first,
38 | environment: \.first), // (or \.level2 if we had used only one property)
39 |
40 | level2Reducer.pullback(
41 | state: \.second,
42 | action: /Level1Action.second,
43 | environment: \.second) // (or \.level2 if we had used only one property)
44 |
45 | // Alternatively, we can forgo the `@DerivedEnvironment` declarations in `Level1Environment`, and
46 | // use the environment-less pullback variants:
47 | // level2Reducer.pullback(state: \.first,
48 | // action: /Level1Action.first)
49 | //
50 | // level2Reducer.pullback(state: \.second,
51 | // action: /Level1Action.second)
52 | )
53 |
54 | struct Level1View: View {
55 | let store: Store
56 | init(store: Store) {
57 | self.store = store
58 | }
59 |
60 | var body: some View {
61 | Stack {
62 | VStack {
63 | Text("First random number")
64 | .font(.title3)
65 | Level2View(store: store.scope(state: \.first, action: Level1Action.first))
66 | .padding()
67 | }
68 | VStack {
69 | Text("Second random number")
70 | .font(.title3)
71 | Level2View(store: store.scope(state: \.second, action: Level1Action.second))
72 | .padding()
73 | }
74 | }
75 | }
76 |
77 | #if os(macOS)
78 | typealias Stack = HStack
79 | #else
80 | typealias Stack = VStack
81 | #endif
82 | }
83 |
84 | struct Level1View_Preview: PreviewProvider {
85 | static var previews: some View {
86 | Level1View(
87 | store:
88 | .init(
89 | initialState:
90 | .init(
91 | first: .init(randomNumber: 6),
92 | second: .init(randomNumber: nil)
93 | ),
94 | reducer: level1Reducer,
95 | environment: .init())
96 | )
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Example/Shared/Level2Feature.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import ComposableArchitecture
3 | import ComposableEnvironment
4 | import SwiftUI
5 |
6 | // Define some domain-specific dependency
7 | public typealias RandomNumberGenerator = () -> Int
8 |
9 | // Create a Composable Dependency:
10 | private struct RNGKey: DependencyKey {
11 | static var defaultValue: RandomNumberGenerator {
12 | { Int.random(in: 0...1000) }
13 | }
14 | }
15 |
16 | // Install it in `Dependencies`:
17 | extension Dependencies {
18 | public var rng: RandomNumberGenerator {
19 | get { self[RNGKey.self] }
20 | set { self[RNGKey.self] = newValue }
21 | }
22 | }
23 |
24 | // "Level2" Feature
25 |
26 | struct Level2State: Equatable {
27 | var randomNumber: Int?
28 | }
29 |
30 | enum Level2Action {
31 | case randomNumber(Int)
32 | case requestRandomNumber
33 | }
34 |
35 | class Level2Environment: ComposableEnvironment {
36 | @Dependency(\.rng) var randomNumberGenerator
37 |
38 | func randomNumber() -> Future {
39 | .init { [randomNumberGenerator] in
40 | let number = randomNumberGenerator()
41 | $0(.success(number))
42 | }
43 | }
44 | }
45 |
46 | let level2Reducer = Reducer {
47 | state, action, environment in
48 | switch action {
49 | case let .randomNumber(number):
50 | state.randomNumber = number
51 | return .none
52 | case .requestRandomNumber:
53 | // Note that we don't have defined any `@Dependency(\.mainQueue)` in environment.
54 | // We use its global property name instead:
55 | return
56 | environment
57 | .randomNumber()
58 | .map(Level2Action.randomNumber)
59 | .receive(on: environment.mainQueue)
60 | .eraseToEffect()
61 | }
62 | }
63 |
64 | struct Level2View: View {
65 | let store: Store
66 | init(store: Store) {
67 | self.store = store
68 | }
69 |
70 | var body: some View {
71 | WithViewStore(store) { viewStore in
72 | VStack {
73 | if let number = viewStore.randomNumber {
74 | Text("Your random number is \(number)!")
75 | .font(.headline)
76 | } else {
77 | Text("No number yet…")
78 | .foregroundColor(.secondary)
79 | }
80 | Button(action: { viewStore.send(.requestRandomNumber) }) {
81 | Text("Request some random number")
82 | }
83 | }
84 | }
85 | }
86 | }
87 |
88 | struct Level2View_Preview: PreviewProvider {
89 | static var previews: some View {
90 | Level2View(
91 | store:
92 | .init(
93 | initialState:
94 | .init(
95 | randomNumber: 5
96 | ),
97 | reducer: level2Reducer,
98 | environment:
99 | Level2Environment() // Swift ≥ 5.4 can use .init()
100 | .with(\.mainQueue, .immediate)
101 | .with(\.rng) { 12 })
102 | )
103 | Level2View(
104 | store:
105 | .init(
106 | initialState:
107 | .init(
108 | randomNumber: nil
109 | ),
110 | reducer: level2Reducer,
111 | environment:
112 | Level2Environment() // Swift ≥ 5.4 can use .init()
113 | .with(\.mainQueue, .immediate)
114 | .with(\.rng) { 54 })
115 | )
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Example/Shared/SharedDependencies.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import ComposableEnvironment
3 |
4 | // mainQueue dependency:
5 | private struct MainQueueKey: DependencyKey {
6 | static var defaultValue: AnySchedulerOf {
7 | .main
8 | }
9 | }
10 |
11 | extension Dependencies {
12 | public var mainQueue: AnySchedulerOf {
13 | get { self[MainQueueKey.self] }
14 | set { self[MainQueueKey.self] = newValue }
15 | }
16 | }
17 |
18 | // backgroundQueue dependency:
19 | private struct BackgroundQueueKey: DependencyKey {
20 | static var defaultValue: AnySchedulerOf {
21 | DispatchQueue.global().eraseToAnyScheduler()
22 | }
23 | }
24 |
25 | extension Dependencies {
26 | public var backgroundQueue: AnySchedulerOf {
27 | get { self[BackgroundQueueKey.self] }
28 | set { self[BackgroundQueueKey.self] = newValue }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Example/iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIApplicationSupportsIndirectInputEvents
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Example/macOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Example/macOS/macOS.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Thomas Grapperon
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | format:
2 | swift format \
3 | --ignore-unparsable-files \
4 | --in-place \
5 | --recursive \
6 | ./Example ./Package.swift ./Sources ./Tests
7 |
8 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 |
3 | import PackageDescription
4 |
5 | /// Because some code is shared between `ComposableEnvironment` and `GlobalEnvironment`, and in
6 | /// order to expose only the minimum API surface, the package is split in several targets.
7 | ///
8 | /// The third product called `ComposableDependencies` can be used in case you want to define
9 | /// dependencies in an environment-agnostic way. Such dependencies can then be imported and used by
10 | /// `ComposableEnvironment` or `GlobalEnvironment`.
11 | ///
12 | /// Targets with names starting with a underscore are used for implementation only and their types
13 | /// exported on a case by case basis. They should not be imported as a whole without prefixing the
14 | /// import with the `@_implementationOnly` keyword.
15 |
16 | let package = Package(
17 | name: "swift-composable-environment",
18 | platforms: [
19 | .iOS(.v13),
20 | .macOS(.v10_15),
21 | .tvOS(.v13),
22 | .watchOS(.v6),
23 | ],
24 | products: [
25 | .library(
26 | name: "ComposableDependencies",
27 | targets: ["ComposableDependencies"]
28 | ),
29 | .library(
30 | name: "ComposableEnvironment",
31 | targets: ["ComposableEnvironment"]
32 | ),
33 | .library(
34 | name: "GlobalEnvironment",
35 | targets: ["GlobalEnvironment"]
36 | ),
37 | ],
38 | dependencies: [
39 | .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.21.0")
40 | ],
41 | targets: [
42 | .target(
43 | name: "ComposableDependencies",
44 | dependencies: ["_Dependencies"]
45 | ),
46 |
47 | .target(
48 | name: "ComposableEnvironment",
49 | dependencies: [
50 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
51 | "ComposableDependencies",
52 | "_Dependencies",
53 | "_DependencyAliases",
54 | ]
55 | ),
56 | .testTarget(
57 | name: "ComposableEnvironmentTests",
58 | dependencies: ["ComposableEnvironment"]
59 | ),
60 |
61 | .target(name: "_Dependencies"),
62 |
63 | .target(
64 | name: "_DependencyAliases",
65 | dependencies: ["ComposableDependencies"]
66 | ),
67 | .testTarget(
68 | name: "DependencyAliasesTests",
69 | dependencies: ["_DependencyAliases"]
70 | ),
71 |
72 | .target(
73 | name: "GlobalEnvironment",
74 | dependencies: [
75 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
76 | "ComposableDependencies",
77 | "_Dependencies",
78 | "_DependencyAliases",
79 | ]
80 | ),
81 | .testTarget(
82 | name: "GlobalEnvironmentTests",
83 | dependencies: ["GlobalEnvironment"]
84 | ),
85 | ]
86 | )
87 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ComposableEnvironment
2 | [](https://github.com/tgrapperon/swift-composable-environment/actions/workflows/swift.yml)
3 | [](https://github.com/tgrapperon/swift-composable-environment/wiki/ComposableEnvironment-Documentation)
4 | [](https://swiftpackageindex.com/tgrapperon/swift-composable-environment)
5 | [](https://swiftpackageindex.com/tgrapperon/swift-composable-environment)
6 |
7 | This library brings an API similar to SwiftUI's `Environment` to derive and compose `Environment`'s in [The Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture) (TCA).
8 |
9 | **TCA is moving toward protocol reducers. This simplifies greatly the way dependencies are passed around between features. It is encouraged to migrate toward this approach. TCA will also use a `@Dependency` property wrapper, and a `DependencyKey` protocol, so [you will need to perform a few actions](#migrating-to-tcas-protocol-reducers) to have both systems working at the same time while you transition out from this library.**
10 |
11 | By `Environment`, one understands a type that vends *dependencies*. This library eases this process by standardizing these dependencies, and the way they are passed from one environment type to another when composing domains using TCA. Like in SwiftUI, this library allows passing values (in this case dependencies) down a tree of values (in this case the reducers) without having to specify them at each step. You don't need to provide initial values for dependencies in your `Environment`'s, you don't need to inject dependencies from a parent environment to a child environment, and in many cases, you don't even need to instantiate the child environment.
12 |
13 | This library comes with two mutually exclusive modules, `ComposableEnvironment` and `GlobalEnvironment`, which are providing different functionalities for different tradeoffs.
14 | `ComposableEnvironment` allows defining environments where dependencies can be overridden at any point in the reducer chain. Like in SwiftUI, setting a value for a dependency propagates downstream until it is eventually overridden again.
15 | `GlobalEnvironment` allows defining global dependencies that are the same for all reducers in the chain. This is the most frequent configuration.
16 | Both modules are defined in the same repository to maintain source compatibility between them.
17 |
18 | **The `GlobalEnvironment` module should fit most of the cases.**
19 |
20 | ## Defining dependencies
21 | Each dependency we want to share should be declared with a `DependencyKey`'s in a similar fashion one declares custom `EnvironmentValue`'s in SwiftUI using `EnvironmentKey`'s. Let define a `mainQueue` dependency:
22 | ```swift
23 | struct MainQueueKey: DependencyKey {
24 | static var defaultValue: AnySchedulerOf { .main }
25 | }
26 | ```
27 | This key doesn't need to be public. If the dependency is an existential type, it can be even used as a `DependencyKey` itself, without needing to introduce an additional type.
28 |
29 | Like we would do with SwiftUI's `EnvironmentValues`, we also install it in `Dependencies`:
30 | ```swift
31 | extension Dependencies {
32 | var mainQueue: AnySchedulerOf {
33 | get { self[MainQueueKey.self] }
34 | set { self[MainQueueKey.self] = newValue }
35 | }
36 | }
37 | ```
38 | ## Using dependencies
39 | Whereas you're using `ComposableEnvironment` or `GlobalEnvironment`, there are distinct ways to access your dependencies.
40 |
41 | ### `@Dependency` property wrapper
42 | You use the `@Dependency` property wrapper to expose a dependency to your environment. This property wrapper takes as argument the `KeyPath` of the property you defined in `Dependencies`. For example, to expose the `mainQueue` defined above, you declare
43 | ```swift
44 | @Dependency(\.mainQueue) var main
45 | ```
46 | Note that you don't need to provide a value for the dependency. The effective value for this property is the current value from the environment, or the `default` value if you defined none.
47 |
48 | ### Implicit subscript
49 | You can also already use a subscript from your `Environment` to directly access the dependency without having to expose it. You use this subscript with the `KeyPath` from the property defined in `Dependencies`. For example:
50 | ```swift
51 | environment[\.mainQueue]
52 | ```
53 | returns the same value as `@Dependency(\.mainQueue)`.
54 |
55 | Whereas you use one or another is up to you. The implicit subscript is faster, but some prefer having explicit declarations to assess the environment's dependencies.
56 |
57 | ### Direct access (`ComposableEnvironment` only)
58 | When using `ComposableEnvironment`, you can directly access a dependency by using its computed property name in `Dependencies` from any `ComposableEnvironment` subclass, even if you did not expose the dependency using the `@Dependency` property wrapper:
59 | ```swift
60 | environment.mainQueue
61 | ```
62 | This direct access is unfortunately not possible when using `GlobalEnvironment`.
63 |
64 | ## Environments
65 |
66 | The way you define environments differs, whereas you're using `ComposableEnvironment` or `GlobalEnvironment`.
67 |
68 | ### Defining Environments while using `ComposableEnvironment`
69 | When using `ComposableEnvironment`, all your environments need to be subclasses of `ComposableEnvironment`. This is unfortunately required to automatically handle the storage of the private environment values state at a given node. Let define the `ParentEnvironment` exposing the `mainQueue` dependency:
70 | ```swift
71 | public class ParentEnvironment: ComposableEnvironment {
72 | @Dependency(\.mainQueue) var main
73 | }
74 | ```
75 | Imagine that you need to embed a `Child` TCA feature into the `Parent` feature. You declare the embedding using the `@DerivedEnvironment` property wrapper:
76 | ```swift
77 | public class ParentEnvironment: ComposableEnvironment {
78 | @Dependency(\.mainQueue) var main
79 | @DerivedEnvironment var child
80 | }
81 | ```
82 | When you access the `child` property of `ParentEnvironment`, it automatically inherit the dependencies from `ParentEnvironment`. You can pullback `childReducer` using the standard methods:
83 | ```swift
84 | childReducer.pullback(state: \.child, action: /ParentAction.child, environment: \.child)
85 | ```
86 | You can assign a value to the child environment inline with its declaration, or let the library handle the initialization of an instance for you.
87 | In this last case, you can even embed the child reducer using environment-less pullbacks:
88 | ```swift
89 | childReducer.pullback(state: \.child, action: /ParentAction.child)
90 | ```
91 | Note: If you use an environment-less pullback, any initial value you may have defined inline will be discarded. In this case, you should use standard pullbacks with the `\.child` `KeyPath` as a function `(ParentEnvironment) -> ChildEnvironment`.
92 |
93 | ### Defining Environments while using `GlobalEnvironment`
94 | When using `GlobalEnvironment`, your environment, whereas it's a value or a reference type, should conform to the `GlobalEnvironment` protocol.
95 | You can then define and use your dependencies in the same way as for `ComposableEnvironment`. As all dependencies are globally shared and there are no specific dependencies to inherit, it makes less sense to use the `@DerivedEnvironment` property wrapper if you're not using it to define dependency aliases (see below).
96 | ```swift
97 | public struct ParentEnvironment: GlobalEnvironment {
98 | public init() {}
99 | @Dependency(\.mainQueue) var main
100 | }
101 | ```
102 | You still have access to environment-less pullbacks, with the same API:
103 | ```swift
104 | childReducer.pullback(state: \.child, action: /ParentAction.child)
105 | ```
106 | The only requirement for `GlobalEnvironment` is to provide an `init()` initializer. If this is not possible for your child environment, you can still implement the `GlobalDependenciesAccessing` marker protocol which has no requirements but gives your type access to global dependencies using the implicit subscript accessors. You can also do nothing and use the `@Dependency` which has no restriction over its host like the `ComposableEnvironment` version has (it needs to be installed in a `ComposableEnvironment` subclass).
107 | If you can't conform to `GlobalEnvironment`, you only lose access to the environment-less pullbacks.
108 |
109 | ## Assigning values to dependencies
110 | Once dependencies are defined as computed properties of the `Dependencies`, you only access them through your environment, whereas it's a `ComposableEnvironment` subclass or some type conforming to `GlobalDependenciesAccessing`.
111 |
112 | To set a value to a dependency, you use the `with(keyPath,value)` chainable method from your environment:
113 | ```
114 | environment
115 | .with(\.mainQueue, DispatchQueue.main)
116 | .with(\.uuidGenerator, { UUID() })
117 | …
118 | ```
119 | When you're using `GlobalEnvironment`, each dependency is set globally. If you set the same dependency twice, the last call prevails.
120 | When you're using `ComposableEnvironment`, each dependency is set along the dependency tree until it eventually is set again using a `with(keyPath, anotherValue)` call on a child environment. This works in the same fashion as SwiftUI `Environment`.
121 |
122 | ## Aliasing dependencies
123 | In the case the same dependency was defined by different domains using different computed properties in `Dependencies`, you can alias them using the `aliasing(dependencyKeyPath, to: referenceDependencyKeyPath)` chainable method from your environment. For example, if you defined the main queue as `.main` in some feature, and as `mainQueue` in another, you can alias both using
124 | ```swift
125 | environment.aliasing(\.main, to: \.mainQueue)
126 | ```
127 | Once aliased, you can assign a value using either `KeyPath`. If no value is set for the dependency, the second argument provides its default for both `KeyPaths`.
128 |
129 | You can also alias dependencies "on the spot", using the `@DerivedEnvironment` property wrapper. Its initializer provides a closure transforming a provided `AliasBuilder`.
130 | This type has only one chainable method, `alias(dependencyKeyPath, to: referenceDependencyKeyPath)`. For example, if the `main` dependency is defined in the `child` derived environment, you can define an alias to the `mainQueue` dependency from `ParentEnvironment` using:
131 | ```swift
132 | public class ParentEnvironment: ComposableEnvironment {
133 | @Dependency(\.mainQueue) var mainQueue
134 | @DerivedEnvironment(aliases: {
135 | $0.alias(\.main, to: \.mainQueue)
136 | }) var child
137 | }
138 | ```
139 | When using this property wrapper, you don't need to define the alias from the environment using `.aliasing()`.
140 |
141 | Dependencies aliases are always global.
142 |
143 | ## Environment-less pullbacks
144 |
145 | ### Omitting the `@DerivedEnvironment` property wrapper
146 | You can forgo `@DerivedEnvironment` declarations when:
147 | - You don't need to customize dependencies specifically for the child environment when using `ComposableEnvironment`.
148 | - You don't need to alias dependencies "on the spot", using the `@DerivedEnvironment` property wrapper, when using either module.
149 | The example app shows how this feature can be used and mixed with the property-wrapper approach when using `ComposableEnvironment`.
150 |
151 | ### Using environment-less pullbacks
152 | When your environment can be instantiated automatically, you can use environment-less pullbacks:
153 | ```swift
154 | childReducer.pullback(state: \.child, action: /ParentAction.child)
155 | // or, for collections of features:
156 | childReducer.forEach(state: \.children, action: /ParentAction.children)
157 | ```
158 | Please note that in order to access such pullbacks when using `GlobalEnvironment`, your environment needs to conform to the `GlobalEnvironment` protocol.
159 |
160 | ## Choosing between `ComposableEnvironment` and `GlobalEnvironment`
161 | As a rule of thumb, if you need to modify your dependencies in the middle of the environment's tree, you should use `ComposableEnvironment`. If all dependencies are shared across your environments, you should use `GlobalEnvironment`. As the first configuration is quite rare, we recommend using `GlobalEnvironment` if you're in doubt, as it is the simplest to implement in an existing TCA project.
162 |
163 | The principal differences between the two approaches are summarized in the following table:
164 | | | `ComposableEnvironment` | `GlobalEnvironment` |
165 | |---|---|---|
166 | | Environment Type | Classes | Any existential
(struct, classes, etc.) |
167 | | Environment Tree | All nodes should be
`ComposableEnvironment` subclasses | Free,
can opt-in/opt-out at any point |
168 | | Dependency values | Customizable per instance | Globally defined |
169 | | Access to dependencies | `@Dependency`, direct, implicit | `@Dependency`, implicit |
170 |
171 | ## Correspondence with SwiftUI's Environment
172 | In order to ease its learning curve, the library bases its API on SwiftUI's Environment. We have the following functional correspondences:
173 | | SwiftUI | ComposableEnvironment| Usage |
174 | |---|---|---|
175 | |`EnvironmentKey`|`DependencyKey`| Identify a shared value |
176 | |`EnvironmentValues`|`Dependencies`| Expose a shared value |
177 | |`@Environment`|`@Dependency`| Retrieve a shared value |
178 | |`View`|`(Composable/Global)Environment`| A node |
179 | |`View.body`| `@DerivedEnvironment`'s | A list of children of the node |
180 | |`View`
`.environment(keyPath:value:)`|`(Composable/Global)Environment`
`.with(keyPath:value:)`| Set a shared value for a node and its children |
181 |
182 | ## Documentation
183 | The latest documentation for ComposableEnvironment's APIs is available [here](https://github.com/tgrapperon/swift-composable-environment/wiki/ComposableEnvironment-Documentation).
184 |
185 | ## Installation
186 | Add
187 | ```swift
188 | .package(url: "https://github.com/tgrapperon/swift-composable-environment", from: "0.5.0")
189 | ```
190 | to your Package dependencies in `Package.swift`, and then
191 | ```swift
192 | .product(name: "ComposableEnvironment", package: "swift-composable-environment")
193 | // or
194 | .product(name: "GlobalEnvironment", package: "swift-composable-environment")
195 | ```
196 | to your target's dependencies, depending on the module you want to use.
197 |
198 | ## Migrating to TCA's protocol reducers
199 |
200 | When importing the latest versions of TCA, there will likely be ambiguities when using the `@Dependency` property wrapper, or the `DependencyKey` protocol. As this library will give way to TCA, the preferred approach is the following:
201 |
202 | - Define a typealias to both TCA's types in a module you own (or in your application if you're not using modules):
203 | ```swift
204 | import ComposableArchitecture
205 | public typealias DependencyKey = ComposableArchitecture.DependencyKey
206 | public typealias Dependency = ComposableArchitecture.Dependency
207 | ```
208 | Because these typeliases are defined in modules you own, they will be preferred to external definitions when resolving types.
209 | - Replace all occurences of `DependencyKey` by `Compatible.DependencyKey` and `@Dependency` by `@Compatible.Dependency`. You can use Xcode search/replace in all files for this purpose.
210 |
211 | In this state, your project should build without ambiguities.
212 | ```swift
213 | DependencyKey: TCA's DependencyKey
214 | @Dependency: TCA's @Dependency
215 | ---
216 | Compatible.DependencyKey: Composable Environment's DependencyKey
217 | @Compatible.Dependency: Composable Environment's @Dependency
218 | ```
219 | You can then migrate at your rhythm to protocol reducers. Once the migration is complete, you can remove the dependency to this library. I hope it served you well!
220 |
--------------------------------------------------------------------------------
/Sources/ComposableDependencies/ComposableDependencies.swift:
--------------------------------------------------------------------------------
1 | @_exported import struct _Dependencies.Dependencies
2 | @_exported import protocol _Dependencies.DependencyKey
3 |
4 | @available(*, deprecated, renamed: "Dependencies")
5 | public typealias ComposableDependencies = Dependencies
6 |
7 | /// This namespace is used to provide non-clashing variants to the `DependencyKey` protocol and the
8 | /// `@Dependency` property wrapper.
9 | public enum Compatible {}
10 |
--------------------------------------------------------------------------------
/Sources/ComposableEnvironment/ComposableEnvironment.swift:
--------------------------------------------------------------------------------
1 | @_exported import ComposableDependencies
2 | import Foundation
3 | @_implementationOnly import _Dependencies
4 | @_implementationOnly import _DependencyAliases
5 |
6 | /// The base class of your environments.
7 | ///
8 | /// Subclass this class to define your feature's environment. You can expose
9 | /// `Dependencies` values using the ``Dependency`` property wrapper and declare child
10 | /// environment using the ``DerivedEnvironment`` property wrapper.
11 | ///
12 | /// For example, if you define:
13 | /// ```swift
14 | /// extension Dependencies {
15 | /// var uuidGenerator: () -> UUID {…}
16 | /// var mainQueue: AnySchedulerOf {…}
17 | /// },
18 | /// ```
19 | /// you can declare the `LocalEnvironment` class, with `ChildEnvironment1` and `ChildEnvironment2`
20 | /// like:
21 | /// ```swift
22 | /// class LocalEnvironment: ComposableEnvironment {
23 | /// @Dependency(\.uuidGenerator) var uuidGenerator
24 | /// @Dependency(\.mainQueue) var mainQueue
25 | /// @DerivedEnvironment var child1
26 | /// @DerivedEnvironment var child2
27 | /// }
28 | /// ```
29 | /// - Warning: All child environment must be themself subclasses of ``ComposableEnvironment``. If
30 | /// the environments chain is broken, an environment will retrieve the value of a dependency from
31 | /// its farthest direct ascendant, or use the default value if none was specificied. It will not
32 | /// "jump" over ascendants that are not ``ComposableEnvironment`` to retrieve the value of a
33 | /// dependency.
34 | @dynamicMemberLookup
35 | open class ComposableEnvironment {
36 | /// Instantiate a ``ComposableEnvironment`` instance with all dependencies sets to their defaults.
37 | ///
38 | /// After using this initializer, you can chain ``with(_:_:)`` calls to set the values of
39 | /// individual dependencies. These values ill propagate to each child``DerivedEnvironment`` as
40 | /// well as their own children ``DerivedEnvironment``.
41 | public required init() {}
42 |
43 | var dependencies: Dependencies = DependenciesUtilities.new() {
44 | didSet {
45 | // This will make any child refetch its upstream dependencies when accessed.
46 | upToDateDerivedEnvironments.removeAllObjects()
47 | }
48 | }
49 |
50 | static var aliases = DependencyAliases()
51 | var upToDateDerivedEnvironments: NSHashTable = .weakObjects()
52 |
53 | @discardableResult
54 | func updatingFromParentIfNeeded(_ parent: ComposableEnvironment) -> Self {
55 | if !parent.upToDateDerivedEnvironments.contains(self) {
56 | // The following line updates the `environment`'s dependencies, invalidating its children
57 | // dependencies when it mutates its own `dependencies` property as a side effect.
58 | DependenciesUtilities.merge(parent.dependencies, to: &dependencies)
59 | parent.upToDateDerivedEnvironments.add(self)
60 | }
61 | return self
62 | }
63 |
64 | /// Use this function to set the values of a given dependency for this environment and all its
65 | /// descendants.
66 | ///
67 | /// Calls to this function are chainable, and you can specify any `Dependencies`'s
68 | /// `KeyPath`, even if the current environment instance does not expose the corresponding
69 | /// dependency itself.
70 | ///
71 | /// For example, if you define:
72 | /// ```swift
73 | /// extension Dependencies {
74 | /// var uuidGenerator: () -> UUID {…}
75 | /// var mainQueue: AnySchedulerOf {…}
76 | /// },
77 | /// ```
78 | /// you can set their values in a `LocalEnvironment` instance and all its descendants like:
79 | /// ```swift
80 | /// LocalEnvironment()
81 | /// .with(\.uuidGenerator, { UUID() })
82 | /// .with(\.mainQueue, .main)
83 | /// ```
84 | @discardableResult
85 | public func with(_ keyPath: WritableKeyPath, _ value: V) -> Self {
86 | for alias in Self.aliases.aliasing(with: keyPath) {
87 | dependencies[keyPath: alias] = value
88 | }
89 | return self
90 | }
91 |
92 | /// A read-write subcript to directly access a dependency from its `KeyPath` in
93 | /// `Dependencies`.
94 | public subscript(keyPath: WritableKeyPath) -> Value {
95 | get {
96 | dependencies[keyPath: Self.aliases.standardAlias(for: keyPath)]
97 | }
98 | set {
99 | for alias in Self.aliases.aliasing(with: keyPath) {
100 | dependencies[keyPath: alias] = newValue
101 | }
102 | }
103 | }
104 |
105 | /// A read-only subcript to directly access a dependency from `Dependencies`.
106 | /// - Remark: This direct access can't be used to set a dependency, as it will try to go through
107 | /// the setter part of a ``Dependency`` property wrapper, which is not allowed yet. You can use
108 | /// ``with(_:_:)`` or ``subscript(_:)`` instead.
109 | public subscript(
110 | dynamicMember keyPath: KeyPath
111 | ) -> Value {
112 | dependencies[keyPath: Self.aliases.standardAlias(for: keyPath)]
113 | }
114 |
115 | /// Identify a dependency to another one.
116 | ///
117 | /// You can use this method to synchronize identical dependencies from different domains.
118 | /// For example, if you defined a main dispatch queue dependency called `.main` in one domain and
119 | /// `.mainQueue` in another, you can identify both dependencies using
120 | /// ```swift
121 | /// environment.aliasing(\.main, to: \.mainQueue)
122 | /// ```
123 | /// The second argument provides its default value to all aliased dependencies, and all aliased
124 | /// dependencies returns this default value until the value any of the aliased dependencies is
125 | /// set.
126 | ///
127 | /// You can set the value of any aliased dependency using any `KeyPath`:
128 | /// ```swift
129 | /// environment
130 | /// .aliasing(\.main, to: \.mainQueue)
131 | /// .with(\.main, DispatchQueue.main)
132 | /// // is equivalent to:
133 | /// environment
134 | /// .aliasing(\.main, to: \.mainQueue)
135 | /// .with(\.mainQueue, DispatchQueue.main)
136 | /// ```
137 | ///
138 | /// If you chain multiple aliases for the same dependency, the closest to the root is the one
139 | /// responsible for the default value:
140 | /// ```swift
141 | /// environment
142 | /// .aliasing(\.main, to: \.mainQueue) // <- The default value will be the
143 | /// .aliasing(\.uiQueue, to: \.main) // default value of `mainqueue`
144 | /// ```
145 | /// If dependencies aliased through `DerivedEnvironment` are aliased in the order of environment
146 | /// composition, with the dependency closest to the root environment providing the default value
147 | /// if no value is set for any aliased dependency.
148 | ///
149 | /// - Parameters:
150 | /// - dependency: The `KeyPath` of the aliased dependency in `Dependencies`
151 | /// - to: A `KeyPath` of another dependency in `Dependencies` that serves as a reference value.
152 | public func aliasing(
153 | _ dependency: WritableKeyPath,
154 | to default: WritableKeyPath
155 | ) -> Self {
156 | Self.aliases.alias(dependency: dependency, to: `default`)
157 | upToDateDerivedEnvironments.removeAllObjects()
158 | return self
159 | }
160 | }
161 |
162 | extension Dependencies {
163 | /// Use this static method to reset all aliases you may have set between dependencies.
164 | /// You typically call this method during the `setUp()` method of some `XCTestCase` subclass:
165 | /// ```swift
166 | /// class SomeFeatureTests: XCTextCase {
167 | /// override func setUp() {
168 | /// super.setUp()
169 | /// Dependencies.clearAliases()
170 | /// }
171 | /// // …
172 | /// }
173 | /// ```
174 | public static func clearAliases() {
175 | ComposableEnvironment.aliases.clear()
176 | }
177 | }
178 |
179 | extension Compatible {
180 | /// You can use this typealias if `DependencyKey` is clashing with other modules offering
181 | /// a similarly named protocol.
182 | ///
183 | /// You should be able to replace:
184 | /// ```swift
185 | /// struct MainQueueKey: DependencyKey { … }
186 | /// ```
187 | /// by
188 | /// ```swift
189 | /// struct MainQueueKey: Compatible.DependencyKey { … }
190 | /// ```
191 | public typealias DependencyKey = _Dependencies.DependencyKey
192 | }
193 |
--------------------------------------------------------------------------------
/Sources/ComposableEnvironment/Dependency.swift:
--------------------------------------------------------------------------------
1 | import ComposableDependencies
2 |
3 | /// Use this property wrapper to declare depencies in a ``ComposableEnvironment`` subclass.
4 | ///
5 | /// You reference the dependency by its `KeyPath` originating from `Dependencies`, and
6 | /// you declare its name in the local environment. The dependency should not be instantiated, as it
7 | /// is either inherited from a ``ComposableEnvironment`` parent, or installed with
8 | /// ``ComposableEnvironment/with(_:_:)``.
9 | ///
10 | /// For example, if the dependency is declared as:
11 | /// ```swift
12 | /// extension Dependencies {
13 | /// var uuidGenerator: () -> UUID {
14 | /// get { self[UUIDGeneratorKey.self] }
15 | /// set { self[UUIDGeneratorKey.self] = newValue }
16 | /// }
17 | /// },
18 | /// ```
19 | /// you can install it in `LocalEnvironment` like:
20 | /// ```swift
21 | /// class LocalEnvironment: ComposableEnvironment {
22 | /// @Dependency(\.uuidGenerator) var uuid
23 | /// }
24 | /// ```
25 | /// This exposes a `var uuid: () -> UUID` read-only property in the `LocalEnvironment`. This
26 | /// property can then be used as any vanilla dependency.
27 | @propertyWrapper
28 | public struct Dependency {
29 | /// Alternative to ``wrappedValue`` with access to the enclosing instance.
30 | public static subscript(
31 | _enclosingInstance instance: EnclosingSelf,
32 | wrapped wrappedKeyPath: ReferenceWritableKeyPath,
33 | storage storageKeyPath: ReferenceWritableKeyPath
34 | ) -> Value {
35 | get {
36 | let wrapper = instance[keyPath: storageKeyPath]
37 | let keyPath = wrapper.keyPath
38 | let value = instance[keyPath]
39 | return value
40 | }
41 | set {
42 | fatalError("@Dependency are read-only in their ComposableEnvironment")
43 | }
44 | }
45 |
46 | var keyPath: WritableKeyPath
47 |
48 | /// See ``Dependency`` discussion
49 | public init(_ keyPath: WritableKeyPath) {
50 | self.keyPath = keyPath
51 | }
52 |
53 | @available(
54 | *, unavailable,
55 | message:
56 | """
57 | @Dependency should be used in conjunction with a `WritableKeyPath`. Please implement a setter
58 | part in the `Dependencies`'s computed property for this dependency.
59 | """
60 | )
61 | public init(_ keyPath: KeyPath) {
62 | fatalError()
63 | }
64 |
65 | @available(
66 | *, unavailable, message: "@Dependency should be used in a ComposableEnvironment class."
67 | )
68 | public var wrappedValue: Value {
69 | get { fatalError() }
70 | set { fatalError() }
71 | }
72 | }
73 |
74 | /// Convenience typealias in case of name clashes. See ``Compatible.Dependency``.
75 | public typealias ComposableEnvironmentDependency = Dependency
76 |
77 | extension Compatible {
78 | /// You can use this typealias if `@Dependency` is clashing with other modules offering
79 | /// a similarly named property wrapper.
80 | ///
81 | /// You should be able to replace
82 | /// ```swift
83 | /// @Dependency(\.mainQueue) var mainQueue
84 | /// ```
85 | /// by
86 | /// ```swift
87 | /// @Compatible.Dependency(\.mainQueue) var mainQueue
88 | /// ```
89 | public typealias Dependency = ComposableEnvironmentDependency
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/ComposableEnvironment/Deprecations/GlobalEnvironment+Migration.swift:
--------------------------------------------------------------------------------
1 | @available(
2 | *, deprecated,
3 | message:
4 | """
5 | If you are transitioning from `GlobalEnvironment`, you should make sure that is type is a \
6 | subclass of `ComposableEnvironment`.
7 |
8 | If your environment is a `struct`, replacing this by `class` should allow the project to build \
9 | and run again as a temporary workaround.
10 |
11 | If you are not transitioning from `GlobalEnvironment`, you should not have to use this type \
12 | at all. It is only provided to help transitioning projects from `GlobalEnvironment` to \
13 | `ComposableEnvironment`.
14 | """
15 | )
16 | open class GlobalEnvironment: ComposableEnvironment {
17 | public required init() {}
18 | }
19 |
20 | @available(
21 | *, deprecated,
22 | message:
23 | """
24 | If you are transitioning from `GlobalEnvironment`, you should make sure that is type is a \
25 | subclass of `ComposableEnvironment`.
26 |
27 | If your environment is a `struct`, replacing this by `class` should allow the project to build \
28 | and run again as a temporary workaround.
29 |
30 | If you are not transitioning from `GlobalEnvironment`, you should not have to use this type \
31 | at all. It is only provided to help transitioning projects from `GlobalEnvironment` to \
32 | `ComposableEnvironment`.
33 | """
34 | )
35 | open class GlobalDependenciesAccessing: ComposableEnvironment {
36 | public required init() {}
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/ComposableEnvironment/Deprecations/Reducers+Deprecations.swift:
--------------------------------------------------------------------------------
1 | // Deprecated after TCA 0.31.0:
2 | import ComposableArchitecture
3 |
4 | @available(
5 | *,
6 | deprecated,
7 | message: "'pullback' no longer takes a 'breakpointOnNil' argument"
8 | )
9 | extension Reducer where Environment: ComposableEnvironment {
10 | public func pullback(
11 | state toLocalState: CasePath,
12 | action toLocalAction: CasePath,
13 | breakpointOnNil: Bool = true,
14 | _ file: StaticString = #file,
15 | _ line: UInt = #line
16 | ) -> Reducer
17 | where GlobalEnvironment: ComposableEnvironment {
18 | let local = Environment()
19 | return pullback(
20 | state: toLocalState,
21 | action: toLocalAction,
22 | environment: local.updatingFromParentIfNeeded,
23 | breakpointOnNil: breakpointOnNil
24 | )
25 | }
26 |
27 | @available(
28 | *,
29 | deprecated,
30 | message: "'forEach' no longer takes a 'breakpointOnNil' argument"
31 | )
32 | public func forEach(
33 | state toLocalState: WritableKeyPath>,
34 | action toLocalAction: CasePath,
35 | breakpointOnNil: Bool = true,
36 | _ file: StaticString = #file,
37 | _ line: UInt = #line
38 | ) -> Reducer
39 | where GlobalEnvironment: ComposableEnvironment {
40 | let local = Environment()
41 | return forEach(
42 | state: toLocalState,
43 | action: toLocalAction,
44 | environment: local.updatingFromParentIfNeeded,
45 | breakpointOnNil: breakpointOnNil
46 | )
47 | }
48 |
49 | @available(
50 | *,
51 | deprecated,
52 | message: "'forEach' no longer takes a 'breakpointOnNil' argument"
53 | )
54 | public func forEach(
55 | state toLocalState: WritableKeyPath,
56 | action toLocalAction: CasePath,
57 | breakpointOnNil: Bool = true,
58 | _ file: StaticString = #file,
59 | _ line: UInt = #line
60 | ) -> Reducer
61 | where GlobalEnvironment: ComposableEnvironment {
62 | let local = Environment()
63 | return forEach(
64 | state: toLocalState,
65 | action: toLocalAction,
66 | environment: local.updatingFromParentIfNeeded,
67 | breakpointOnNil: breakpointOnNil
68 | )
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/ComposableEnvironment/DerivedEnvironment.swift:
--------------------------------------------------------------------------------
1 | import ComposableDependencies
2 |
3 | /// Use this property wrapper to declare child ``ComposableEnvironment`` in a
4 | /// ``ComposableEnvironment`` subclass.
5 | ///
6 | /// You only need to specify the subclass used and its name. You don't need to instantiate the
7 | /// subclass. For example, if `ChildEnvironment` is a ``ComposableEnvironment`` subclass, you can
8 | /// install a representant in `ParentEnvironment` as:
9 | /// ```swift
10 | /// class ParentEnvironment: ComposableEnvironment {
11 | /// @DerivedEnvironment var child
12 | /// }.
13 | /// ```
14 | /// This exposes a `var child: ChildEnvironment` read-only property in the `ParentEnvironment`.
15 | /// This child environment inherits the current dependencies of all its ancestor. They can be
16 | /// exposed using the ``Dependency`` property wrapper.
17 | ///
18 | /// You can also use this property wrapper is to define `DependencyAlias`'s using the
19 | /// ``AliasBuilder`` closure from the intializers:
20 | /// ```swift
21 | /// struct ParentEnvironment: GlobalEnvironment {
22 | /// @DerivedEnvironment(aliases: {
23 | /// $0.alias(\.main, to: \.mainQueue)
24 | /// }) var child
25 | /// }
26 | /// ```
27 | @propertyWrapper
28 | public final class DerivedEnvironment where Environment: ComposableEnvironment {
29 | /// Alternative to ``wrappedValue`` with access to the enclosing instance.
30 | public static subscript(
31 | _enclosingInstance instance: EnclosingSelf,
32 | wrapped wrappedKeyPath: ReferenceWritableKeyPath,
33 | storage storageKeyPath: ReferenceWritableKeyPath
34 | ) -> Environment {
35 | get {
36 | let environment = instance[keyPath: storageKeyPath]
37 | .environment
38 | if !instance[keyPath: storageKeyPath].didSetAliases,
39 | let aliasBuilder = instance[keyPath: storageKeyPath].aliasBuilder
40 | {
41 | defer { instance[keyPath: storageKeyPath].didSetAliases = true }
42 | return aliasBuilder.transforming(environment)
43 | }
44 |
45 | return environment.updatingFromParentIfNeeded(instance)
46 | }
47 | set {
48 | fatalError("@DerivedEnvironments are read-only in their parent")
49 | }
50 | }
51 |
52 | lazy var environment: Environment = .init()
53 |
54 | var aliasBuilder: AliasBuilder?
55 | var didSetAliases: Bool = false
56 |
57 | /// See ``DerivedEnvironment`` discussion
58 | public init(
59 | wrappedValue: Environment,
60 | aliases: (
61 | (AliasBuilder)
62 | -> AliasBuilder
63 | )? = nil
64 | ) {
65 | self.environment = wrappedValue
66 | self.aliasBuilder = aliases.map { $0(.init()) }
67 | }
68 |
69 | /// See ``DerivedEnvironment`` discussion
70 | public init(aliases: ((AliasBuilder) -> AliasBuilder)? = nil) {
71 | self.aliasBuilder = aliases.map { $0(.init()) }
72 | }
73 |
74 | @available(
75 | *, unavailable,
76 | message: "@DerivedEnvironment should be used in a ComposableEnvironment class."
77 | )
78 | public var wrappedValue: Environment {
79 | get { fatalError() }
80 | set { fatalError() }
81 | }
82 | }
83 |
84 | /// A type that is used to configure dependencies aliases when using the ``DerivedEnvironment``
85 | /// property wrapper.
86 | public struct AliasBuilder where Environment: ComposableEnvironment {
87 | var transforming: (Environment) -> Environment = { $0 }
88 |
89 | /// Add a new dependency alias to the builder
90 | ///
91 | /// You can chain calls to define multiple aliases:
92 | /// ```swift
93 | /// builder
94 | /// .alias(\.main, to: \.mainQueue)
95 | /// .alias(\.uuid, to: \.idGenerator)
96 | /// …
97 | /// ```
98 | /// See the discussion at `DependenciesAccessing.aliasing(:to:)` for more information.
99 | ///
100 | /// - Parameters:
101 | /// - dependency: The `KeyPath` of the aliased dependency in `Dependencies`
102 | /// - to: A `KeyPath` of another dependency in `Dependencies` that serves as a reference value.
103 | public func alias(
104 | _ dependency: WritableKeyPath,
105 | to default: WritableKeyPath
106 | ) -> Self {
107 | AliasBuilder {
108 | transforming($0)
109 | .aliasing(dependency, to: `default`)
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/Sources/ComposableEnvironment/Reducer+ComposableEnvironment.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 |
3 | extension Reducer where Environment: ComposableEnvironment {
4 | /// Transforms a reducer that works on local state, action, and environment into one that works on
5 | /// global state, action and environment when the local environment is a subclass of
6 | /// ``ComposableEnvironment``.
7 | /// It accomplishes this by providing 2 transformations to the method:
8 | ///
9 | /// * A writable key path that can get/set a piece of local state from the global state.
10 | /// * A case path that can extract/embed a local action into a global action.
11 | ///
12 | /// Because the environment is ``ComposableEnvironment``, its lifecycle is automatically managed
13 | /// by the library.
14 | /// For more information about this reducer, see the discussion about the equivalent function
15 | /// using unbounded environments in `swift-composable-architecture`.
16 | ///
17 | /// - Parameters:
18 | /// - toLocalState: A key path that can get/set `State` inside `GlobalState`.
19 | /// - toLocalAction: A case path that can extract/embed `Action` from `GlobalAction`.
20 | /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`.
21 | public func pullback(
22 | state toLocalState: WritableKeyPath,
23 | action toLocalAction: CasePath
24 | ) -> Reducer
25 | where GlobalEnvironment: ComposableEnvironment {
26 | let local = Environment()
27 | return pullback(
28 | state: toLocalState,
29 | action: toLocalAction,
30 | environment: local.updatingFromParentIfNeeded
31 | )
32 | }
33 |
34 | /// Transforms a reducer that works on local state, action, and environment into one that works on
35 | /// global state, action and environmentwhen the local environment is a subclass of
36 | /// ``ComposableEnvironment``.
37 | ///
38 | /// It accomplishes this by providing 2 transformations to the method:
39 | ///
40 | /// * A case path that can extract/embed a piece of local state from the global state, which is
41 | /// typically an enum.
42 | /// * A case path that can extract/embed a local action into a global action.
43 | ///
44 | /// Because the environment is ``ComposableEnvironment``, its lifecycle is automatically managed
45 | /// by the library.
46 | /// For more information about this reducer, see the discussion about the equivalent function
47 | /// using unbounded environments in `swift-composable-architecture`.
48 | ///
49 | /// - Parameters:
50 | /// - toLocalState: A case path that can extract/embed `State` from `GlobalState`.
51 | /// - toLocalAction: A case path that can extract/embed `Action` from `GlobalAction`.
52 | /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`.
53 | public func pullback(
54 | state toLocalState: CasePath,
55 | action toLocalAction: CasePath,
56 | _ file: StaticString = #file,
57 | _ line: UInt = #line
58 | ) -> Reducer
59 | where GlobalEnvironment: ComposableEnvironment {
60 | let local = Environment()
61 | return pullback(
62 | state: toLocalState,
63 | action: toLocalAction,
64 | environment: local.updatingFromParentIfNeeded
65 | )
66 | }
67 |
68 | /// A version of ``pullback(state:action)`` that transforms a reducer that works on
69 | /// an element into one that works on an identified array of elements, when the local environment
70 | /// is a subclass of``ComposableEnvironment``.
71 | ///
72 | /// For more information about this reducer, see the discussion about the equivalent function
73 | /// using unbounded environments in `swift-composable-architecture`.
74 | ///
75 | /// - Parameters:
76 | /// - toLocalState: A key path that can get/set a collection of `State` elements inside
77 | /// `GlobalState`.
78 | /// - toLocalAction: A case path that can extract/embed `(Collection.Index, Action)` from
79 | /// `GlobalAction`.
80 | /// - breakpointOnNil: Raises `SIGTRAP` signal when an action is sent to the reducer but the
81 | /// identified array does not contain an element with the action's identifier. This is
82 | /// generally considered a logic error, as a child reducer cannot process a child action
83 | /// for unavailable child state.
84 | /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`.
85 | public func forEach(
86 | state toLocalState: WritableKeyPath>,
87 | action toLocalAction: CasePath,
88 | _ file: StaticString = #file,
89 | _ line: UInt = #line
90 | ) -> Reducer
91 | where GlobalEnvironment: ComposableEnvironment {
92 | let local = Environment()
93 | return forEach(
94 | state: toLocalState,
95 | action: toLocalAction,
96 | environment: local.updatingFromParentIfNeeded
97 | )
98 | }
99 |
100 | /// A version of ``pullback(state:action:environment:)`` that transforms a reducer that works on
101 | /// an element into one that works on a dictionary of element values, when the local environment
102 | /// is a subclass of``ComposableEnvironment``.
103 | ///
104 | /// For more information about this reducer, see the discussion about the equivalent function
105 | /// using unbounded environments in `swift-composable-architecture`.
106 | ///
107 | /// - Parameters:
108 | /// - toLocalState: A key path that can get/set a dictionary of `State` values inside
109 | /// `GlobalState`.
110 | /// - toLocalAction: A case path that can extract/embed `(Key, Action)` from `GlobalAction`.
111 | /// - breakpointOnNil: Raises `SIGTRAP` signal when an action is sent to the reducer but the
112 | /// identified array does not contain an element with the action's identifier. This is
113 | /// generally considered a logic error, as a child reducer cannot process a child action
114 | /// for unavailable child state.
115 | /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`.
116 | public func forEach(
117 | state toLocalState: WritableKeyPath,
118 | action toLocalAction: CasePath,
119 | _ file: StaticString = #file,
120 | _ line: UInt = #line
121 | ) -> Reducer
122 | where GlobalEnvironment: ComposableEnvironment {
123 | let local = Environment()
124 | return forEach(
125 | state: toLocalState,
126 | action: toLocalAction,
127 | environment: local.updatingFromParentIfNeeded
128 | )
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/Sources/GlobalEnvironment/Dependency.swift:
--------------------------------------------------------------------------------
1 | import ComposableDependencies
2 |
3 | /// Use this property wrapper to access global depencies anywhere.
4 | ///
5 | /// You reference the dependency by its `KeyPath` originating from `Dependencies`, and
6 | /// you declare its name in the local environment. The dependency should not be instantiated.
7 | ///
8 | /// For example, if the dependency is declared as:
9 | /// ```swift
10 | /// extension Dependencies {
11 | /// var uuidGenerator: () -> UUID {
12 | /// get { self[UUIDGeneratorKey.self] }
13 | /// set { self[UUIDGeneratorKey.self] = newValue }
14 | /// }
15 | /// },
16 | /// ```
17 | /// you can install it in `LocalEnvironment` like:
18 | /// ```swift
19 | /// struct LocalEnvironment {
20 | /// @Dependency(\.uuidGenerator) var uuid
21 | /// }
22 | /// ```
23 | /// This exposes a `var uuid: () -> UUID` read-only property in the `LocalEnvironment`. This
24 | /// property can then be used as any vanilla dependency.
25 | @propertyWrapper
26 | public struct Dependency {
27 | var keyPath: KeyPath
28 |
29 | /// See ``Dependency`` discussion
30 | public init(_ keyPath: KeyPath) {
31 | self.keyPath = keyPath
32 | }
33 |
34 | public var wrappedValue: Value {
35 | Dependencies.global[keyPath: Dependencies.aliases.standardAlias(for: keyPath)]
36 | }
37 | }
38 |
39 | /// Convenience typealias in case of name clashes. See ``Compatible.Dependency``.
40 | public typealias ComposableEnvironmentDependency = Dependency
41 |
42 | extension Compatible {
43 | /// You can use this typealias if `@Dependency` is clashing with other modules offering
44 | /// a similarly named property wrapper.
45 | ///
46 | /// You should be able to replace
47 | /// ```swift
48 | /// @Dependency(\.mainQueue) var mainQueue
49 | /// ```
50 | /// by
51 | /// ```swift
52 | /// @Compatible.Dependency(\.mainQueue) var mainQueue
53 | /// ```
54 | public typealias Dependency = ComposableEnvironmentDependency
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/GlobalEnvironment/Deprecations/ComposableEnvironment+Migration.swift:
--------------------------------------------------------------------------------
1 | @available(
2 | *, deprecated,
3 | message:
4 | """
5 | If you are transitioning from `ComposableEnvironment`, you should replace this class by a type \
6 | conforming to `GlobalEnvironment` or `GlobalDependenciesAccessing`. Please make sure that you \
7 | are not overriding dependencies mid-chain, as all dependencies are shared globally when using \
8 | `GlobalEnvironment`. If your project depends on mid-chain dependencies overrides, using \
9 | `GlobalEnvironment` will likely produce incoherent results. In this case, you should continue \
10 | using `ComposableEnvironment`.
11 |
12 | If you are not transitioning from `ComposableEnvironment`, you should not have to use this type \
13 | at all. It is only provided to help transitioning projects from `ComposableEnvironment` to \
14 | `GlobalEnvironment`.
15 | """
16 | )
17 | open class ComposableEnvironment: GlobalEnvironment {
18 | public required init() {}
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/GlobalEnvironment/Deprecations/Reducers+Deprecations.swift:
--------------------------------------------------------------------------------
1 | // Deprecated after TCA 0.31.0:
2 | import ComposableArchitecture
3 |
4 | @available(
5 | *,
6 | deprecated,
7 | message: "'pullback' no longer takes a 'breakpointOnNil' argument"
8 | )
9 | extension Reducer where Environment: GlobalEnvironment {
10 | public func pullback(
11 | state toLocalState: CasePath,
12 | action toLocalAction: CasePath,
13 | breakpointOnNil: Bool = true,
14 | _ file: StaticString = #file,
15 | _ line: UInt = #line
16 | ) -> Reducer {
17 | let local = Environment()
18 | return pullback(
19 | state: toLocalState,
20 | action: toLocalAction,
21 | environment: { _ in local },
22 | breakpointOnNil: breakpointOnNil
23 | )
24 | }
25 |
26 | @available(
27 | *,
28 | deprecated,
29 | message: "'forEach' no longer takes a 'breakpointOnNil' argument"
30 | )
31 | public func forEach(
32 | state toLocalState: WritableKeyPath>,
33 | action toLocalAction: CasePath,
34 | breakpointOnNil: Bool = true,
35 | _ file: StaticString = #file,
36 | _ line: UInt = #line
37 | ) -> Reducer {
38 | let local = Environment()
39 | return forEach(
40 | state: toLocalState,
41 | action: toLocalAction,
42 | environment: { _ in local },
43 | breakpointOnNil: breakpointOnNil
44 | )
45 | }
46 |
47 | @available(
48 | *,
49 | deprecated,
50 | message: "'forEach' no longer takes a 'breakpointOnNil' argument"
51 | )
52 | public func forEach(
53 | state toLocalState: WritableKeyPath,
54 | action toLocalAction: CasePath,
55 | breakpointOnNil: Bool = true,
56 | _ file: StaticString = #file,
57 | _ line: UInt = #line
58 | ) -> Reducer {
59 | let local = Environment()
60 | return forEach(
61 | state: toLocalState,
62 | action: toLocalAction,
63 | environment: { _ in local },
64 | breakpointOnNil: breakpointOnNil
65 | )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/GlobalEnvironment/DerivedEnvironment.swift:
--------------------------------------------------------------------------------
1 | import ComposableDependencies
2 |
3 | /// Use this property wrapper to declare some child ``GlobalEnvironment`` in a
4 | /// ``GlobalEnvironment`` parent.
5 | ///
6 | /// You only need to specify the type used and its name. You don't need to instantiate the
7 | /// type. For example, if `ChildEnvironment` is some ``GlobalEnvironment``, you can install a
8 | /// representant in `ParentEnvironment` as:
9 | /// ```swift
10 | /// struct ParentEnvironment: GlobalEnvironment {
11 | /// @DerivedEnvironment var child
12 | /// }.
13 | /// ```
14 | /// This exposes a `var child: ChildEnvironment` read-only property in the `ParentEnvironment`.
15 | ///
16 | /// When using `GlobalEnvironment`, the principal use of this property wrapper is to define
17 | /// `DependencyAlias`'s using the ``AliasBuilder`` closure from the intializers:
18 | /// ```swift
19 | /// struct ParentEnvironment: GlobalEnvironment {
20 | /// @DerivedEnvironment(aliases: {
21 | /// $0.alias(\.main, to: \.mainQueue)
22 | /// }) var child
23 | /// }
24 | /// ```
25 | @propertyWrapper
26 | public final class DerivedEnvironment where Environment: GlobalEnvironment {
27 | lazy var environment: Environment = .init()
28 | var aliasBuilder: AliasBuilder?
29 | var didSetAliases: Bool = false
30 |
31 | /// See ``DerivedEnvironment`` discussion
32 | public init(
33 | wrappedValue: Environment,
34 | aliases: ((AliasBuilder) -> AliasBuilder)? = nil
35 | ) {
36 | self.environment = wrappedValue
37 | self.aliasBuilder = aliases.map { $0(.init()) }
38 | }
39 |
40 | /// See ``DerivedEnvironment`` discussion
41 | public init(aliases: ((AliasBuilder) -> AliasBuilder)? = nil) {
42 | self.aliasBuilder = aliases.map { $0(.init()) }
43 | }
44 |
45 | public var wrappedValue: Environment {
46 | if !didSetAliases, let aliasBuilder = aliasBuilder {
47 | defer { didSetAliases = true }
48 | return aliasBuilder.transforming(environment)
49 | }
50 | return environment
51 | }
52 | }
53 |
54 | /// A type that is used to configure dependencies aliases when using the ``DerivedEnvironment``
55 | /// property wrapper.
56 | public struct AliasBuilder where Environment: GlobalDependenciesAccessing {
57 | var transforming: (Environment) -> Environment = { $0 }
58 |
59 | /// Add a new dependency alias to the builder
60 | ///
61 | /// You can chain calls to define multiple aliases:
62 | /// ```swift
63 | /// builder
64 | /// .alias(\.main, to: \.mainQueue)
65 | /// .alias(\.uuid, to: \.idGenerator)
66 | /// …
67 | /// ```
68 | /// See the discussion at `DependenciesAccessing.aliasing(:to:)` for more information.
69 | ///
70 | /// - Parameters:
71 | /// - dependency: The `KeyPath` of the aliased dependency in `Dependencies`
72 | /// - to: A `KeyPath` of another dependency in `Dependencies` that serves as a reference value.
73 | public func alias(
74 | _ dependency: WritableKeyPath,
75 | to default: WritableKeyPath
76 | ) -> Self {
77 | AliasBuilder {
78 | transforming($0)
79 | .aliasing(dependency, to: `default`)
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/GlobalEnvironment/GlobalEnvironment.swift:
--------------------------------------------------------------------------------
1 | @_exported import ComposableDependencies
2 | @_implementationOnly import _Dependencies
3 | @_implementationOnly import _DependencyAliases
4 |
5 | extension Dependencies {
6 | static var global: Dependencies = DependenciesUtilities.new()
7 | static var aliases = DependencyAliases()
8 | }
9 |
10 | /// A marker protocol that provides convenient access to global dependencies.
11 | ///
12 | /// This protocol has not requirements. By conforming to it you expose some convenient methods to
13 | /// setup and access global depencies.
14 | public protocol GlobalDependenciesAccessing {}
15 |
16 | /// A protocol characterizing a type that has no local dependencies.
17 | ///
18 | /// If your environment has no local dependencies, that is, if all dependencies are global, you can
19 | /// make it conform to ``GlobalEnvironment``. This opens access to environment-less pullbacks on
20 | /// Reducers using this environment.
21 | ///
22 | /// The only requirement is to provide an argument-less initializer. A default implementation is
23 | /// provided.
24 | public protocol GlobalEnvironment: GlobalDependenciesAccessing {
25 | /// An argument-less initializer.
26 | init()
27 | }
28 |
29 | extension GlobalDependenciesAccessing {
30 | /// Use this function to set the values of a given dependency for the global environment.
31 | ///
32 | /// Calls to this function are chainable, and you can specify any `Dependencies`
33 | /// `KeyPath`, even if the current environment does not expose the corresponding
34 | /// dependency itself.
35 | ///
36 | /// For example, if you define:
37 | /// ```swift
38 | /// extension Dependencies {
39 | /// var uuidGenerator: () -> UUID {…}
40 | /// var mainQueue: AnySchedulerOf {…}
41 | /// },
42 | /// ```
43 | /// you can set their values in a `LocalEnvironment` instance and all its descendants like:
44 | /// ```swift
45 | /// LocalEnvironment()
46 | /// .with(\.uuidGenerator, { UUID() })
47 | /// .with(\.mainQueue, .main)
48 | /// ```
49 | @discardableResult
50 | public func with(_ keyPath: WritableKeyPath, _ value: V) -> Self {
51 | for alias in Dependencies.aliases.aliasing(with: keyPath) {
52 | Dependencies.global[keyPath: alias] = value
53 | }
54 | return self
55 | }
56 |
57 | /// A read-write subcript to directly access a dependency from its `KeyPath` in
58 | /// `Dependencies`.
59 | public subscript(keyPath: WritableKeyPath) -> Value {
60 | get { Dependencies.global[keyPath: Dependencies.aliases.standardAlias(for: keyPath)] }
61 | set {
62 | for alias in Dependencies.aliases.aliasing(with: keyPath) {
63 | Dependencies.global[keyPath: alias] = newValue
64 | }
65 | }
66 | }
67 |
68 | /// A read-only subcript to directly access a global dependency from `Dependencies`.
69 | /// - Remark: This direct access can't be used to set a dependency, as it will try to go through
70 | /// the setter part of a `Dependency` property wrapper, which is not allowed yet. You can use
71 | /// ``with(_:_:)`` or ``subscript(_:)`` instead.
72 | public subscript(
73 | dynamicMember keyPath: KeyPath
74 | ) -> Value {
75 | Dependencies.global[keyPath: Dependencies.aliases.standardAlias(for: keyPath)]
76 | }
77 |
78 | /// Identify a dependency to another one.
79 | ///
80 | /// You can use this method to synchronize identical dependencies from different domains.
81 | /// For example, if you defined a main dispatch queue dependency called `.main` in one domain and
82 | /// `.mainQueue` in another, you can identify both dependencies using
83 | /// ```swift
84 | /// environment.aliasing(\.main, to: \.mainQueue)
85 | /// ```
86 | /// The second argument provides its default value to all aliased dependencies, and all aliased
87 | /// dependencies returns this default value until the value any of the aliased dependencies is
88 | /// set.
89 | ///
90 | /// You can set the value of any aliased dependency using any `KeyPath`:
91 | /// ```swift
92 | /// environment
93 | /// .aliasing(\.main, to: \.mainQueue)
94 | /// .with(\.main, DispatchQueue.main)
95 | /// // is equivalent to:
96 | /// environment
97 | /// .aliasing(\.main, to: \.mainQueue)
98 | /// .with(\.mainQueue, DispatchQueue.main)
99 | /// ```
100 | ///
101 | /// If you chain multiple aliases for the same dependency, the closest to the root is the one
102 | /// responsible for the default value:
103 | /// ```swift
104 | /// environment
105 | /// .aliasing(\.main, to: \.mainQueue) // <- The default value will be the
106 | /// .aliasing(\.uiQueue, to: \.main) // default value of `mainqueue`
107 | /// ```
108 | /// If dependencies aliased through `DerivedEnvironment` are aliased in the order of environment
109 | /// composition, with the dependency closest to the root environment providing the default value
110 | /// if no value is set for any aliased dependency.
111 | ///
112 | /// - Parameters:
113 | /// - dependency: The `KeyPath` of the aliased dependency in `Dependencies`
114 | /// - to: A `KeyPath` of another dependency in `Dependencies` that serves as a reference value.
115 | public func aliasing(
116 | _ dependency: WritableKeyPath,
117 | to default: WritableKeyPath
118 | ) -> Self {
119 | Dependencies.aliases.alias(dependency: dependency, to: `default`)
120 | return self
121 | }
122 | }
123 |
124 | extension Dependencies {
125 | /// Use this static method to reset all aliases you may have set between dependencies.
126 | /// You typically call this method during the `setUp()` method of some `XCTestCase` subclass:
127 | /// ```swift
128 | /// class SomeFeatureTests: XCTextCase {
129 | /// override func setUp() {
130 | /// super.setUp()
131 | /// Dependencies.clearAliases()
132 | /// }
133 | /// // …
134 | /// }
135 | /// ```
136 | public static func clearAliases() {
137 | Self.aliases.clear()
138 | }
139 | }
140 |
141 | extension Dependencies {
142 | /// Use this static method to reset all global depedencies to their default values.
143 | /// You typically call this method during the `setUp()` method of some `XCTestCase` subclass:
144 | /// ```swift
145 | /// class SomeFeatureTests: XCTextCase {
146 | /// override func setUp() {
147 | /// super.setUp()
148 | /// Dependencies.reset()
149 | /// }
150 | /// // …
151 | /// }
152 | /// ```
153 | public static func reset() {
154 | Dependencies.global = DependenciesUtilities.new()
155 | }
156 | }
157 |
158 | extension Compatible {
159 | /// You can use this typealias if `DependencyKey` is clashing with other modules offering
160 | /// a similarly named protocol.
161 | ///
162 | /// You should be able to replace:
163 | /// ```swift
164 | /// struct MainQueueKey: DependencyKey { … }
165 | /// ```
166 | /// by
167 | /// ```swift
168 | /// struct MainQueueKey: Compatible.DependencyKey { … }
169 | /// ```
170 | public typealias DependencyKey = _Dependencies.DependencyKey
171 | }
172 |
--------------------------------------------------------------------------------
/Sources/GlobalEnvironment/Reducer+GlobalEnvironment.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 |
3 | extension Reducer where Environment: GlobalEnvironment {
4 | /// Transforms a reducer that works on local state, action, and environment into one that works on
5 | /// global state, action and environment when the local environment is some ``GlobalEnvironment``.
6 | /// It accomplishes this by providing 2 transformations to the method:
7 | ///
8 | /// * A writable key path that can get/set a piece of local state from the global state.
9 | /// * A case path that can extract/embed a local action into a global action.
10 | ///
11 | /// Because the environment is ``GlobalEnvironment``, its lifecycle is automatically managed by
12 | /// the library.
13 | /// For more information about this reducer, see the discussion about the equivalent function
14 | /// using unbounded environments in `swift-composable-architecture`.
15 | ///
16 | /// - Parameters:
17 | /// - toLocalState: A key path that can get/set `State` inside `GlobalState`.
18 | /// - toLocalAction: A case path that can extract/embed `Action` from `GlobalAction`.
19 | /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`.
20 | public func pullback(
21 | state toLocalState: WritableKeyPath,
22 | action toLocalAction: CasePath
23 | ) -> Reducer {
24 | let local = Environment()
25 | return pullback(
26 | state: toLocalState,
27 | action: toLocalAction,
28 | environment: { _ in local }
29 | )
30 | }
31 |
32 | /// Transforms a reducer that works on local state, action, and environment into one that works on
33 | /// global state, action and environmentwhen the local environment is a ``GlobalEnvironment``.
34 | ///
35 | /// It accomplishes this by providing 2 transformations to the method:
36 | ///
37 | /// * A case path that can extract/embed a piece of local state from the global state, which is
38 | /// typically an enum.
39 | /// * A case path that can extract/embed a local action into a global action.
40 | ///
41 | /// Because the environment is ``GlobalEnvironment``, its lifecycle is automatically managed by
42 | /// the library.
43 | /// For more information about this reducer, see the discussion about the equivalent function using
44 | /// unbounded environments in `swift-composable-architecture`.
45 | ///
46 | /// - Parameters:
47 | /// - toLocalState: A case path that can extract/embed `State` from `GlobalState`.
48 | /// - toLocalAction: A case path that can extract/embed `Action` from `GlobalAction`.
49 | /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`.
50 | public func pullback(
51 | state toLocalState: CasePath,
52 | action toLocalAction: CasePath,
53 | _ file: StaticString = #file,
54 | _ line: UInt = #line
55 | ) -> Reducer {
56 | let local = Environment()
57 | return pullback(
58 | state: toLocalState,
59 | action: toLocalAction,
60 | environment: { _ in local }
61 | )
62 | }
63 |
64 | /// A version of ``pullback(state:action)`` that transforms a reducer that works on
65 | /// an element into one that works on an identified array of elements, when the local environment
66 | /// is some ``GlobalEnvironment``.
67 | ///
68 | /// For more information about this reducer, see the discussion about the equivalent function
69 | /// using unbounded environments in `swift-composable-architecture`.
70 | ///
71 | /// - Parameters:
72 | /// - toLocalState: A key path that can get/set a collection of `State` elements inside
73 | /// `GlobalState`.
74 | /// - toLocalAction: A case path that can extract/embed `(Collection.Index, Action)` from
75 | /// `GlobalAction`.
76 | /// - breakpointOnNil: Raises `SIGTRAP` signal when an action is sent to the reducer but the
77 | /// identified array does not contain an element with the action's identifier. This is
78 | /// generally considered a logic error, as a child reducer cannot process a child action
79 | /// for unavailable child state.
80 | /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`.
81 | public func forEach(
82 | state toLocalState: WritableKeyPath>,
83 | action toLocalAction: CasePath,
84 | _ file: StaticString = #file,
85 | _ line: UInt = #line
86 | ) -> Reducer {
87 | let local = Environment()
88 | return forEach(
89 | state: toLocalState,
90 | action: toLocalAction,
91 | environment: { _ in local }
92 | )
93 | }
94 |
95 | /// A version of ``pullback(state:action:environment:)`` that transforms a reducer that works on
96 | /// an element into one that works on a dictionary of element values, when the local environment
97 | /// is some ``GlobalEnvironment``.
98 | ///
99 | /// For more information about this reducer, see the discussion about the equivalent function
100 | /// using unbounded environments in `swift-composable-architecture`.
101 | ///
102 | /// - Parameters:
103 | /// - toLocalState: A key path that can get/set a dictionary of `State` values inside
104 | /// `GlobalState`.
105 | /// - toLocalAction: A case path that can extract/embed `(Key, Action)` from `GlobalAction`.
106 | /// - breakpointOnNil: Raises `SIGTRAP` signal when an action is sent to the reducer but the
107 | /// identified array does not contain an element with the action's identifier. This is
108 | /// generally considered a logic error, as a child reducer cannot process a child action
109 | /// for unavailable child state.
110 | /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`.
111 | public func forEach(
112 | state toLocalState: WritableKeyPath,
113 | action toLocalAction: CasePath,
114 | _ file: StaticString = #file,
115 | _ line: UInt = #line
116 | ) -> Reducer {
117 | let local = Environment()
118 | return forEach(
119 | state: toLocalState,
120 | action: toLocalAction,
121 | environment: { _ in local }
122 | )
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Sources/_Dependencies/Dependencies.swift:
--------------------------------------------------------------------------------
1 | /// This type acts as a namespace to reference your dependencies.
2 | ///
3 | /// To declare a dependency, create a ``DependencyKey``, and declare a computed property in this
4 | /// type like you would declare a custom `EnvironmentValue` in SwiftUI. For example, if
5 | /// `UUIDGeneratorKey` is a ``DependencyKey`` with ``DependencyKey/Value`` == `() -> UUID`:
6 | /// ```swift
7 | /// extension Dependencies {
8 | /// var uuidGenerator: () -> UUID {
9 | /// get { self[UUIDGeneratorKey.self] }
10 | /// set { self[UUIDGeneratorKey.self] = newValue }
11 | /// }
12 | /// }
13 | /// ```
14 | /// This dependency can then be referenced by its keypath `\.uuidGenerator` when invoking the
15 | /// `Dependency` property wrapper.
16 | public struct Dependencies {
17 | /// This wrapper enum allows to distinguish dependencies that where defined explicitely for a
18 | /// given environment from dependencies that were inherited from their parent environment.
19 | fileprivate enum DependencyValue {
20 | case defined(Any)
21 | case inherited(Any)
22 |
23 | var value: Any {
24 | switch self {
25 | case let .defined(value): return value
26 | case let .inherited(value): return value
27 | }
28 | }
29 |
30 | func inherit() -> DependencyValue {
31 | switch self {
32 | case let .defined(value): return .inherited(value)
33 | case .inherited: return self
34 | }
35 | }
36 |
37 | var isDefined: Bool {
38 | switch self {
39 | case .defined: return true
40 | case .inherited: return false
41 | }
42 | }
43 | }
44 |
45 | fileprivate var values = [ObjectIdentifier: DependencyValue]()
46 |
47 | fileprivate init() {}
48 |
49 | public subscript(_ key: T.Type) -> T.Value where T: DependencyKey {
50 | get { values[ObjectIdentifier(key)]?.value as? T.Value ?? key.defaultValue }
51 | set { values[ObjectIdentifier(key)] = .defined(newValue) }
52 | }
53 | }
54 |
55 | // This type is used internally only
56 | public enum DependenciesUtilities {
57 | public static func new() -> Dependencies { .init() }
58 | public static func merge(_ upstream: Dependencies, to dependencies: inout Dependencies) {
59 | // We should preserve dependencies that were defined explicitely.
60 | for (key, value) in upstream.values {
61 | guard dependencies.values[key]?.isDefined != true else { continue }
62 | dependencies.values[key] = value.inherit()
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/_Dependencies/DependencyKey.swift:
--------------------------------------------------------------------------------
1 | /// Conform types to this protocol to define dependencies as ``Dependencies`` computed
2 | /// properties.
3 | ///
4 | /// You use this protocol like `EnvironmentKey` are used in SwiftUI. Types conforming to this
5 | /// protocol can then be used to declare the dependency in the ``Dependencies`` namespace.
6 | public protocol DependencyKey {
7 | associatedtype Value
8 | /// The default value returned when accessing the corresponding dependency when no value was
9 | /// defined by one of its parents.
10 | static var defaultValue: Self.Value { get }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/_DependencyAliases/DependencyAliases.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct DependencyAliases {
4 | var aliases: [AnyHashable: AnyHashable] = [:]
5 |
6 | public init() {}
7 |
8 | public mutating func clear() {
9 | aliases.removeAll()
10 | }
11 |
12 | public mutating func alias(dependency: T, to default: T) where T: Hashable {
13 | if let existingForDefault = aliases[`default`] as? T {
14 | aliases[dependency] = existingForDefault
15 | } else {
16 | aliases[dependency] = `default`
17 | }
18 | }
19 |
20 | public func standardAlias(for dependency: T) -> T where T: Hashable {
21 | if aliases.isEmpty { return dependency }
22 | return path(for: dependency).last ?? dependency
23 | }
24 |
25 | func path(for dependency: T) -> [T] where T: Hashable {
26 | var path = [dependency]
27 | if aliases.isEmpty { return path }
28 | var dependency = dependency
29 | while let alias = aliases[dependency] as? T {
30 | guard !path.contains(alias) else {
31 | breakpoint(
32 | """
33 | ---
34 | Warning: Cyclic dependency aliases for \(String(describing: T.self))
35 |
36 | A cycle was detected in the graph of dependency aliases. As a consequence, the depedency
37 | providing the default value is ambiguous.
38 |
39 | Please review your dependency aliases to make aliases for \(String(describing: T.self))
40 | form a directed graph.
41 | """)
42 | break
43 | }
44 | path.append(alias)
45 | dependency = alias
46 | }
47 | return path
48 | }
49 |
50 | public func aliasing(with dependency: T) -> Set where T: Hashable {
51 | if aliases.isEmpty { return [dependency] }
52 | let canonical = self.standardAlias(for: dependency)
53 | return Set(
54 | aliases
55 | .filter { $0.key is T }
56 | .map { path(for: $0.key as! T) }
57 | .filter { $0.contains(canonical) || $0.contains(dependency) }
58 | .flatMap { $0 }
59 | )
60 | }
61 | }
62 |
63 | /// Extracted from "swift-composable-architecture",
64 | /// https://github.com/pointfreeco/swift-composable-architecture
65 | /// Raises a debug breakpoint iff a debugger is attached.
66 | @inline(__always) func breakpoint(_ message: @autoclosure () -> String = "") {
67 | #if DEBUG
68 | // https://github.com/bitstadium/HockeySDK-iOS/blob/c6e8d1e940299bec0c0585b1f7b86baf3b17fc82/Classes/BITHockeyHelper.m#L346-L370
69 | var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
70 | var info = kinfo_proc()
71 | var info_size = MemoryLayout.size
72 |
73 | let isDebuggerAttached = name.withUnsafeMutableBytes {
74 | $0.bindMemory(to: Int32.self).baseAddress
75 | .map {
76 | sysctl($0, 4, &info, &info_size, nil, 0) != -1 && info.kp_proc.p_flag & P_TRACED != 0
77 | }
78 | ?? false
79 | }
80 |
81 | if isDebuggerAttached {
82 | fputs(
83 | """
84 | \(message())
85 |
86 | Caught debug breakpoint. Type "continue" ("c") to resume execution.
87 |
88 | """,
89 | stderr
90 | )
91 | raise(SIGTRAP)
92 | }
93 | #endif
94 | }
95 |
--------------------------------------------------------------------------------
/Tests/ComposableEnvironmentTests/ComposableEnvironmentTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import ComposableEnvironment
4 |
5 | private struct IntKey: DependencyKey {
6 | static var defaultValue: Int { 1 }
7 | }
8 |
9 | private struct Int1Key: DependencyKey {
10 | static var defaultValue: Int { -1 }
11 | }
12 |
13 | private struct Int2Key: DependencyKey {
14 | static var defaultValue: Int { -10 }
15 | }
16 |
17 | extension Dependencies {
18 | fileprivate var int: Int {
19 | get { self[IntKey.self] }
20 | set { self[IntKey.self] = newValue }
21 | }
22 |
23 | fileprivate var int1: Int {
24 | get { self[Int1Key.self] }
25 | set { self[Int1Key.self] = newValue }
26 | }
27 |
28 | fileprivate var int2: Int {
29 | get { self[Int2Key.self] }
30 | set { self[Int2Key.self] = newValue }
31 | }
32 | }
33 |
34 | final class ComposableEnvironmentTests: XCTestCase {
35 | override func setUp() {
36 | super.setUp()
37 | Dependencies.clearAliases()
38 | }
39 |
40 | func testDependency() {
41 | class Env: ComposableEnvironment {
42 | @Dependency(\.int) var int
43 | }
44 | let env = Env()
45 | XCTAssertEqual(env.int, 1)
46 | }
47 |
48 | func testDependencyImplicitAccess() {
49 | class Env: ComposableEnvironment {}
50 | let env = Env()
51 | XCTAssertEqual(env[\.int], 1)
52 | }
53 |
54 | func testDependencyPropagation() {
55 | class Parent: ComposableEnvironment {
56 | @Dependency(\.int) var int
57 | @DerivedEnvironment var child
58 | }
59 | class Child: ComposableEnvironment {
60 | @Dependency(\.int) var int
61 | }
62 | let parent = Parent()
63 | XCTAssertEqual(parent.child.int, 1)
64 |
65 | let parentWith2 = Parent().with(\.int, 2)
66 | XCTAssertEqual(parentWith2.int, 2)
67 | XCTAssertEqual(parentWith2.child.int, 2)
68 | }
69 |
70 | func testDependencyOverride() {
71 | class Parent: ComposableEnvironment {
72 | @Dependency(\.int) var int
73 | @DerivedEnvironment var child
74 | @DerivedEnvironment var sibling = Child().with(\.int, 3)
75 | }
76 | class Child: ComposableEnvironment {
77 | @Dependency(\.int) var int
78 | }
79 |
80 | let parent = Parent().with(\.int, 2)
81 | XCTAssertEqual(parent.int, 2)
82 | XCTAssertEqual(parent.child.int, 2)
83 | XCTAssertEqual(parent.sibling.int, 3)
84 | }
85 |
86 | func testDerivedWithProperties() {
87 | class Parent: ComposableEnvironment {
88 | @Dependency(\.int) var int
89 | @DerivedEnvironment var child
90 | @DerivedEnvironment var sibling = Child(otherInt: 5).with(\.int, 3)
91 | }
92 | final class Child: ComposableEnvironment {
93 | @Dependency(\.int) var int
94 | var otherInt: Int = 4
95 | required init() {}
96 | init(otherInt: Int) {
97 | self.otherInt = otherInt
98 | }
99 | }
100 |
101 | let parent = Parent().with(\.int, 2)
102 | XCTAssertEqual(parent.int, 2)
103 | XCTAssertEqual(parent.child.int, 2)
104 | XCTAssertEqual(parent.sibling.int, 3)
105 |
106 | XCTAssertEqual(parent.child.otherInt, 4)
107 | XCTAssertEqual(parent.sibling.otherInt, 5)
108 | }
109 |
110 | func testLongChainsPropagation() {
111 | class Parent: ComposableEnvironment {
112 | @Dependency(\.int) var int
113 | @DerivedEnvironment var c1
114 | }
115 | final class C1: ComposableEnvironment {
116 | @DerivedEnvironment var c2
117 | }
118 | final class C2: ComposableEnvironment {
119 | @DerivedEnvironment var c3
120 | }
121 | final class C3: ComposableEnvironment {
122 | @DerivedEnvironment var c4
123 | @Dependency(\.int) var int
124 | }
125 | final class C4: ComposableEnvironment {
126 | @DerivedEnvironment var c5
127 | }
128 | final class C5: ComposableEnvironment {
129 | @Dependency(\.int) var int
130 | }
131 | let parent = Parent().with(\.int, 4)
132 | XCTAssertEqual(parent.c1.c2.c3.c4.c5.int, 4)
133 | XCTAssertEqual(parent.c1.c2.c3.int, 4)
134 | }
135 |
136 | func testModifyingDependenciesOncePrimed() {
137 | class Parent: ComposableEnvironment {
138 | @Dependency(\.int) var int
139 | @DerivedEnvironment var c1
140 | }
141 | final class C1: ComposableEnvironment {
142 | @DerivedEnvironment var c2
143 | }
144 | final class C2: ComposableEnvironment {
145 | @DerivedEnvironment var c3
146 | }
147 | final class C3: ComposableEnvironment {
148 | @DerivedEnvironment var c4
149 | @Dependency(\.int) var int
150 | }
151 | final class C4: ComposableEnvironment {
152 | @DerivedEnvironment var c5
153 | }
154 | final class C5: ComposableEnvironment {
155 | @Dependency(\.int) var int
156 | }
157 | let parent = Parent().with(\.int, 4)
158 | XCTAssertEqual(parent.c1.c2.c3.int, 4)
159 | XCTAssertEqual(parent.c1.c2.c3.c4.c5.int, 4)
160 | // At this stage, the chain is completely primed.
161 |
162 | //Update parent with 7
163 | parent[\.int] = 7
164 | XCTAssertEqual(parent.c1.c2.c3.c4.c5.int, 7)
165 |
166 | //Update c3 with 8
167 | parent.c1.c2.c3[\.int] = 8
168 | XCTAssertEqual(parent.c1.c2.c3.c4.c5.int, 8)
169 |
170 | //Update parent again with 9
171 | parent[\.int] = 9
172 | // c5 should keep c3's value
173 | XCTAssertEqual(parent.c1.c2.c3.int, 8)
174 | XCTAssertEqual(parent.c1.c2.c3.c4.c5.int, 8)
175 | }
176 |
177 | func testDependencyAliasing() {
178 | class Parent: ComposableEnvironment {
179 | @Dependency(\.int) var int
180 | }
181 | let parent = Parent()
182 | .aliasing(\.int1, to: \.int)
183 | .aliasing(\.int2, to: \.int1)
184 | XCTAssertEqual(parent.int, 1)
185 | XCTAssertEqual(parent.with(\.int2, 4).int, 4)
186 | XCTAssertEqual(parent.int1, 4)
187 | }
188 |
189 | func testDependencyAliasingViaPropertyWrapper() {
190 | class Parent: ComposableEnvironment {
191 | @Dependency(\.int) var int
192 | @DerivedEnvironment(aliases: { $0.alias(\.int1, to: \.int) }) var c1
193 | }
194 | final class Child: ComposableEnvironment {
195 | @Dependency(\.int1) var otherInt
196 | }
197 | let parent = Parent()
198 | XCTAssertEqual(parent.c1.int1, 1)
199 | XCTAssertEqual(parent.with(\.int, 4).c1.int1, 4)
200 | }
201 |
202 | func testRecursiveEnvironment() {
203 | class FirstEnvironment: ComposableEnvironment {
204 | @DerivedEnvironment
205 | var second
206 |
207 | @Dependency(\.int1)
208 | var int1
209 | }
210 |
211 | class SecondEnvironment: ComposableEnvironment {
212 | @DerivedEnvironment
213 | var first
214 |
215 | @Dependency(\.int2)
216 | var int2
217 | }
218 |
219 | let first = FirstEnvironment()
220 | XCTAssertEqual(first.int1, -1)
221 | XCTAssertEqual(first.second.first.int1, -1)
222 |
223 | XCTAssertEqual(first.second.int2, -10)
224 | XCTAssertEqual(first.second.first.second.int2, -10)
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/Tests/ComposableEnvironmentTests/ReducerComposableEnvironmentTests.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import ComposableDependencies
3 | import XCTest
4 |
5 | @testable import ComposableEnvironment
6 |
7 | private struct IntKey: DependencyKey {
8 | static var defaultValue: Int { 1 }
9 | }
10 |
11 | extension Dependencies {
12 | fileprivate var int: Int {
13 | get { self[IntKey.self] }
14 | set { self[IntKey.self] = newValue }
15 | }
16 | }
17 |
18 | final class ReducerAdditionsTests: XCTestCase {
19 | func testPullbackWithKeyPath() {
20 | enum Action {
21 | case action
22 | }
23 |
24 | class First: ComposableEnvironment {}
25 | class Second: ComposableEnvironment {}
26 | class Third: ComposableEnvironment {}
27 |
28 | let thirdReducer = Reducer {
29 | state, _, environment in
30 | state = environment.int
31 | return .none
32 | }
33 |
34 | let secondReducer = Reducer.combine(
35 | thirdReducer.pullback(state: \.self, action: /.self)
36 | )
37 |
38 | let firstReducer = Reducer.combine(
39 | secondReducer.pullback(state: \.self, action: /.self)
40 | )
41 |
42 | let store = TestStore(
43 | initialState: 0,
44 | reducer: firstReducer,
45 | environment: First() // Swift ≥ 5.4 can use .init()
46 | .with(\.int, 2)
47 | )
48 |
49 | store.send(.action) { $0 = 2 }
50 | }
51 |
52 | func testPullbackWithCasePath() {
53 | enum State: Equatable {
54 | case int(Int)
55 | }
56 | enum Action {
57 | case action
58 | }
59 |
60 | class First: ComposableEnvironment {}
61 | class Second: ComposableEnvironment {}
62 | class Third: ComposableEnvironment {}
63 |
64 | let thirdReducer = Reducer {
65 | state, _, environment in
66 | state = .int(environment.int)
67 | return .none
68 | }
69 |
70 | let secondReducer = Reducer.combine(
71 | thirdReducer.pullback(state: /.self, action: /.self)
72 | )
73 |
74 | let firstReducer = Reducer.combine(
75 | secondReducer.pullback(state: /.self, action: /.self)
76 | )
77 |
78 | let store = TestStore(
79 | initialState: .int(0),
80 | reducer: firstReducer,
81 | environment: First() // Swift ≥ 5.4 can use .init()
82 | .with(\.int, 2)
83 | )
84 |
85 | store.send(.action) { $0 = .int(2) }
86 | }
87 |
88 | func testForEachIdentifiedArray() {
89 | enum Action {
90 | case action
91 | }
92 |
93 | class First: ComposableEnvironment {}
94 | class Second: ComposableEnvironment {}
95 | class Third: ComposableEnvironment {}
96 |
97 | struct Value: Identifiable, Equatable {
98 | var id: String
99 | var int: Int
100 | }
101 |
102 | let thirdReducer = Reducer, Action, Third> {
103 | state, _, environment in
104 | for index in state.indices {
105 | state.update(.init(id: state[index].id, int: environment.int), at: index)
106 | }
107 | return .none
108 | }
109 |
110 | let secondReducer = Reducer, Action, Second>.combine(
111 | thirdReducer.pullback(state: \.self, action: /.self)
112 | )
113 |
114 | let firstReducer = Reducer, Action, First>.combine(
115 | secondReducer.pullback(state: \.self, action: /.self)
116 | )
117 |
118 | let store = TestStore(
119 | initialState: .init(uniqueElements: [
120 | .init(id: "A", int: 0),
121 | .init(id: "B", int: 3),
122 | ]),
123 | reducer: firstReducer,
124 | environment: First() // Swift ≥ 5.4 can use .init()
125 | .with(\.int, 2)
126 | )
127 |
128 | store.send(.action) {
129 | $0 = .init(uniqueElements: [
130 | .init(id: "A", int: 2),
131 | .init(id: "B", int: 2),
132 | ])
133 | }
134 | }
135 |
136 | func testForEachDictionary() {
137 | enum Action {
138 | case action
139 | }
140 | class First: ComposableEnvironment {}
141 | class Second: ComposableEnvironment {}
142 | class Third: ComposableEnvironment {}
143 |
144 | let thirdReducer = Reducer<[String: Int], Action, Third> {
145 | state, _, environment in
146 | for key in state.keys {
147 | state[key] = environment.int
148 | }
149 | return .none
150 | }
151 |
152 | let secondReducer = Reducer<[String: Int], Action, Second>.combine(
153 | thirdReducer.pullback(state: \.self, action: /.self)
154 | )
155 |
156 | let firstReducer = Reducer<[String: Int], Action, First>.combine(
157 | secondReducer.pullback(state: \.self, action: /.self)
158 | )
159 |
160 | let store = TestStore(
161 | initialState: [
162 | "A": 0,
163 | "B": 3,
164 | ],
165 | reducer: firstReducer,
166 | environment: First() // Swift ≥ 5.4 can use .init()
167 | .with(\.int, 2)
168 | )
169 |
170 | store.send(.action) {
171 | $0 = [
172 | "A": 2,
173 | "B": 2,
174 | ]
175 | }
176 | }
177 |
178 | func testComposableAutoComposableComposableBridging() {
179 | class Third: ComposableEnvironment {
180 | @Dependency(\.int) var integer
181 | }
182 | class Second: ComposableEnvironment {
183 | @DerivedEnvironment var third
184 | }
185 | class First: ComposableEnvironment {}
186 |
187 | enum Action {
188 | case action
189 | }
190 |
191 | let thirdReducer = Reducer {
192 | state, _, environment in
193 | state = environment.integer
194 | return .none
195 | }
196 |
197 | let secondReducer = Reducer.combine(
198 | thirdReducer.pullback(state: \.self, action: /.self, environment: \.third)
199 | )
200 |
201 | let firstReducer = Reducer.combine(
202 | secondReducer.pullback(state: \.self, action: /.self)
203 | )
204 |
205 | let store = TestStore(
206 | initialState: 0,
207 | reducer: firstReducer,
208 | environment: First() // Swift 5.4+ can use .init()
209 | .with(\.int, 2)
210 | )
211 |
212 | store.send(.action) { $0 = 2 }
213 | }
214 |
215 | func testAutoComposableComposableAutoComposableBridging() {
216 | class Third: ComposableEnvironment {}
217 | class Second: ComposableEnvironment {}
218 | class First: ComposableEnvironment {
219 | @DerivedEnvironment var second
220 | }
221 |
222 | enum Action {
223 | case action
224 | }
225 |
226 | let thirdReducer = Reducer {
227 | state, _, environment in
228 | state = environment.int
229 | return .none
230 | }
231 |
232 | let secondReducer = Reducer.combine(
233 | thirdReducer.pullback(state: \.self, action: /.self)
234 | )
235 |
236 | let firstReducer = Reducer.combine(
237 | secondReducer.pullback(state: \.self, action: /.self, environment: \.second)
238 | )
239 |
240 | let store = TestStore(
241 | initialState: 0,
242 | reducer: firstReducer,
243 | environment: First() // Swift ≥ 5.4 can use .init()
244 | .with(\.int, 2)
245 | )
246 |
247 | store.send(.action) { $0 = 2 }
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/Tests/DependencyAliasesTests/DependencyAliasesTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import _DependencyAliases
4 |
5 | private struct Dependencies {
6 | var int: Int
7 | var int1: Int
8 | var int2: Int
9 | }
10 |
11 | final class DependencyAliasesTests: XCTestCase {
12 | func testStandardAlias1() {
13 | var dep = DependencyAliases()
14 | dep.alias(dependency: \Dependencies.int1, to: \Dependencies.int)
15 | dep.alias(dependency: \Dependencies.int2, to: \Dependencies.int1)
16 | XCTAssertEqual(dep.standardAlias(for: \Dependencies.int), \.int)
17 | XCTAssertEqual(dep.standardAlias(for: \Dependencies.int1), \.int)
18 | XCTAssertEqual(dep.standardAlias(for: \Dependencies.int2), \.int)
19 | }
20 | func testStandardAlias2() {
21 | var dep = DependencyAliases()
22 | dep.alias(dependency: \Dependencies.int, to: \Dependencies.int1)
23 | dep.alias(dependency: \Dependencies.int1, to: \Dependencies.int2)
24 | XCTAssertEqual(dep.standardAlias(for: \Dependencies.int), \.int2)
25 | XCTAssertEqual(dep.standardAlias(for: \Dependencies.int1), \.int2)
26 | XCTAssertEqual(dep.standardAlias(for: \Dependencies.int2), \.int2)
27 | }
28 |
29 | func testAliasesForDependency() {
30 | var dep = DependencyAliases()
31 | dep.alias(dependency: \Dependencies.int1, to: \Dependencies.int)
32 | dep.alias(dependency: \Dependencies.int2, to: \Dependencies.int1)
33 |
34 | XCTAssertEqual(dep.aliasing(with: \Dependencies.int), [\.int, \.int1, \.int2])
35 | XCTAssertEqual(dep.aliasing(with: \Dependencies.int1), [\.int, \.int1, \.int2])
36 | XCTAssertEqual(dep.aliasing(with: \Dependencies.int2), [\.int, \.int1, \.int2])
37 | }
38 |
39 | // func testCyclicDependencyRaiseBreakpoint() {
40 | // var dep = DependencyAliases()
41 | // dep.alias(dependency: \Dependencies.int, to: \Dependencies.int1)
42 | // dep.alias(dependency: \Dependencies.int1, to: \Dependencies.int)
43 | // _ = dep.standardAlias(for: \Dependencies.int)
44 | // }
45 | }
46 |
--------------------------------------------------------------------------------
/Tests/GlobalEnvironmentTests/GlobalEnvironmentTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import _Dependencies
3 |
4 | @testable import GlobalEnvironment
5 |
6 | private struct IntKey: DependencyKey {
7 | static var defaultValue: Int { 1 }
8 | }
9 |
10 | private struct Int1Key: DependencyKey {
11 | static var defaultValue: Int { -1 }
12 | }
13 |
14 | private struct Int2Key: DependencyKey {
15 | static var defaultValue: Int { -10 }
16 | }
17 |
18 | extension Dependencies {
19 | fileprivate var int: Int {
20 | get { self[IntKey.self] }
21 | set { self[IntKey.self] = newValue }
22 | }
23 |
24 | fileprivate var int1: Int {
25 | get { self[Int1Key.self] }
26 | set { self[Int1Key.self] = newValue }
27 | }
28 |
29 | fileprivate var int2: Int {
30 | get { self[Int2Key.self] }
31 | set { self[Int2Key.self] = newValue }
32 | }
33 | }
34 |
35 | final class GlobalEnvironmentTests: XCTestCase {
36 | override func setUp() {
37 | super.setUp()
38 | Dependencies.reset()
39 | Dependencies.clearAliases()
40 | }
41 |
42 | func testDependency() {
43 | struct Env: GlobalEnvironment {
44 | @Dependency(\.int) var int
45 | }
46 | let env = Env()
47 | XCTAssertEqual(env.int, 1)
48 | }
49 |
50 | func testDependencyImplicitAccess() {
51 | struct Env: GlobalDependenciesAccessing {}
52 | let env = Env()
53 | XCTAssertEqual(env[\.int], 1)
54 | }
55 |
56 | func testDependenciesOverride() {
57 | struct Env: GlobalEnvironment {
58 | @Dependency(\.int) var int
59 | }
60 | struct Env2: GlobalEnvironment {
61 | @Dependency(\.int) var int
62 | }
63 | let env = Env().with(\.int, 2)
64 | XCTAssertEqual(env.int, 2)
65 | XCTAssertEqual(Env2().int, 2)
66 | }
67 |
68 | func testDependencyAliasing() {
69 | struct Parent: GlobalEnvironment {
70 | @Dependency(\.int) var int
71 | }
72 | let parent = Parent()
73 | .aliasing(\.int1, to: \.int)
74 | .aliasing(\.int2, to: \.int1)
75 | XCTAssertEqual(parent[\.int1], 1)
76 | XCTAssertEqual(parent.with(\.int1, 4).int, 4)
77 | }
78 |
79 | func testDependencyAliasingViaPropertyWrapper() {
80 | struct Parent: GlobalEnvironment {
81 | @Dependency(\.int) var int
82 | @DerivedEnvironment(aliases: { $0.alias(\.int1, to: \.int) }) var c1
83 | }
84 | struct Child: GlobalEnvironment {
85 | @Dependency(\.int1) var otherInt
86 | }
87 | let parent = Parent()
88 | XCTAssertEqual(parent.c1.otherInt, 1)
89 | XCTAssertEqual(parent.with(\.int, 4).c1.otherInt, 4)
90 | }
91 |
92 | func testRecursiveEnvironment() {
93 | struct FirstEnvironment: GlobalEnvironment {
94 | @DerivedEnvironment
95 | var second
96 |
97 | @Dependency(\.int1)
98 | var int1
99 | }
100 |
101 | struct SecondEnvironment: GlobalEnvironment {
102 | @DerivedEnvironment
103 | var first
104 |
105 | @Dependency(\.int2)
106 | var int2
107 | }
108 |
109 | let first = FirstEnvironment()
110 | XCTAssertEqual(first.int1, -1)
111 | XCTAssertEqual(first.second.first.int1, -1)
112 |
113 | XCTAssertEqual(first.second.int2, -10)
114 | XCTAssertEqual(first.second.first.second.int2, -10)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Tests/GlobalEnvironmentTests/ReducerGlobalEnvironmentTests.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import ComposableDependencies
3 | import XCTest
4 |
5 | @testable import GlobalEnvironment
6 |
7 | private struct IntKey: DependencyKey {
8 | static var defaultValue: Int { 1 }
9 | }
10 |
11 | extension Dependencies {
12 | fileprivate var int: Int {
13 | get { self[IntKey.self] }
14 | set { self[IntKey.self] = newValue }
15 | }
16 | }
17 |
18 | final class ReducerAdditionsTests: XCTestCase {
19 | func testPullbackWithKeyPath() {
20 | enum Action {
21 | case action
22 | }
23 |
24 | struct First: GlobalEnvironment {}
25 | struct Second: GlobalEnvironment {}
26 | struct Third: GlobalEnvironment {}
27 |
28 | let thirdReducer = Reducer {
29 | state, _, environment in
30 | state = environment[\.int]
31 | return .none
32 | }
33 |
34 | let secondReducer = Reducer.combine(
35 | thirdReducer.pullback(state: \.self, action: /.self)
36 | )
37 |
38 | let firstReducer = Reducer.combine(
39 | secondReducer.pullback(state: \.self, action: /.self)
40 | )
41 |
42 | let store = TestStore(
43 | initialState: 0,
44 | reducer: firstReducer,
45 | environment: First() // Swift ≥ 5.4 can use .init()
46 | .with(\.int, 2)
47 | )
48 |
49 | store.send(.action) { $0 = 2 }
50 | }
51 |
52 | func testPullbackWithCasePath() {
53 | enum State: Equatable {
54 | case int(Int)
55 | }
56 | enum Action {
57 | case action
58 | }
59 |
60 | struct First: GlobalEnvironment {}
61 | struct Second: GlobalEnvironment {}
62 | struct Third: GlobalEnvironment {}
63 |
64 | let thirdReducer = Reducer {
65 | state, _, environment in
66 | state = .int(environment[\.int])
67 | return .none
68 | }
69 |
70 | let secondReducer = Reducer.combine(
71 | thirdReducer.pullback(state: /.self, action: /.self)
72 | )
73 |
74 | let firstReducer = Reducer.combine(
75 | secondReducer.pullback(state: /.self, action: /.self)
76 | )
77 |
78 | let store = TestStore(
79 | initialState: .int(0),
80 | reducer: firstReducer,
81 | environment: First() // Swift ≥ 5.4 can use .init()
82 | .with(\.int, 2)
83 | )
84 |
85 | store.send(.action) { $0 = .int(2) }
86 | }
87 |
88 | func testForEachIdentifiedArray() {
89 | enum Action {
90 | case action
91 | }
92 |
93 | struct First: GlobalEnvironment {}
94 | struct Second: GlobalEnvironment {}
95 | struct Third: GlobalEnvironment {}
96 |
97 | struct Value: Identifiable, Equatable {
98 | var id: String
99 | var int: Int
100 | }
101 |
102 | let thirdReducer = Reducer, Action, Third> {
103 | state, _, environment in
104 | for index in state.indices {
105 | state.update(.init(id: state[index].id, int: environment[\.int]), at: index)
106 | }
107 | return .none
108 | }
109 |
110 | let secondReducer = Reducer, Action, Second>.combine(
111 | thirdReducer.pullback(state: \.self, action: /.self)
112 | )
113 |
114 | let firstReducer = Reducer, Action, First>.combine(
115 | secondReducer.pullback(state: \.self, action: /.self)
116 | )
117 |
118 | let store = TestStore(
119 | initialState: .init(uniqueElements: [
120 | .init(id: "A", int: 0),
121 | .init(id: "B", int: 3),
122 | ]),
123 | reducer: firstReducer,
124 | environment: First() // Swift ≥ 5.4 can use .init()
125 | .with(\.int, 2)
126 | )
127 |
128 | store.send(.action) {
129 | $0 = .init(uniqueElements: [
130 | .init(id: "A", int: 2),
131 | .init(id: "B", int: 2),
132 | ])
133 | }
134 | }
135 |
136 | func testForEachDictionary() {
137 | enum Action {
138 | case action
139 | }
140 | struct First: GlobalEnvironment {}
141 | struct Second: GlobalEnvironment {}
142 | struct Third: GlobalEnvironment {}
143 |
144 | let thirdReducer = Reducer<[String: Int], Action, Third> {
145 | state, _, environment in
146 | for key in state.keys {
147 | state[key] = environment[\.int]
148 | }
149 | return .none
150 | }
151 |
152 | let secondReducer = Reducer<[String: Int], Action, Second>.combine(
153 | thirdReducer.pullback(state: \.self, action: /.self)
154 | )
155 |
156 | let firstReducer = Reducer<[String: Int], Action, First>.combine(
157 | secondReducer.pullback(state: \.self, action: /.self)
158 | )
159 |
160 | let store = TestStore(
161 | initialState: [
162 | "A": 0,
163 | "B": 3,
164 | ],
165 | reducer: firstReducer,
166 | environment: First() // Swift ≥ 5.4 can use .init()
167 | .with(\.int, 2)
168 | )
169 |
170 | store.send(.action) {
171 | $0 = [
172 | "A": 2,
173 | "B": 2,
174 | ]
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------