This library brings an API similar to SwiftUI’s Environment to derive and compose Environment‘s in The Composable Architecture (TCA).
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 to have both systems working at the same time while you transition out from this library.
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.
This library comes with two mutually exclusive modules, ComposableEnvironment and GlobalEnvironment, which are providing different functionalities for different tradeoffs.
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.
GlobalEnvironment allows defining global dependencies that are the same for all reducers in the chain. This is the most frequent configuration.
Both modules are defined in the same repository to maintain source compatibility between them.
The GlobalEnvironment module should fit most of the cases.
Defining dependencies
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:
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.
Like we would do with SwiftUI’s EnvironmentValues, we also install it in Dependencies:
extension Dependencies {
var mainQueue: AnySchedulerOf<DispatchQueue> {
get { self[MainQueueKey.self] }
set { self[MainQueueKey.self] = newValue }
}
}
Using dependencies
Whereas you’re using ComposableEnvironment or GlobalEnvironment, there are distinct ways to access your dependencies.
@Dependency property wrapper
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
@Dependency(\.mainQueue) var main
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.
Implicit subscript
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:
environment[\.mainQueue]
returns the same value as @Dependency(\.mainQueue).
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.
Direct access (ComposableEnvironment only)
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:
environment.mainQueue
This direct access is unfortunately not possible when using GlobalEnvironment.
Environments
The way you define environments differs, whereas you’re using ComposableEnvironment or GlobalEnvironment.
Defining Environments while using ComposableEnvironment
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:
public class ParentEnvironment: ComposableEnvironment {
@Dependency(\.mainQueue) var main
}
Imagine that you need to embed a Child TCA feature into the Parent feature. You declare the embedding using the @DerivedEnvironment property wrapper:
public class ParentEnvironment: ComposableEnvironment {
@Dependency(\.mainQueue) var main
@DerivedEnvironment<ChildEnvironment> var child
}
When you access the child property of ParentEnvironment, it automatically inherit the dependencies from ParentEnvironment. You can pullback childReducer using the standard methods:
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.
In this last case, you can even embed the child reducer using environment-less pullbacks:
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 \.childKeyPath as a function (ParentEnvironment) -> ChildEnvironment.
Defining Environments while using GlobalEnvironment
When using GlobalEnvironment, your environment, whereas it’s a value or a reference type, should conform to the GlobalEnvironment protocol.
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).
public struct ParentEnvironment: GlobalEnvironment {
public init() {}
@Dependency(\.mainQueue) var main
}
You still have access to environment-less pullbacks, with the same API:
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).
If you can’t conform to GlobalEnvironment, you only lose access to the environment-less pullbacks.
Assigning values to dependencies
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.
To set a value to a dependency, you use the with(keyPath,value) chainable method from your environment:
When you’re using GlobalEnvironment, each dependency is set globally. If you set the same dependency twice, the last call prevails.
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.
Aliasing dependencies
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
environment.aliasing(\.main, to: \.mainQueue)
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.
You can also alias dependencies “on the spot”, using the @DerivedEnvironment property wrapper. Its initializer provides a closure transforming a provided AliasBuilder.
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:
public class ParentEnvironment: ComposableEnvironment {
@Dependency(\.mainQueue) var mainQueue
@DerivedEnvironment<ChildEnvironment>(aliases: {
$0.alias(\.main, to: \.mainQueue)
}) var child
}
When using this property wrapper, you don’t need to define the alias from the environment using .aliasing().
Dependencies aliases are always global.
Environment-less pullbacks
Omitting the @DerivedEnvironment property wrapper
You can forgo @DerivedEnvironment declarations when:
You don’t need to customize dependencies specifically for the child environment when using ComposableEnvironment.
You don’t need to alias dependencies “on the spot”, using the @DerivedEnvironment property wrapper, when using either module.
The example app shows how this feature can be used and mixed with the property-wrapper approach when using ComposableEnvironment.
Using environment-less pullbacks
When your environment can be instantiated automatically, you can use environment-less pullbacks:
childReducer.pullback(state: \.child, action: /ParentAction.child)
// or, for collections of features:
childReducer.forEach(state: \.children, action: /ParentAction.children)
Please note that in order to access such pullbacks when using GlobalEnvironment, your environment needs to conform to the GlobalEnvironment protocol.
Choosing between ComposableEnvironment and GlobalEnvironment
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.
The principal differences between the two approaches are summarized in the following table:
| | ComposableEnvironment | GlobalEnvironment |
|—|—|—|
| Environment Type | Classes | Any existential (struct, classes, etc.) |
| Environment Tree | All nodes should be ComposableEnvironment subclasses | Free, can opt-in/opt-out at any point |
| Dependency values | Customizable per instance | Globally defined |
| Access to dependencies | @Dependency, direct, implicit | @Dependency, implicit |
Correspondence with SwiftUI’s Environment
In order to ease its learning curve, the library bases its API on SwiftUI’s Environment. We have the following functional correspondences:
| SwiftUI | ComposableEnvironment| Usage |
|—|—|—|
|EnvironmentKey|DependencyKey| Identify a shared value |
|EnvironmentValues|Dependencies| Expose a shared value |
|@Environment|@Dependency| Retrieve a shared value |
|View|(Composable/Global)Environment| A node |
|View.body| @DerivedEnvironment‘s | A list of children of the node |
|View .environment(keyPath:value:)|(Composable/Global)Environment .with(keyPath:value:)| Set a shared value for a node and its children |
Documentation
The latest documentation for ComposableEnvironment’s APIs is available here.
to your Package dependencies in Package.swift, and then
.product(name: "ComposableEnvironment", package: "swift-composable-environment")
// or
.product(name: "GlobalEnvironment", package: "swift-composable-environment")
to your target’s dependencies, depending on the module you want to use.
Migrating to TCA’s protocol reducers
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:
Define a typealias to both TCA’s types in a module you own (or in your application if you’re not using modules):
import ComposableArchitecture
public typealias DependencyKey = ComposableArchitecture.DependencyKey
public typealias Dependency = ComposableArchitecture.Dependency
Because these typeliases are defined in modules you own, they will be preferred to external definitions when resolving types.
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.
In this state, your project should build without ambiguities.
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!
ComposableEnvironment
This library brings an API similar to SwiftUI’s
Environment
to derive and composeEnvironment
‘s in The Composable Architecture (TCA).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 aDependencyKey
protocol, so you will need to perform a few actions to have both systems working at the same time while you transition out from this library.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 yourEnvironment
‘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.This library comes with two mutually exclusive modules,
ComposableEnvironment
andGlobalEnvironment
, which are providing different functionalities for different tradeoffs.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.GlobalEnvironment
allows defining global dependencies that are the same for all reducers in the chain. This is the most frequent configuration. Both modules are defined in the same repository to maintain source compatibility between them.The
GlobalEnvironment
module should fit most of the cases.Defining dependencies
Each dependency we want to share should be declared with a
DependencyKey
‘s in a similar fashion one declares customEnvironmentValue
‘s in SwiftUI usingEnvironmentKey
‘s. Let define amainQueue
dependency: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.Like we would do with SwiftUI’s
EnvironmentValues
, we also install it inDependencies
:Using dependencies
Whereas you’re using
ComposableEnvironment
orGlobalEnvironment
, there are distinct ways to access your dependencies.@Dependency
property wrapperYou use the
@Dependency
property wrapper to expose a dependency to your environment. This property wrapper takes as argument theKeyPath
of the property you defined inDependencies
. For example, to expose themainQueue
defined above, you declareNote 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.Implicit subscript
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 theKeyPath
from the property defined inDependencies
. For example:returns the same value as
@Dependency(\.mainQueue)
.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.
Direct access (
ComposableEnvironment
only)When using
ComposableEnvironment
, you can directly access a dependency by using its computed property name inDependencies
from anyComposableEnvironment
subclass, even if you did not expose the dependency using the@Dependency
property wrapper:This direct access is unfortunately not possible when using
GlobalEnvironment
.Environments
The way you define environments differs, whereas you’re using
ComposableEnvironment
orGlobalEnvironment
.Defining Environments while using
ComposableEnvironment
When using
ComposableEnvironment
, all your environments need to be subclasses ofComposableEnvironment
. This is unfortunately required to automatically handle the storage of the private environment values state at a given node. Let define theParentEnvironment
exposing themainQueue
dependency:Imagine that you need to embed a
Child
TCA feature into theParent
feature. You declare the embedding using the@DerivedEnvironment
property wrapper:When you access the
child
property ofParentEnvironment
, it automatically inherit the dependencies fromParentEnvironment
. You can pullbackchildReducer
using the standard methods: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. In this last case, you can even embed the child reducer using environment-less pullbacks:
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
.Defining Environments while using
GlobalEnvironment
When using
GlobalEnvironment
, your environment, whereas it’s a value or a reference type, should conform to theGlobalEnvironment
protocol. You can then define and use your dependencies in the same way as forComposableEnvironment
. 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).You still have access to environment-less pullbacks, with the same API:
The only requirement for
GlobalEnvironment
is to provide aninit()
initializer. If this is not possible for your child environment, you can still implement theGlobalDependenciesAccessing
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 theComposableEnvironment
version has (it needs to be installed in aComposableEnvironment
subclass). If you can’t conform toGlobalEnvironment
, you only lose access to the environment-less pullbacks.Assigning values to dependencies
Once dependencies are defined as computed properties of the
Dependencies
, you only access them through your environment, whereas it’s aComposableEnvironment
subclass or some type conforming toGlobalDependenciesAccessing
.To set a value to a dependency, you use the
with(keyPath,value)
chainable method from your environment:When you’re using
GlobalEnvironment
, each dependency is set globally. If you set the same dependency twice, the last call prevails. When you’re usingComposableEnvironment
, each dependency is set along the dependency tree until it eventually is set again using awith(keyPath, anotherValue)
call on a child environment. This works in the same fashion as SwiftUIEnvironment
.Aliasing dependencies
In the case the same dependency was defined by different domains using different computed properties in
Dependencies
, you can alias them using thealiasing(dependencyKeyPath, to: referenceDependencyKeyPath)
chainable method from your environment. For example, if you defined the main queue as.main
in some feature, and asmainQueue
in another, you can alias both usingOnce aliased, you can assign a value using either
KeyPath
. If no value is set for the dependency, the second argument provides its default for bothKeyPaths
.You can also alias dependencies “on the spot”, using the
@DerivedEnvironment
property wrapper. Its initializer provides a closure transforming a providedAliasBuilder
. This type has only one chainable method,alias(dependencyKeyPath, to: referenceDependencyKeyPath)
. For example, if themain
dependency is defined in thechild
derived environment, you can define an alias to themainQueue
dependency fromParentEnvironment
using:When using this property wrapper, you don’t need to define the alias from the environment using
.aliasing()
.Dependencies aliases are always global.
Environment-less pullbacks
Omitting the
@DerivedEnvironment
property wrapperYou can forgo
@DerivedEnvironment
declarations when:ComposableEnvironment
.@DerivedEnvironment
property wrapper, when using either module. The example app shows how this feature can be used and mixed with the property-wrapper approach when usingComposableEnvironment
.Using environment-less pullbacks
When your environment can be instantiated automatically, you can use environment-less pullbacks:
Please note that in order to access such pullbacks when using
GlobalEnvironment
, your environment needs to conform to theGlobalEnvironment
protocol.Choosing between
ComposableEnvironment
andGlobalEnvironment
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 useGlobalEnvironment
. As the first configuration is quite rare, we recommend usingGlobalEnvironment
if you’re in doubt, as it is the simplest to implement in an existing TCA project.The principal differences between the two approaches are summarized in the following table: | |
ComposableEnvironment
|GlobalEnvironment
| |—|—|—| | Environment Type | Classes | Any existential(struct, classes, etc.) | | Environment Tree | All nodes should be
ComposableEnvironment
subclasses | Free,can opt-in/opt-out at any point | | Dependency values | Customizable per instance | Globally defined | | Access to dependencies |
@Dependency
, direct, implicit |@Dependency
, implicit |Correspondence with SwiftUI’s Environment
In order to ease its learning curve, the library bases its API on SwiftUI’s Environment. We have the following functional correspondences: | SwiftUI | ComposableEnvironment| Usage | |—|—|—| |
EnvironmentKey
|DependencyKey
| Identify a shared value | |EnvironmentValues
|Dependencies
| Expose a shared value | |@Environment
|@Dependency
| Retrieve a shared value | |View
|(Composable/Global)Environment
| A node | |View.body
|@DerivedEnvironment
‘s | A list of children of the node | |View
.environment(keyPath:value:)
|(Composable/Global)Environment
.with(keyPath:value:)
| Set a shared value for a node and its children |Documentation
The latest documentation for ComposableEnvironment’s APIs is available here.
Installation
Add
to your Package dependencies in
Package.swift
, and thento your target’s dependencies, depending on the module you want to use.
Migrating to TCA’s protocol reducers
When importing the latest versions of TCA, there will likely be ambiguities when using the
@Dependency
property wrapper, or theDependencyKey
protocol. As this library will give way to TCA, the preferred approach is the following:DependencyKey
byCompatible.DependencyKey
and@Dependency
by@Compatible.Dependency
. You can use Xcode search/replace in all files for this purpose.In this state, your project should build without ambiguities.
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!