Skip to main content

Overview

The @ObservableState macro implements SwiftUI’s Observable protocol for your reducer’s state, enabling efficient view updates when state changes. This is the recommended way to make your feature’s state observable in SwiftUI. The macro generates:
  • Conformance to Swift’s Observable and TCA’s ObservableState protocols
  • Internal observation tracking infrastructure (_$observationRegistrar, _$id, _$willModify)
  • Automatic property observation for all stored properties
  • Integration with TCA’s state change notification system

Basic Usage

Apply @ObservableState to your reducer’s State type:
@Reducer
struct Feature {
  @ObservableState
  struct State {
    var count = 0
    var message = "Hello"
    var isLoading = false
  }
  
  enum Action {
    case incrementTapped
    case setMessage(String)
  }
  
  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .incrementTapped:
        state.count += 1
        return .none
        
      case .setMessage(let text):
        state.message = text
        return .none
      }
    }
  }
}
In your SwiftUI view, use @Bindable to create bindings:
struct FeatureView: View {
  @Bindable var store: StoreOf<Feature>
  
  var body: some View {
    VStack {
      Text("Count: \(store.count)")
      Button("Increment") {
        store.send(.incrementTapped)
      }
      
      TextField("Message", text: $store.message.sending(\.setMessage))
    }
  }
}

Why Use @ObservableState?

Without @ObservableState, you need to manually observe the store using observe in your views, which can be verbose and error-prone. The macro provides:
  • Automatic observation: Views automatically update when observed state changes
  • Fine-grained updates: Only views observing changed properties re-render
  • SwiftUI integration: Works seamlessly with SwiftUI’s observation system
  • Type safety: Compile-time guarantees about observable properties

Controlling Observation

You can control which properties are observed using helper macros:

@ObservationStateTracked

Explicitly marks a property as tracked (this is the default for all stored properties):
@ObservableState
struct State {
  @ObservationStateTracked
  var importantValue = 0
  
  var count = 0  // Also tracked by default
}

@ObservationStateIgnored

Marks a property to ignore for observation. Use this for properties that shouldn’t trigger view updates:
@ObservableState
struct State {
  var displayedCount = 0
  
  @ObservationStateIgnored
  var internalCache: [String: Any] = [:]
  
  @ObservationStateIgnored
  var debugInfo = ""
}
Views observing this state won’t update when internalCache or debugInfo change, only when displayedCount changes.

Working with Nested State

When composing features, apply @ObservableState to each feature’s state:
@Reducer
struct ParentFeature {
  @ObservableState
  struct State {
    var child = ChildFeature.State()
    var title = "Parent"
  }
  
  enum Action {
    case child(ChildFeature.Action)
  }
  
  var body: some ReducerOf<Self> {
    Scope(state: \.child, action: \.child) {
      ChildFeature()
    }
    Reduce { state, action in
      // Parent logic
      return .none
    }
  }
}

@Reducer
struct ChildFeature {
  @ObservableState
  struct State {
    var value = 0
  }
  
  enum Action {
    case increment
  }
  
  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .increment:
        state.value += 1
        return .none
      }
    }
  }
}

Using with @Presents

For presented features, use the @Presents macro instead of the PresentationState property wrapper:
@Reducer
struct Feature {
  @ObservableState
  struct State {
    @Presents var destination: Destination.State?
    var items: [Item] = []
  }
  
  enum Action {
    case destination(PresentationAction<Destination.Action>)
    case addButtonTapped
  }
  
  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .addButtonTapped:
        state.destination = .addItem(AddItemFeature.State())
        return .none
        
      case .destination:
        return .none
      }
    }
    .ifLet(\.$destination, action: \.destination)
  }
  
  @Reducer
  enum Destination {
    case addItem(AddItemFeature)
    case editItem(EditItemFeature)
  }
}
The @Presents macro is required when using @ObservableState because property wrappers like PresentationState are incompatible with Swift’s observation system.

Collections and Identified Arrays

The macro works seamlessly with collections:
@ObservableState
struct State {
  var items: IdentifiedArrayOf<Item> = []
  var selectedIDs: Set<Item.ID> = []
  var searchText = ""
}

struct Item: Identifiable {
  let id: UUID
  var name: String
  var isComplete: Bool
}
Views observing items will update when the array changes.

Performance Considerations

The @ObservableState macro is designed for performance:
  • Structural identity: Each state value has a unique identifier for efficient change detection
  • Willset notifications: Observers are notified before mutations occur
  • Minimal overhead: Only properties actually accessed by views are tracked

Migration from ViewStore

If you’re migrating from the older ViewStore pattern: Before:
struct FeatureView: View {
  let store: StoreOf<Feature>
  
  var body: some View {
    WithViewStore(store, observe: { $0 }) { viewStore in
      Text("Count: \(viewStore.count)")
      Button("Increment") {
        viewStore.send(.incrementTapped)
      }
    }
  }
}
After:
struct FeatureView: View {
  @Bindable var store: StoreOf<Feature>
  
  var body: some View {
    Text("Count: \(store.count)")
    Button("Increment") {
      store.send(.incrementTapped)
    }
  }
}

See Also

Build docs developers (and LLMs) love