Skip to main content
The SyncUps example is a complete application that demonstrates advanced TCA patterns including navigation stacks, shared state, persistence, and device capabilities like speech recognition and timers.

Overview

This example shows how to:
  • Build navigation stacks with NavigationStack
  • Share state across features with @Shared
  • Persist data automatically
  • Integrate with device capabilities (microphone, speech)
  • Manage complex feature dependencies
  • Handle real-time updates with effects

Implementation

import ComposableArchitecture
import SwiftUI

@Reducer
struct AppFeature {
  @Reducer
  enum Path {
    case detail(SyncUpDetail)
    case meeting(Meeting, syncUp: SyncUp)
    case record(RecordMeeting)
  }

  @ObservableState
  struct State: Equatable {
    var path = StackState<Path.State>()
    var syncUpsList = SyncUpsList.State()
  }

  enum Action {
    case path(StackActionOf<Path>)
    case syncUpsList(SyncUpsList.Action)
  }

  @Dependency(\.date.now) var now
  @Dependency(\.uuid) var uuid

  var body: some ReducerOf<Self> {
    Scope(state: \.syncUpsList, action: \.syncUpsList) {
      SyncUpsList()
    }
    Reduce { state, action in
      switch action {
      case .path(.element(_, .detail(.delegate(let delegateAction)))):
        switch delegateAction {
        case .startMeeting(let sharedSyncUp):
          state.path.append(
            .record(RecordMeeting.State(syncUp: sharedSyncUp))
          )
          return .none
        }

      case .path:
        return .none

      case .syncUpsList:
        return .none
      }
    }
    .forEach(\.path, action: \.path)
  }
}

Key Concepts

The app uses StackState and StackAction for navigation:
@ObservableState
struct State: Equatable {
  var path = StackState<Path.State>()
  var syncUpsList = SyncUpsList.State()
}

enum Action {
  case path(StackActionOf<Path>)
  case syncUpsList(SyncUpsList.Action)
}
Push a screen:
state.path.append(.record(RecordMeeting.State(syncUp: sharedSyncUp)))
Pop the current screen:
state.path.removeLast()

Enum-Based Destinations

Destinations are modeled as an enum:
@Reducer
enum Path {
  case detail(SyncUpDetail)
  case meeting(Meeting, syncUp: SyncUp)
  case record(RecordMeeting)
}
This provides exhaustive handling and type safety.

Shared State

Data is shared across features using @Shared:
@Shared(.syncUps) var syncUps: IdentifiedArrayOf<SyncUp>
Changes automatically:
  • Update all observers
  • Persist to disk
  • Propagate through navigation

Persistence

Define a shared persistence strategy:
extension PersistenceKey where Self == FileStorageKey<IdentifiedArrayOf<SyncUp>> {
  static var syncUps: Self {
    fileStorage(.documentsDirectory.appending(component: "sync-ups.json"))
  }
}
Data automatically loads and saves:
@Shared(.syncUps) var syncUps: IdentifiedArrayOf<SyncUp> = []
// Automatically persisted to sync-ups.json

Device Integration

The app integrates with device capabilities:

Speech Recognition

@Dependency(\.speechRecognizer) var speechRecognizer

return .run { send in
  let result = try await speechRecognizer.recognizeAudioFile(url)
  await send(.transcriptUpdated(result))
}

Timers

@Dependency(\.continuousClock) var clock

return .run { send in
  for await _ in clock.timer(interval: .seconds(1)) {
    await send(.timerTicked)
  }
}

Audio Recording

@Dependency(\.audioRecorder) var audioRecorder

return .run { send in
  try await audioRecorder.startRecording(url: url)
  await send(.recordingStarted)
}

Delegate Pattern

Child features communicate with parents using delegates:
// Child action
enum Action {
  case delegate(Delegate)

  enum Delegate {
    case startMeeting(Shared<SyncUp>)
  }
}

// Child sends delegate action
return .send(.delegate(.startMeeting(state.$syncUp)))

// Parent observes delegate action
case .path(.element(_, .detail(.delegate(.startMeeting(let sharedSyncUp))))):
  state.path.append(.record(RecordMeeting.State(syncUp: sharedSyncUp)))
  return .none

Form Management

The app handles editing with draft state:
@ObservableState
struct State {
  @Presents var editSyncUp: SyncUpForm.State?
}

// Start editing
state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)

// Save changes
case .editSyncUp(.presented(.saveButtonTapped)):
  guard let editedSyncUp = state.editSyncUp?.syncUp
  else { return .none }
  state.syncUp = editedSyncUp
  state.editSyncUp = nil
  return .none

Architecture

The app is organized into focused features:
AppFeature (Root)
├── SyncUpsList
│   └── SyncUp rows
└── Path (Navigation)
    ├── SyncUpDetail
    │   ├── SyncUpForm (edit)
    │   └── Past meetings
    ├── RecordMeeting
    │   ├── Timer
    │   ├── Speech recognition
    │   └── Attendee tracking
    └── Meeting (past meeting view)
Each feature:
  • Has its own state and actions
  • Manages its own dependencies
  • Can be developed and tested independently

Testing

@Test
func testRecordMeeting() async {
  let clock = TestClock()
  let syncUp = SyncUp(
    id: SyncUp.ID(),
    attendees: [
      Attendee(id: Attendee.ID(), name: "Blob"),
      Attendee(id: Attendee.ID(), name: "Blob Jr"),
    ],
    duration: .seconds(6)
  )

  let store = TestStore(
    initialState: RecordMeeting.State(syncUp: Shared(syncUp))
  ) {
    RecordMeeting()
  } withDependencies: {
    $0.continuousClock = clock
    $0.speechRecognizer.recognizeAudioFile = { _ in "Hello" }
  }

  await store.send(.task)

  await clock.advance(by: .seconds(3))
  await store.receive(\.timerTicked) {
    $0.secondsElapsed = 3
    $0.speakerIndex = 1
  }

  await clock.advance(by: .seconds(3))
  await store.receive(\.timerTicked) {
    $0.secondsElapsed = 6
  }

  await store.receive(\.delegate.saveMeeting)
}

Source Code

View the complete example in the TCA repository:

Next Steps

Build docs developers (and LLMs) love