A library to derive and compose Environment's in The Composable Architecture.

Overview

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 { .main }
}

We also install it in ComposableDependencies:

extension ComposableDependencies {
  var mainQueue: AnySchedulerOf {
    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.
Comments
  • Add EnvironmentalDependencies product

    Add EnvironmentalDependencies product

    Hi, the new GlobalDependencies target brings some ambiguity to library extensions. It's a draft PR to discuss ways to extend the package. For example, I'd like to use it in https://github.com/capturecontext/composable-architecture-extensions, and provide some default implementations and also, maybe create a wrapper for https://github.com/capturecontext/swift-standard-clients. But I'd like to avoid forcing people to use a specific target from this package. Probably it's not an issue if I had the EnvironmentalDependencies product in the case of swift-standard-clients, because clients are simple dependencies. But in case of composable-architecture-extensions I tried to create StoreSchedulers environment and it is pattern-aware. By writing this I understood that I could make StoreSchedulersClient and it should work, but I need EnvironmentalDependencies product anyway

    I've chosen this name for the product to avoid taking too generic Dependencies name. Usually I have Dependencies local package for my apps, it should not be an issue, because I have a separate target for each dependency and no dependency is named Dependency. But maybe someone uses another approach and the name is taken, so EnvironmentalDependencies is more verbose and should not cause any conflicts.

    opened by maximkrouk 6
  • Hide internal `Dependencies` methods

    Hide internal `Dependencies` methods

    As discussed in #10, this should finish concealing internal APIs. Dependencies should act as an opaque type, only presenting defined dependencies. There's one exception when using GlobalEnvironment, where a static .reset() method on Dependencies was implemented to help when testing.

    I've weighted if we should keep or not the underscore for _Dependencies and _DependencyAliases, as they are completely hidden to the end-user. I'm not fond of the look, but I think it's better to keep them like this, for now, to make the distinction clearer.

    Thanks for the idea of importing only types instead of whole modules, @maximkrouk! This is what was missing to solve this puzzle.

    opened by tgrapperon 0
  • Add migration helpers

    Add migration helpers

    This PR aims to provide contextual information when migrating from Composable to Global environments (in both ways). It also helps to get the project back into a buildable, and potentially runnable, state sooner.

    opened by tgrapperon 0
  • Simplifying dependency declaration

    Simplifying dependency declaration

    Dependency declaration via DependencyKey is overcomplicated and requires too many lines of code for one property. I suggest to simplify it and use KeyPath itself instead of DependencyKey. Example:

    extension Dependencies {
        public var exampleInt: Int {
            get { self[\.exampleInt] ?? 0 }
            set { self[\.exampleInt] = newValue }
        }
    }
    
    opened by dankinsoid 3
  • Declare DependencyKeys namespace

    Declare DependencyKeys namespace

    I find it pretty convenient to declare dependency keys in a namespace. I can implement it in my higher-level package (tca-extensions), but I guess that tca-env is a more appropriate place for this thing 🙂.

    Note that Readme is updated a bit

    opened by maximkrouk 10
  • wip: Expose ComposableEnvironment.updatingFromParentIfNeeded to public API

    wip: Expose ComposableEnvironment.updatingFromParentIfNeeded to public API

    It is needed to create custom pullbacks, in my case for recursive reducers

    https://github.com/CaptureContext/composable-architecture-extensions/blob/main/Sources/ComposableExtensionsCore/Reducer%2BRecursion.swift

    opened by maximkrouk 5
Releases(0.5.3)
  • 0.5.3(Aug 22, 2022)

    When importing versions of TCA defining ReducerProtocol, 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 occurrences 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 and you can start transitioning to reducer protocols at your rhythm.

    Source code(tar.gz)
    Source code(zip)
  • 0.5.2(Jan 6, 2022)

  • 0.5.1(Dec 28, 2021)

    A new ComposableDependencies module can be used when you only want to define dependencies without having to commit to ComposableEnvironment or GlobalEnvironment at this level[^1]. These dependencies can then be imported and used by either environment's style (thanks @maximkrouk).

    [^1]: You don't need to import ComposableDependencies if you're already importing ComposableEnvironment or GlobalEnvironment where you declare your dependency.

    You can now call Dependencies.reset() to reset dependencies to their default when testing features using GlobalEnvironment. This method is not available for ComposableEnvironment, as you can create new environment instances instead.

    Source code(tar.gz)
    Source code(zip)
  • 0.5.0(Dec 27, 2021)

    GlobalEnvironment

    The library is now split into two mutually exclusive modules:

    • ComposableEnvironment, when you need to update a dependency value mid-chain
    • GlobalEnvironment, when your dependencies are shared among all your environments with the same values.

    Requirements for both modules are different. GlobalEnvironment is the simplest one to implement and should fit most cases. If your project doesn't require mid-chain overrides of dependencies, we recommend adopting GlobalEnvironment, which can be as simple as conforming your environment type to a marker protocol.

    The API is kept as similar as possible, so you can migrate a project from one to another without having to change much code.

    Dependency aliases

    You can now identify dependencies defined with different names in different domains. For example, if you defined main for the main queue in one domain and mainQueue in another, you can alias the former with the latter using:

    environment.aliasing(\.main, to: \.mainQueue)
    

    Both properties will then return the same dependency value. Aliasing can also be defined when using the @DerivedEnvironment property wrapper.

    ComposableDependencies

    The ComposableDependencies type, where you install your computed properties to access your dependencies, has been renamed Dependencies. An Xcode "fix-it" is provided.

    Source code(tar.gz)
    Source code(zip)
  • 0.4.0(Aug 7, 2021)

    You can now optionally forgo @Dependency and @DerivedEnvironment declarations:

    • You can directly access dependencies using their property name defined in ComposableDepencies directly in your ComposableEnvironment 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 was updated to show how this feature can be used and mixed with the property-wrapper approach.

    Source code(tar.gz)
    Source code(zip)
  • 0.3.0(Jul 16, 2021)

    This release removes the restriction which prohibited dependencies values to be modified once the chain of environment was accessed for the first time.

    This safeguard was installed because environments did retrieve dependencies from their parent only once, on first access (for performance reasons), without any updating mechanism.

    This restriction is now lifted and environments are now spotting when their parent's dependencies have potentially changed, and update accordingly.

    Source code(tar.gz)
    Source code(zip)
  • 0.2.0(Jun 8, 2021)

    @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.

    Source code(tar.gz)
    Source code(zip)
Owner
Thomas Grapperon
Thomas Grapperon
A library that provides undo semantics for the Composable Architecture with optional bridging tofUndoManager.

Swift Composable Undo A library that provides undo semantics for the Composable Architecture with optional bridging with UndoManager. Motivation It is

Aacapella Holdings Pty. Ltd. 17 Nov 18, 2022
Learn how to structure your iOS App with declarative state changes using Point-Free's The Composable Architecture (TCA) library.

Learn how to structure your iOS App with declarative state changes using Point-Free's The Composable Architecture (TCA) library.

Tiago Henriques 0 Oct 2, 2022
Open source game built in SwiftUI and the Composable Architecture.

isowords This repo contains the full source code for isowords, an iOS word search game played on a vanishing cube. Connect touching letters to form wo

Point-Free 2.1k Jan 1, 2023
Pointfree's Composable Architecture Relay

RelayStore Pointfree's Composable Architecture Relay to hook into Actions sent to the Store from outside. Read more at Observe actions in The Composab

Alejandro Martínez 3 May 29, 2022
ScrumdingerTCA - Apple’s tutorial app recreated using The Composable Architecture

ScrumdingerTCA Apple’s tutorial app recreated using The Composable Architecture

Pat Brown 9 Nov 29, 2022
Jetpack Compose and SwiftUI based Kotlin Multiplatform project

BikeShare Jetpack Compose and SwiftUI based Kotlin Multiplatform sample project (based on CityBikes API). Running on iOS (SwiftUI) macOS (SwiftUI) And

John O'Reilly 464 Jan 1, 2023
A basic twitter app to view, compose, favorite, and retweet tweets

A basic twitter app to view, compose, favorite, and retweet tweets

Kaya Yeboah 0 Nov 4, 2021
Best architecture for SwiftUI + CombineBest architecture for SwiftUI + Combine

Best architecture for SwiftUI + Combine The content of the presentation: First of the proposed architectures - MVP + C Second of the proposed architec

Kyrylo Triskalo 3 Sep 1, 2022
Mvi Architecture for SwiftUI Apps. MVI is a unidirectional data flow architecture.

Mvi-SwiftUI If you like to read this on Medium , you can find it here MVI Architecture for SwiftUI Apps MVI Architecture Model-View-Intent (MVI) is a

null 12 Dec 7, 2022
Mahmoud-Abdelwahab 5 Nov 23, 2022
Github repo search with using mvvm-c and clean architecture and using combine swift

GitSearchWithMVVM-C-CleanArchitecture Github repo search with using mvvm-c and clean architecture and using combine swift. Content Overview How To Run

Muhammad Qasim Majeed 1 Mar 16, 2022
SwiftUI sample app using Clean Architecture. Examples of working with CoreData persistence, networking, dependency injection, unit testing, and more.

Articles related to this project Clean Architecture for SwiftUI Programmatic navigation in SwiftUI project Separation of Concerns in Software Design C

Alexey Naumov 4k Jan 8, 2023
An iOS template project using SwiftUI, Combine and MVVM-C software architecture

SwiftUI-MVVM-C A template project that uses SwiftUI for UI, Combine for event handling, MVVM-C for software architecture. I have done some small proje

Huy Nguyen 107 Jan 2, 2023
RippleQueries is an iOS application built as assessment task at Ripple Egypt. Built Using MVVM (Model-View-ViewModel) and Clean Architecture concepts

RippleRepositories RippleRepositories is an iOS application built as an assessment task at Ripple Egypt. Built Using RxSwift & MVVM (Model-View-ViewMo

Muhammad Ewaily 3 Sep 16, 2021
ToDo App Build With Swift And MVVM Architecture

To Do App Project Description This project is the result of what i learned during IB Tech iOS Mobile Development Bootcamp. This project includes view

null 0 Oct 28, 2021
A demo app to showcase testable, modern iOS development with SwiftUI and Combine on MVVM-C architecture.

Coinz_App_iOS A demo app to showcase testable, modern iOS development with SwiftUI and Combine on MVVM-C architecture. Tech Stack: Swift, SwiftUI, Com

Burhan Aras 0 Dec 26, 2021
Anime schedule, korean subtitle for iOS with SwiftUI + Combine and MVVM architecture

AniTime Anime schedule, korean subtitle for iOS with SwiftUI + Combine and MVVM architecture I'm developing a new one for the SwiftUI life cycle annou

Kwangmin Bae 8 Mar 14, 2022
SwiftUI-TodoList - This project uses MVVM architecture and UserDefaults to persist data

SwiftUI-TodoList | ├── SwiftUITodoList | ├── Library | ├── Models

Yezan Ahmed 0 Mar 9, 2022