Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Documentation/ArchitecturalComparison.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Architectural Comparison

EffectView is not a new idea. It translates a family of well-established patterns — Elm, Redux, Elixir/GenServer — into idiomatic SwiftUI, using Swift's own concurrency model rather than fighting it.
EffectComponents is not a new idea. It translates a family of well-established patterns — Elm, Redux, Elixir/GenServer — into idiomatic SwiftUI, using Swift's own concurrency model rather than fighting it.

This document maps EffectView against the patterns iOS developers are most likely to know, across five dimensions that matter in practice:
This document maps EffectComponents against the patterns iOS developers are most likely to know, across five dimensions that matter in practice:

1. **State model** — what kinds of state exist, who owns each kind, and who can mutate it
2. **Effect/side-effect model** — how async work is described and executed
Expand Down Expand Up @@ -52,7 +52,7 @@ A global store holds the entire application state. A single pure *reducer* funct

### The Composable Architecture (TCA)

The most direct comparison to EffectView. TCA targets SwiftUI with a Redux-shaped architecture: a `@Reducer` macro generates a store, actions map to state mutations and `Effect<Action>` return values.
The most direct comparison to EffectComponents. TCA targets SwiftUI with a Redux-shaped architecture: a `@Reducer` macro generates a store, actions map to state mutations and `Effect<Action>` return values.

**State model:** Composable tree of child stores. Parent features compose child features using `Scope` and `IfLetStore`. Shared state is managed via `@Shared` property wrappers with explicit persistence strategies.

Expand Down Expand Up @@ -86,7 +86,7 @@ The direct ancestor of all patterns in this family. An Elm application is define

The most conceptually illuminating comparison. A `GenServer` is an actor with a single `handle_call` / `handle_cast` callback — the structural equivalent of `update`. State is private to the process; all mutations go through the callback; side effects are either synchronous return values or out-of-band messages sent to other processes.

Phoenix LiveView's `handle_event` maps almost directly to EffectView's `update`: it receives the current socket (state), an event name, and parameters, mutates the socket, and optionally pushes async work via `Task.async` or `send_update`.
Phoenix LiveView's `handle_event` maps almost directly to a transducer's `update`: it receives the current socket (state), an event name, and parameters, mutates the socket, and optionally pushes async work via `Task.async` or `send_update`.

**State model:** Process-local. Each LiveView socket / GenServer process owns its own state exclusively. Shared state between processes requires explicit message passing or a shared ETS table — it is never implicit.

Expand All @@ -100,9 +100,9 @@ Phoenix LiveView's `handle_event` maps almost directly to EffectView's `update`:

---

## EffectView
## EffectComponents

EffectView translates the Elm/GenServer model into idiomatic SwiftUI — using `@State`, structured concurrency, and `@MainActor` as the runtime rather than a custom one.
EffectComponents translates the Elm/GenServer model into idiomatic SwiftUI — using `@State`, structured concurrency, and `@MainActor` as the runtime rather than a custom one.

**State model:** Three kinds of state are structurally distinct:

Expand Down Expand Up @@ -198,7 +198,7 @@ This is a stronger encapsulation boundary than TCA's store, which is deliberatel

## Summary

| | MVVM | Redux | TCA | Elm | Elixir/GenServer | EffectView |
| | MVVM | Redux | TCA | Elm | Elixir/GenServer | EffectComponents |
|---|---|---|---|---|---|---|
| **Ephemeral state owner** | ViewModel class | Global store | Feature store | Model value | Process-local | `ViewState` value |
| **Shared state access** | Direct reference | Global selector | `@Shared` wrapper | Message-passing only | Explicit IPC | Read-only slice via `.observe` |
Expand All @@ -210,6 +210,6 @@ This is a stronger encapsulation boundary than TCA's store, which is deliberatel
| **Dispatch levels** | Synchronous call | Fire-and-forget | Fire-and-forget (+ async `send` inside effects) | Fire-and-forget | `call` (sync) / `cast` (async) | `post` / `send` / `request` |
| **Test surface** | Full class construction | Reducer pure function | `TestStore` harness | Pure `update` function | Process message passing | Static pure function |

The common thread in the well-designed patterns (Elm, GenServer, TCA, EffectView) is the same: a single authoritative transition function that owns all state mutations and returns effect descriptions. The differences are in scope (global vs. local), dispatch semantics, task lifecycle management, and how much framework ceremony is required to express the pattern.
The common thread in the well-designed patterns (Elm, GenServer, TCA, EffectComponents) is the same: a single authoritative transition function that owns all state mutations and returns effect descriptions. The differences are in scope (global vs. local), dispatch semantics, task lifecycle management, and how much framework ceremony is required to express the pattern.

EffectView's position is that the SwiftUI runtime already provides the scope, lifecycle, and concurrency model — the only missing piece is a structured way to describe and manage effects. The library adds that piece and nothing else.
EffectComponents' position is that the SwiftUI runtime already provides the scope, lifecycle, and concurrency model — the only missing piece is a structured way to describe and manage effects. The library adds that piece and nothing else.
12 changes: 6 additions & 6 deletions Documentation/CorrectByConstruction.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Correct by Construction: State Machines and MVI with EffectView
# Correct by Construction: State Machines and MVI with EffectComponents

The [previous article](TamingAsyncTasksInSwiftUIViews.md) solved a mechanical problem: `.task` doesn't give you the tools to manage task lifetimes properly. This article addresses a deeper one.

Expand All @@ -11,13 +11,13 @@ When behaviour lives inside closures, async functions, and stored properties sca
- Can two pieces of state ever be in contradiction with each other?
- What happens if the user taps a button while something is already loading?

EffectView addresses this by pulling all logic into a single, pure function.
EffectComponents addresses this by pulling all logic into a single, pure function.

---

## The update function

The heart of EffectView is the update function:
The heart of EffectComponents is the update function:

```swift
(inout State, Event) -> Effect<Event, Env>?
Expand Down Expand Up @@ -59,7 +59,7 @@ Without an FSM, a search screen typically accumulates state like this:

There are immediately several illegal combinations: `isLoading == true && errorMessage != nil`. `results.isEmpty && !isLoading && errorMessage == nil` — is that idle, or empty results? Tests have to enumerate these combinations and hope they've covered the right ones.

With EffectView you model state as a Swift enum instead:
With EffectComponents you model state as a Swift enum instead:

```swift
enum SearchState {
Expand Down Expand Up @@ -246,9 +246,9 @@ Concurrency exists — tasks genuinely run in the background — but concurrency

## MVI in practice

EffectView implements the **Model–View–Intent** (MVI) pattern:
EffectComponents implements the **Model–View–Intent** (MVI) pattern:

| MVI concept | EffectView equivalent |
| MVI concept | EffectComponents equivalent |
|---|---|
| **Model** | `State` — the single source of truth |
| **Intent** | `Event` — user actions and system callbacks |
Expand Down
2 changes: 1 addition & 1 deletion Documentation/GitWorkflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ git push --force-with-lease origin feature/my-feature # after a rebase
## 7. Open a Pull Request

```bash
open https://github.com/couchdeveloper/EffectView/compare/main...feature/my-feature?expand=1
open https://github.com/couchdeveloper/EffectComponents/compare/main...feature/my-feature?expand=1
```

Fill in title and body, then click **Create pull request**.
Expand Down
6 changes: 3 additions & 3 deletions Documentation/Recipes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Recipes

Short, practical snippets for common EffectView patterns.
Short, practical snippets for common EffectComponents patterns.

## Post an event from the view

Expand All @@ -17,8 +17,8 @@ Button("Retry") {
```

Note: in this case it is safe to write `try?` since we can ignore the error when
attempting to dispatch an event when it happens within the button actions or
whithin the onChange closure.
attempting to dispatch an event when it happens within the button actions or
within the onChange closure.



Expand Down
6 changes: 3 additions & 3 deletions Documentation/RuntimeDesign.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ In short, `EffectView` is an event-driven runtime built around a finite state ma
At a high level, effects come in two forms:

- Actions are inline effect steps in the current computation cycle. Unlike tasks, they remain part of the current event chain even when they suspend.
- tasks are managed asynchronous operations; they run outside the current reduction step, may be tracked by logical identifier, and can feed events back into the system later
- Tasks are managed asynchronous operations; they run outside the current reduction step, may be tracked by logical identifier, and can feed events back into the system later.

Two earlier articles describe adjacent concerns from the public API side:

Expand Down Expand Up @@ -162,7 +162,7 @@ A fire-and-forget call schedules work and returns immediately. The caller does n

This is intentionally the weakest back pressure mode. It is useful, but it is also the deliberate escape hatch: callers can create pending work without themselves awaiting admission.

### Synchronous disaptch (`send`)
### Synchronous dispatch (`send`)

A synchronous `send` means:

Expand Down Expand Up @@ -377,6 +377,6 @@ The runtime is intentionally built around four ideas:
3. caller suspension provides the primary back pressure mechanism
4. `TaskManager` centralizes shutdown and task-lifecycle semantics

This gives `EffectView` a runtime that stays small in code size while still supporting async actions, request/response bridging, runtime-managed tasks, immediate interruption, and future runtime control features.
This gives EffectComponents a runtime that stays small in code size while still supporting async actions, request/response bridging, runtime-managed tasks, immediate interruption, and future runtime control features.

The design is intentional, not accidental.
8 changes: 4 additions & 4 deletions Documentation/SwiftUIFirst.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Modern SwiftUI development has accumulated a rich set of companion patterns: Vie

These patterns all solve real problems. But they solve them *outside* SwiftUI — as a layer on top of it. The result is that every project ends up with two architectures: SwiftUI's own model and the one the team bolted on.

EffectView takes a different position. It asks: what if SwiftUI's own mechanisms are sufficient, and the only thing missing is structured effect management?
EffectComponents takes a different position. It asks: what if SwiftUI's own mechanisms are sufficient, and the only thing missing is structured effect management?

---

Expand All @@ -22,7 +22,7 @@ SwiftUI is not a rendering library. It is an architecture.

These are not implementation details. They are the intended architecture for SwiftUI applications. `Binding` is the dependency injection mechanism for state. `Environment` is the dependency injection mechanism for services. View identity is the lifecycle. All of these are first-class, framework-supported tools.

The only gap is **effect management**: triggering, naming, cancelling, and coordinating async tasks in response to logic rather than rendering. That is what EffectView adds.
The only gap is **effect management**: triggering, naming, cancelling, and coordinating async tasks in response to logic rather than rendering. That is what EffectComponents adds.

---

Expand Down Expand Up @@ -53,7 +53,7 @@ This function is not an object. It has no stored properties, no lifecycle, and n

Dependency injection frameworks exist to solve one problem: getting concrete implementations of services into the code that needs them, without coupling the two directly. SwiftUI's `@Environment` already does this. It is hierarchical, it propagates automatically, and it can be overridden at any level of the view tree.

EffectView connects to it through `EnvReader` — a four-line wrapper around `@Environment`. No registration, no container, no reflection, no macros.
EffectComponents connects to it through `EnvReader` — a four-line wrapper around `@Environment`. No registration, no container, no reflection, no macros.

Dependencies are declared as structs of closures in the feature module. Concrete implementations are assigned in a single `EnvironmentValues` extension in the glue layer. Test doubles are struct literals.

Expand Down Expand Up @@ -129,7 +129,7 @@ That function requires no framework to test, no mocking library, and no async te
## Getting started

```swift
.package(url: "https://github.com/couchdeveloper/EffectView", from: "0.1.0")
.package(url: "https://github.com/couchdeveloper/EffectComponents.git", from: "0.1.0")
```

Start with the simplest case — one state enum, one event enum, one `update` function — and expand from there. The pattern is the same at every scale.
4 changes: 2 additions & 2 deletions Documentation/TamingAsyncTasksInSwiftUIViews.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,11 @@ case .queryChanged(let q):

---

## Adding `EffectView` to your project
## Adding EffectComponents to your project

```swift
// Package.swift
.package(url: "https://github.com/couchdeveloper/EffectView.git", from: "0.1.0")
.package(url: "https://github.com/couchdeveloper/EffectComponents.git", from: "0.1.0")
```

The library is around 200 lines of source — a focused primitive, not a framework.
Expand Down
4 changes: 2 additions & 2 deletions Documentation/UsingEnvForDependencyInjection.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ struct MovieSearchView: View {
}
```

`EnvReader` is a thin wrapper around `@Environment`; it exists purely for ergonomics at the `EffectView` call site. The value it captures is passed to `initialEnv:`, and EffectView takes ownership from there — forwarding it to every `.run`, `.request`, and `.action` for the lifetime of the view.
`EnvReader` is a thin wrapper around `@Environment`; it exists purely for ergonomics at the `EffectView` call site. The value it captures is passed to `initialEnv:`, and `EffectView` takes ownership from there — forwarding it to every `.run`, `.request`, and `.action` for the lifetime of the view.

Note that the transducer type (`MovieSearchLogic.self`) is passed directly rather than constructing an inline closure. This keeps the view body free of logic and makes the transition function easily findable and independently testable.

Expand Down Expand Up @@ -228,4 +228,4 @@ The feature module declares *what* it needs (closure types). The glue layer deci

This is dependency injection without a framework, without reflection, and without protocols. The only mechanism is function values — which Swift has had since day one.

*Next: [Testing EffectView end-to-end](TestingEffectView.md)*
*Next: [Bridging event-driven and imperative code](BridgingEventDrivenAndImperative.md)*
2 changes: 1 addition & 1 deletion Examples/EffectViewExample/EffectViewExample/App.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SwiftUI
import EffectView
import EffectComponents

@main
struct EffectViewExampleApp: App {
Expand Down
2 changes: 1 addition & 1 deletion Examples/EffectViewExample/EffectViewExample/Counter.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftUI
import Foundation
import EffectView
import EffectComponents

enum Counter {
enum Views {}
Expand Down
2 changes: 1 addition & 1 deletion Examples/EffectViewExample/EffectViewExample/Movies.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SwiftUI
import EffectView
import EffectComponents
import Foundation

enum Movies {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftUI
import Foundation
import EffectView
import EffectComponents

@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
enum RemoteCounter {
Expand Down
12 changes: 6 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import PackageDescription

let package = Package(
name: "EffectView",
name: "EffectComponents",
platforms: [
.iOS(.v15),
.macOS(.v12),
Expand All @@ -15,8 +15,8 @@ let package = Package(
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "EffectView",
targets: ["EffectView"]
name: "EffectComponents",
targets: ["EffectComponents"]
),
],
dependencies: [
Expand All @@ -26,17 +26,17 @@ let package = Package(
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "EffectView",
name: "EffectComponents",
dependencies: [
.product(name: "Mutex", package: "swift-mutex"),
],
swiftSettings: [
]
),
.testTarget(
name: "EffectViewTests",
name: "EffectComponentsTests",
dependencies: [
"EffectView",
"EffectComponents",
.product(name: "Mutex", package: "swift-mutex"),
],
swiftSettings: [
Expand Down
Loading
Loading