Swiftpack.co -  tgrapperon/swift-composable-environment as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
tgrapperon/swift-composable-environment
A library to derive and compose Environment's in The Composable Architecture.
.package(url: "https://github.com/tgrapperon/swift-composable-environment.git", from: "0.2.0")

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.

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

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 the Environment'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 their KeyPath 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 a ComposableEnvironment, nor derived from another via @DerivedEnvironment, automatic syncing of dependencies will stop to work downstream, as the next ComposableEnvironment 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 like mainQueue or Date, in a separate module that can be shared by each feature.
  • With the current implementation, dependencies in a ComposableEnvironment can't be updated once any of its DerivedEnvironment has been accessed.

GitHub

link
Stars: 41
Last commit: 1 week ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Release Notes

1 week ago

@Inlinable and @usableFromInline decoration were removed to avoid polluting the ABI of the library. The performance gain was hypothetic, and they may be reinstated someday if their positive influence is clearly assessed. Premature optimization is the root of all evil.

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API