Skip to main content
The Composable Architecture was designed with SwiftUI in mind and provides seamless integration with SwiftUI’s declarative view system.

Store in SwiftUI Views

The primary way to integrate TCA with SwiftUI is through the Store type, which can be observed directly in your views.

Basic View Integration

Pass stores to your SwiftUI views and observe state changes:
struct FeatureView: View {
  let store: StoreOf<Feature>
  
  var body: some View {
    Form {
      Text(store.count.description)
      Button("Increment") {
        store.send(.incrementButtonTapped)
      }
    }
  }
}

Using @Bindable (iOS 17+)

For iOS 17 and later, use SwiftUI’s @Bindable to create bindings:
struct FeatureView: View {
  @Bindable var store: StoreOf<Feature>
  
  var body: some View {
    Form {
      TextField("Name", text: $store.name)
      Toggle("Is enabled", isOn: $store.isEnabled)
    }
  }
}

Using Perception.Bindable (iOS 13-16)

For pre-iOS 17, use the Perception framework’s @Bindable:
struct FeatureView: View {
  @Perception.Bindable var store: StoreOf<Feature>
  
  var body: some View {
    WithPerceptionTracking {
      Form {
        TextField("Name", text: $store.name)
        Toggle("Is enabled", isOn: $store.isEnabled)
      }
    }
  }
}
When using @Perception.Bindable, you must wrap your view body in WithPerceptionTracking to properly observe state changes.

Scoping Stores

Use the scope method to transform a store for child views:
struct AppView: View {
  let store: StoreOf<AppFeature>
  
  var body: some View {
    TabView {
      ActivityView(
        store: store.scope(state: \.activity, action: \.activity)
      )
      .tabItem { Text("Activity") }
      
      ProfileView(
        store: store.scope(state: \.profile, action: \.profile)
      )
      .tabItem { Text("Profile") }
    }
  }
}
Source: Store.swift:26-85

Bindings

Two-Way Bindings

TCA provides special support for SwiftUI bindings through the BindableAction protocol:
@Reducer
struct Feature {
  @ObservableState
  struct State {
    var email: String = ""
    var password: String = ""
  }
  
  enum Action: BindableAction {
    case binding(BindingAction<State>)
  }
  
  var body: some ReducerOf<Self> {
    BindingReducer()
    Reduce { state, action in
      // Your logic here
    }
  }
}
In your view:
struct LoginView: View {
  @Bindable var store: StoreOf<Feature>
  
  var body: some View {
    Form {
      TextField("Email", text: $store.email)
      SecureField("Password", text: $store.password)
    }
  }
}
Source: Binding.swift:84-101

Custom Bindings

Create custom bindings that send specific actions:
@Bindable var store: StoreOf<Feature>

TextField(
  "Query",
  text: $store.searchQuery.sending(\.searchQueryChanged)
)
Source: Binding+Observation.swift:271-279

Sheet Presentation

Use binding scopes for sheet presentation:
@Reducer
struct Feature {
  @ObservableState
  struct State {
    @Presents var destination: Destination.State?
  }
  
  enum Action {
    case destination(PresentationAction<Destination.Action>)
  }
}

struct FeatureView: View {
  @Bindable var store: StoreOf<Feature>
  
  var body: some View {
    Button("Show Details") {
      store.send(.showDetailButtonTapped)
    }
    .sheet(item: $store.scope(state: \.destination, action: \.destination)) { store in
      DestinationView(store: store)
    }
  }
}
Source: Store+Observation.swift:127-159

Alerts

Present alerts using store bindings:
struct FeatureView: View {
  @Bindable var store: StoreOf<Feature>
  
  var body: some View {
    Button("Delete") {
      store.send(.deleteButtonTapped)
    }
    .alert($store.scope(state: \.alert, action: \.alert))
  }
}
Source: Alert+Observation.swift:4-35

Confirmation Dialogs

Similar to alerts, confirmation dialogs use store bindings:
struct FeatureView: View {
  @Bindable var store: StoreOf<Feature>
  
  var body: some View {
    Button("Options") {
      store.send(.optionsButtonTapped)
    }
    .confirmationDialog($store.scope(state: \.dialog, action: \.dialog))
  }
}
Source: Alert+Observation.swift:38-72

Observable State

Mark your state with @ObservableState to enable observation:
@Reducer
struct Feature {
  @ObservableState
  struct State {
    var count: Int = 0
    var isLoading: Bool = false
  }
}
This allows SwiftUI views to automatically observe changes:
struct FeatureView: View {
  let store: StoreOf<Feature>
  
  var body: some View {
    // View automatically re-renders when count or isLoading changes
    if store.isLoading {
      ProgressView()
    } else {
      Text("Count: \(store.count)")
    }
  }
}
Source: Store+Observation.swift:11-25

Best Practices

Scope Early, Scope Often

Scope stores to the minimum state needed by each view:
// Good: Child view only sees its own state
ChildView(store: store.scope(state: \.child, action: \.child))

// Avoid: Passing entire store when only part is needed
ChildView(store: store)  // Don't do this

Use WithPerceptionTracking for iOS 13-16

When supporting pre-iOS 17, always wrap view bodies in WithPerceptionTracking:
struct FeatureView: View {
  let store: StoreOf<Feature>
  
  var body: some View {
    WithPerceptionTracking {
      // Your view code
    }
  }
}
Don’t forget to also wrap lazy closures like ForEach:
WithPerceptionTracking {
  ForEach(store.rows, id: \.id) { row in
    WithPerceptionTracking {
      RowView(row: row)
    }
  }
}
Source: ObservationBackport.md:85-116

Prefer Direct Store Access

With observable state, access store properties directly:
// Good: Direct access with observation
Text(store.title)

// Old style: No longer needed
WithViewStore(store, observe: { $0 }) { viewStore in
  Text(viewStore.title)
}

Build docs developers (and LLMs) love