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
104 changes: 60 additions & 44 deletions Sources/Flow/Flow.docc/CoreConcepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,76 +54,92 @@ struct CounterView: View {

Actions can return results through `ActionTask`. The result type (`ActionResult`) can be defined for each Feature.

In this example, a child view returns a selection result to the parent, which handles navigation:
**Basic example:**

```swift
import SwiftUI
import Flow

struct ParentView: View {
@Environment(\.navigator) private var navigator

var body: some View {
ChildView { selectedId in
await navigator.navigate(to: .detail(id: selectedId))
}
}
}

struct ChildView: View {
@State private var store = Store(
initialState: .init(),
feature: ChildFeature()
)
let onSelect: (String) async -> Void

var body: some View {
Button("Select") {
Task {
let result = await store.send(.select).value
if case .success(.selected(let id)) = result {
await onSelect(id)
}
}
}
}
}

struct ChildFeature: Feature {
struct LoginFeature: Feature {
@Observable
final class State {
var selectedId = ""
var username = ""
var password = ""
}

enum Action: Sendable {
case select
case login
}

enum ActionResult: Sendable {
case selected(id: String)
case success
case invalidCredentials
case networkError
}

func handle() -> ActionHandler<Action, State, ActionResult> {
ActionHandler { action, state in
switch action {
case .select:
return .run { state in
let id = state.selectedId
return .selected(id: id)
case .login:
if state.username.isEmpty || state.password.isEmpty {
return .just(.invalidCredentials) // Return result immediately
}
return .run { state in // Async work with result
do {
try await api.login(state.username, state.password)
return .success
} catch {
return .networkError
}
}
}
}
}
}

struct LoginView: View {
@State private var store = Store(
initialState: .init(),
feature: LoginFeature()
)

var body: some View {
VStack {
TextField("Username", text: $store.state.username)
SecureField("Password", text: $store.state.password)
Button("Login") {
Task {
let result = await store.send(.login).value
switch result {
case .success(.success):
print("Navigate to home")
case .success(.invalidCredentials):
print("Show error: Invalid credentials")
case .success(.networkError):
print("Show error: Network error")
case .failure(let error):
print("Unexpected error: \(error)")
}
}
}
}
}
}
```

This implementation provides:
- `ChildFeature` returns selection results to the parent via `ActionResult`
- `ParentView` receives results through the `onSelect` callback
- Parent controls side effects like navigation
- Everything stays within the view tree, allowing dependencies to be tracked
**Key concepts:**
- **ActionResult** - Define custom result types for your Feature
- **`.just(result)`** - Return results immediately (synchronous)
- **`.run { ... return result }`** - Return results after async work
- **`await store.send().value`** - Wait for and receive the result
- **`Result<ActionResult, Error>`** - Results are wrapped in Swift's Result type

**Use cases for ActionResult:**
- Form validation with specific error types
- Navigation decisions based on action outcomes
- Showing different toasts based on success/failure patterns

See <doc:PracticalGuide> for more details.
For parent-child communication patterns and more advanced examples, see <doc:PracticalGuide#Parent-Child-Communication>.

### @Observable Support

Expand Down
109 changes: 106 additions & 3 deletions Sources/Flow/Flow.docc/CoreElements.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,29 @@ Flow consists of five core elements:

These elements work together to build applications.

## Quick Reference

| Element | Purpose | Key APIs |
|---------|---------|----------|
| **Store** | Manages state and coordinates actions | `send()`, `state` property |
| **Feature** | Groups State, Actions, and Handler | `State`, `Action`, `handle()` |
| **ActionHandler** | Processes actions and returns tasks | Returns `ActionTask<Action, State, ActionResult>` |
| **ActionResult** | Defines the result type of actions | `Void` or custom type |
| **ActionTask** | Manages async execution | `.none`, `.run`, `.just`, `.cancel`, `.concatenate` |

## Store

**Store** manages state, receives actions from views, sends them to handlers, and processes results.

**Key responsibilities:**
- Holds the current state
- Receives actions from views via `send()`
- Coordinates with ActionHandler to process actions
- Returns `ActionTask` for async result handling

**Why use `@State`?**
Store is held with `@State` to tie its lifecycle to the view. When the view appears, the store initializes; when dismissed, it cleans up automatically.

```swift
import SwiftUI
import Flow
Expand All @@ -37,9 +56,29 @@ var body: some View {
}
```

**Sending actions:**

```swift
// Fire-and-forget (common for state-only updates)
store.send(.increment)

// Handle results for navigation, validation, etc.
Task {
let result = await store.send(.save).value
if case .success = result {
// Navigate or show confirmation
}
}
```

## Feature

**Feature** defines State, Action, and ActionHandler in one place.
**Feature** groups State, Actions, and ActionHandler in one place, providing a complete definition of a feature's behavior.

**Why use Feature?**
- **Cohesion** - Related logic stays together
- **Reusability** - Easy to test and reuse across views
- **Type safety** - State, Action, and ActionResult are strongly typed

```swift
import Flow
Expand All @@ -58,7 +97,18 @@ struct UserFeature: Feature {

func handle() -> ActionHandler<Action, State, Void> {
ActionHandler { action, state in
// Business logic goes here
switch action {
case .load:
state.isLoading = true
return .run { state in
let user = try await api.fetchUser()
state.user = user
state.isLoading = false
}
case .logout:
state.user = nil
return .none
}
}
}
}
Expand All @@ -68,6 +118,35 @@ struct UserFeature: Feature {

**ActionHandler** receives actions and current state, updates state, and returns an ActionTask.

**How it works:**
The handler is a closure that receives two parameters:
- **action** - The action to process
- **state** - The current state (mutable)

You can update state directly and return a task describing any async work.

**Example:**

```swift
ActionHandler<Action, State, Void> { action, state in
switch action {
case .increment:
state.count += 1 // Synchronous state update
return .none // No async work

case .fetchData:
state.isLoading = true
return .run { state in // Async work
let data = try await api.fetch()
state.data = data
state.isLoading = false
}
}
}
```

**Generic type parameters:**

```swift
ActionHandler<Action, State, ActionResult>
```
Expand All @@ -85,7 +164,16 @@ Type parameters:

**Return results from synchronous processing:**

Use `.just()` to return results immediately.
Use `.just()` to return results immediately without async work.

`.just()` is similar to `.none`, but returns a custom result value:
- **`.none`** - Returns `Void` (no result)
- **`.just(result)`** - Returns a specific result value

Common uses:
- Validation results
- Calculations based on current state
- Cache hits or default values

```swift
ActionHandler { action, state in
Expand Down Expand Up @@ -184,6 +272,16 @@ return .just(.success)

**Execute async processing**

Use `.cancellable(id:cancelInFlight:)` to manage long-running tasks:

**Parameters:**
- **`id`** - Unique identifier for the task (used for cancellation)
- **`cancelInFlight`** - Behavior when starting a new task with the same ID:
- `true` - Cancels the existing task before starting the new one
- `false` - Allows both tasks to run concurrently

**Example:**

```swift
return .run { state in
let user = try await api.fetchUser()
Expand All @@ -192,6 +290,11 @@ return .run { state in
.cancellable(id: "load-user", cancelInFlight: true)
```

**Task ID naming:**
- Use descriptive names: `"search"`, `"load-user"`, `"upload-photo"`
- Keep consistent across related actions
- Task IDs are scoped to each Store instance

**Cancel running task**

```swift
Expand Down
57 changes: 57 additions & 0 deletions Sources/Flow/Flow.docc/Flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,63 @@

A library for managing state in SwiftUI applications in a type-safe way. Flow provides a unidirectional data flow architecture and supports Swift 6 Approachable Concurrency.

## Overview

Flow brings predictable state management to SwiftUI with a view-local approach. Unlike architectures with global stores (Redux, TCA), Flow keeps state scoped to views, making it easier to reason about lifecycle and dependencies.

**Design Philosophy:**
- **View-local state** - No singleton stores to manage
- **Unidirectional flow** - Actions → Handler → State → View
- **Type-safe results** - Actions can return typed results for navigation and error handling
- **Concurrency-first** - Built for Swift 6 with MainActor isolation

**Quick Example:**

```swift
import SwiftUI
import Flow

// Define your feature
struct CounterFeature: Feature {
@Observable final class State {
var count = 0
}

enum Action: Sendable {
case increment
}

func handle() -> ActionHandler<Action, State, Void> {
ActionHandler { action, state in
state.count += 1
return .none
}
}
}

// Use in your view
struct CounterView: View {
@State private var store = Store(
initialState: .init(),
feature: CounterFeature()
)

var body: some View {
Button("Count: \(store.state.count)") {
store.send(.increment)
}
}
}
```

For a complete walkthrough, see <doc:GettingStarted>.

**Architecture:**

![Flow Architecture](flow-diagram.svg)

Actions flow through the handler, update state, and SwiftUI re-renders automatically.

## Key Features

- **No global store** - Each view holds its own state with `@State`
Expand Down
10 changes: 6 additions & 4 deletions Sources/Flow/Flow.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ dependencies: [

## Build a Counter App

### Step 1
### Step 1: Define Your Feature

Create a Feature and define State, Action, and Handler.
Create a Feature that groups **State** (data), **Actions** (events), and **ActionHandler** (logic).

```swift
import Flow
Expand Down Expand Up @@ -85,9 +85,9 @@ struct CounterFeature: Feature {
}
```

### Step 2
### Step 2: Create Your View

Create a View and integrate the Store.
Create a View and integrate the **Store**, which manages state and coordinates actions.

```swift
import SwiftUI
Expand Down Expand Up @@ -122,6 +122,8 @@ struct CounterView: View {

That's it! You've implemented the basic functionality.

> **Understanding the Code**: To learn what Feature, State, Action, ActionHandler, and Store mean and how they work together, continue to <doc:CoreConcepts>.

## Next Steps

Now that you've built a counter app, let's dive deeper into Flow's architecture:
Expand Down
Loading
Loading