A library to derive and compose Environment's in The Composable Architecture
ComposableEnvironment
This library brings an API similar to SwiftUI's Environment
to derive and compose Environment
's in The Composable Architecture.
Example
Each dependency we want to share using ComposableEnvironment
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:
struct MainQueueKey: DependencyKey {
static var defaultValue: AnySchedulerOf<DispatchQueue> { .main }
}
We also install it in ComposableDependencies
:
extension ComposableDependencies {
var mainQueue: AnySchedulerOf<DispatchQueue> {
get { self[MainQueueKey.self] }
set { self[MainQueueKey.self] = newValue }
}
}
Now, let define RootEnvironment
:
class RootEnvironment: ComposableEnvironment {
@Dependency(\.mainQueue) var mainQueue
}
Please note that we didn't have to set an initial value to mainQueue
. @Dependency
are immutable, but we can easily attribute new values with a chaining API:
let failingMain = Root().with(\.mainQueue, .failing)
An now, the prestige! Let ChildEnvironment
be
class ChildEnvironment: ComposableEnvironment {
@Dependency(\.mainQueue) var mainQueue
}
If RootEnvironment
is modified like
class RootEnvironment: ComposableEnvironment {
@Dependency(\.mainQueue) var mainQueue
@DerivedEnvironment<ChildEnvironment> var child
}
child.mainQueue
will be synchronized with RootEnvironment
's value. In other words,
Root().with(\.mainQueue, .failing).child.mainQueue == .failing
We only have to declare ChildEnvironment
as a property of RootEnvironment
, with the @DerivedEnvironment
property wrapper. Like with SwiftUI's View
, if one modifies a dependency with with(keypath, value)
, only the environment's instance and its derived environments will receive the new dependency. Its eventual parent and siblings will be unaffected.
AutoComposableEnvironment
Since v0.4
, you can optionally forgo @Dependency
and @DerivedEnvironment
declarations:
-
You can directly access dependencies using their property name defined in
ComposableDepencies
directly in yourComposableEnvironment
subclass, as if you defined@Dependency(\.someDependency) var someDependency
. -
You can use environment-less pullbacks. They will vend your derived feature's reducer a derived environment of the expected type. This is equivalent to defining
@DerivedEnvironment<ChildEnvironment> var child
in your parent's environment, and using[…], environment:\.child)
when pulling-back.
You still need @Dependency
if you want to customize the exposed name of your dependency in your environment, like
@Dependency(\.someDependency) var anotherNameForTheDependency
You still need @DerivedEnvironment
if you want to override the dependencies inside the environment's chain:
@DerivedEnvironment var child = ChildEnvironment().with(\.someDependency, someValue)
The example app shows how this feature can be used and mixed with the property-wrapper approach.
Correspondance 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 correspondances:
SwiftUI | ComposableEnvironment | Usage |
---|---|---|
EnvironmentKey |
DependencyKey |
Identify a shared value |
EnvironmentValues |
ComposableDependencies |
Expose a shared value |
@Environment |
@Dependency |
Retrieve a shared value |
View |
ComposableEnvironment |
A node |
View.body |
@DerivedEnvironment 's |
A list of children of the node |
View.environment(keyPath:value:) |
ComposableEnvironment.with(keyPath:value:) |
Set a shared value for a node and its children |
Documentation
The latest documentation for ComposableEnvironment's APIs is available here.
Advantages over manual management
- You don't have to instantiate your child environments, nor to manage their initializers.
- You don't have to host a dependency in some environment for the sole purpose of passing it to child environments. You can define a dependency in the
Root
environment and retrieve it in any descendant (if none of its ancester has overidden the root's value in the meantime). You don't have to declare this dependency in theEnvironment
's which are not using it explicitly. - Your dependencies are clearly tagged. It's more difficult to mix up dependencies with the same interface.
ComposableEnvironment
's instances are cached, and you can access them direcly by theirKeyPath
in their parent when pulling-back your reducers.- You can quickly override the dependencies of any environment with a chaining API. You can easily create specific configurations for your tests or
SwiftUI
previews. - You write much less code, and you get more autocompletion.
- You can fall back to manual management with
ComposableEnvironment
if necessary, and store properties that are not@Dependency
's.
Inconvenients compared to manual management
- Your environments need to be subclasses of
ComposableEnvironment
. - Your environments must be connected through
@DerivedEnvironment
. If one of the members of the environment tree is not aComposableEnvironment
, nor derived from another via@DerivedEnvironment
, automatic syncing of dependencies will stop to work downstream, as the nextComposableEnvironment
will act as a root for its subtree (I guess some safeguards are possible). - You need to declare your dependencies explicitly in the
ComposedDependencies
pseudo-namespace. It may require to plan ahead if you're working with an highly modularized application. I guess it should be possible to define equivalence relations between dependencies at some point. Otherwise, I would recommend to define transversal dependencies likemainQueue
orDate
, in a separate module that can be shared by each feature.
Installation
Add
.package(url: "https://github.com/tgrapperon/swift-composable-environment", from: "0.0.4")
to your Package dependencies in Package.swift
, and then
.product(name: "ComposableEnvironment", package: "swift-composable-environment")
to your target's dependencies.