├── .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/badge.svg)](https://github.com/tgrapperon/swift-composable-environment/actions/workflows/swift.yml) 3 | [![Documentation](https://github.com/tgrapperon/swift-composable-environment/actions/workflows/documentation.yml/badge.svg)](https://github.com/tgrapperon/swift-composable-environment/wiki/ComposableEnvironment-Documentation) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ftgrapperon%2Fswift-composable-environment%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/tgrapperon/swift-composable-environment) 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ftgrapperon%2Fswift-composable-environment%2Fbadge%3Ftype%3Dplatforms)](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 | --------------------------------------------------------------------------------