Composable-effect-identifier - A library that helps managing `Effect` identifiers when using The Composable Architecture (TCA)

Overview

Composable Effect Identifier

Swift Documentation

This ComposableEffectIdentifier is a small accessory library to The Composable Architecture (TCA). It allows to improve user experience when defining Effect identifiers.

It provides two tools to this end: a @EffectID property wrapper, and a namespace() higher order reducer that allows several similar stores instances to run in the same process without having to micro-manage ongoing Effect identifiers.

The @EffectID property wrapper.

When using TCA with long-lived effects, we need to provide some hashable value to identify them accross runs of the same reducer. If we start a timer effect, we need to provide an identifier for the effect in order to retrieve the effect and cancel it when we don't need it anymore.

Any Hashable value can be used as effect identifier. The authors of TCA are recommending to exploit Swift type system by defining ad hoc local and property-less Hashable structs. Any value of this struct is equal to itself, and collisions risks are limited, as these types are defined locally.

For example, inside some Reducer's block, one can define:

struct TimerID: Hashable {}

We can then use any value of this type as an effect idenfier:

switch action {
  case .start:
    return Effect.timer(id: TimerId(), every: 1, on: environment.mainQueue)
      .map { _ in .tick }
      
  case .stop:
    return .cancel(id: TimerID())
   
  case .tick:
    state.count += 1
    return .none
}

This works well. Calling TimerID() and creating a whole type when we simply need an Hashable value feels a little awkward though.

The @EffectID property wrapper allows to define indentifiers with an absolutely clear intent:

@EffectID var timerID

Accessing this value returns a unique and stable Hashable value that can be used to identify effects:

switch action {
  case .start:
    return Effect.timer(id: timerID, every: 1, on: environment.mainQueue)
      .map { _ in .tick }
      
  case .stop:
    return .cancel(id: timerID)
   
  case .tick:
    state.count += 1
    return .none
}

In order to be defined locally into some reducer, Swift >=5.4 is required (more precisely Swift >=5.5, as there is a bug with value-less local property wrappers in Swift 5.4).

By assigning some Hashable value to the property, you can augment the generated identifier with additional data:

@EffectID var timerID = state.identifier

Please note that @EffectID sharing the same user-defined value will not be equal if defined in difference places:

@EffectID var id1 = "A"
@EffectID var id2 = "A"
// => id1 != id2

The use of user-defined values can be even avoided most of the time when using namespaces.

Namespaces

With its current implementation, the core TCA library can be inconvenient to use in certain configurations, especially when developing document-based apps for example. In this kind of apps, each document is represented by a root Store. Each document should be unware of the existence of other documents opened at the same time. In such an app, many instances of the same type of root Store may run at the same time in the same process. When using local identifiers like Hashable structs in reducers to identify effects, one may create collisions, where one store instance may cancel an effect originating from another store (because ongoing effects are internally stored in a common, top-level, dictionary).

One solution would be to propagate some document-specific identifier in the State or the Environment, but this would require to append this identifier to every effect identifier in order to work properly. Furthermore, "leaking" such an identifier in every unrelated feature impedes feature isolation and reusability (a TCA core principle).

In the same spirit, composing a collection of features with ongoing effects is cumbersome too, as we need to inject some element-specific identifier to discern similar effects originating from different rows. Usually, row features are relatively simple, so the pervasion of the row's identifier is less perceptible, but it's still there, where the row feature should ultimately work in some list-agnostic context.

Fortunately, ComposableEffectIdentifier ships with a feature that helps greatly to solve this kind of issue. It works in conjunction with and requires the use of the @EffectID property wrapper to declare effect identifiers. Any Reducer can be namespaced with some Hashable value. This value is used to augment @EffectID identifiers with contextual data (you can see it like a user-provided value, but coming "from the top"). Namespaces are propagated downstream along the Reducer's tree, and they compose with deaper namespaces.

Reducer namespaces

You namespace a reducer using the .namespace<ID>(_ id: ID) higher order reducer, which doesn't change the generic signature of the source Reducer. The id can be provided directly, or as a function or KeyPath from State or Environment. The id value should be constant for the branch, during all the execution of the program. For document-based app, you will most likely namespace the root-reducer with some stable value that identify uniquely the document.

Automatic namespaces

Two semi-overloads of the forEach pullback are provided. They are both named forEachNamespace, but they share the same arguments as their forEach counterparts otherwise. These reducers are working like forEach, but they're also namespacing their local reducers using the element's identifier (or dictionary key), thereby siloing the effects of each pulled-back reducer. For this reason, these local reducers can define their effect identifiers using the @EffectID property wrapper in isolation, without having to carry an external identifier.

Identified states

TCA already ships with an Identified wrapper that can wrap any value into an Identifiable value. The use of @EffectID leads to features that are becoming more and more agnostic of an external identifier. Because of this, it can be convenient to wrap the State of an identifier-less feature with the Identified wrapper, for example to include-it into an IdentifiedArrayOf<Identified<State, ID>>. As wrapping the feature to make it identifiable may be the only outcome of the procedure, an overload of forEachNamespace is also provided to directly pull-back, namespace, and identify an identifier-less reducer in one call. This overload is available when the GlobalState is IdentifiedArrayOf<Identified<LocalState, ID>>, and the identifier-less reducer works on LocalState.

Example app

In order to demonstrate the power the namespaced reducers and @EffectID property wrappers, the library ships with an example app that pulls a neat trick: A LonelyTimer TCA feature is implemented. This feature handles a timer that count backward down to zero, with some start/stop functions. The LonelyTimer feature is unaware of the outer world. It handles its count, and that's it. It doesn't have an identifier itself. It has a name, but only by courtesy.

Around this LonelyTimer feature, an app called ManyTimers is built. This app handles an arbitrary number of timers, but without touching the code of LonelyTimer. The ManyTimers app furthermore ships in 4 flavors: an shoebox app, where a dozen of timers are hosted in a list at the same time, for iOS and macOS, and a document-based app, where each file handles one timer, again for iOS and macOS. The document based app can have several files opened at the same time, which is in some way similar as hosting them side to side in a list.

With both apps, several timers can run and be interacted with at the same time, each in isolation. Only one effect identifier is defined, at the LonelyTimer level.

Documentation

The latest documentation for ComposableEffectIdentifier's APIs is available here.

Installation

Add

.package(url: "https://github.com/tgrapperon/composable-effect-identifier", from: "0.0.1")

to your Package dependencies in Package.swift, and then

.product(name: "ComposableEffectIdentifier", package: "composable-effect-identifier")

to your target's dependencies.

Credits and thanks

The author (@tgrapperon) would like to especially thank @iampatbrown who gave the initial feedback that allowed to shape this library, and of course @mbrandonw and @stephencelis for the incredible work they put though TCA and their other amazing open-source projects.

License

This library is released under the MIT license. See LICENSE for details.

You might also like...
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

KHabit an open source, pure and minimalistic app which helps you maintain productive habits, and nothing more.

an open source, pure and minimalistic app which helps you maintain productive habits, and nothing more. The app is completely open source, it does not contain in-app or ads, and does not track the user in any way.

an open source, pure and minimalistic app which helps you maintain productive habits, and nothing more.

KHabit an open source, pure and minimalistic app which helps you maintain productive habits, and nothing more. The app is completely open source, it d

Are you bored? This app helps find you something to do via Bored api
Are you bored? This app helps find you something to do via Bored api

Bored Swift Are you bored? This app helps find you something to do via Bored api Features Beautiful widgets 📅 Simple and easy ✅ Open source 📱 Previe

Molt is a meditation/relaxation app that helps you get into the right headspace by shedding your stressful thoughts.
Molt is a meditation/relaxation app that helps you get into the right headspace by shedding your stressful thoughts.

A relaxation app where you toss away your stressful thoughts (represented by sticky notes) and pause for a few minutes with a peaceful scene.

Rhythm helps those with Parkinson's more easily coordinate their gait with rhythmic audio.

Rhythm-Parkinson-s-App Rhythm helps those with Parkinson's more easily coordinate their gait with rhythmic audio. Research https://davisphinneyfoundat

Reading List is an iOS app for iPhone and iPad which helps users track and catalog the books they read
Reading List is an iOS app for iPhone and iPad which helps users track and catalog the books they read

Reading List Reading List is an iOS app for iPhone and iPad which helps users track and catalog the books they read. Reading List v2 As of version 2.0

Pomodoro App - an App that helps you perform tasks for a while, following the Pomodoro technique
Pomodoro App - an App that helps you perform tasks for a while, following the Pomodoro technique

Pomodoro App - an App that helps you perform tasks for a while, following the Pomodoro technique

SwiftUI sample app using Clean Architecture. Examples of working with CoreData persistence, networking, dependency injection, unit testing, and more.
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

Comments
  • Make LonelyTimer depend on the live package instead of the local version

    Make LonelyTimer depend on the live package instead of the local version

    The ../../ local dependency is apparently causing issues with Swift Package Index builds. The local version should still be used by Xcode when using the ComposableIdentifier workspace, so I'll make LonelyTimer depend on the online version to check if it solves the issue. Otherwise, I'll need to check if SPI provides an API to build only the top-level package.

    opened by tgrapperon 0
  • Minor README changes

    Minor README changes

    I noticed some minor typos while reading through the README, although in a couple spots my suggested changes may deviate from your voice/style. And beware, I snuck in an oxford comma, which you can absolutely ignore.

    Thank you so much for your many, many contributions to TCA!

    opened by gohanlon 0
Releases(0.0.1)
Owner
Thomas Grapperon
Thomas Grapperon
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
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.

Thomas Grapperon 129 Dec 14, 2022
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
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
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
Secretive is an app for storing and managing SSH keys in the Secure Enclave.

Secretive is an app for storing and managing SSH keys in the Secure Enclave.

Max Goedjen 4.9k Jan 1, 2023
Stock is a MacOS menu bar app that helps you quickly save a web link, a file link, or a text by using drag and drop

Stock is a MacOS menu bar app that helps you quickly save a web link, a file link, or a text by using drag and drop

シュンジョーァ 19 Dec 4, 2022