Skip to main content

Overview

StackState and StackAction are the core types for implementing stack-based navigation in TCA. StackState is a collection that holds the state of all screens in a navigation stack, while StackAction represents actions that can be sent to or from stack elements.

StackState

Declaration

public struct StackState<Element>
A list of data representing the content of a navigation stack.

Usage

Use StackState to model navigation stack state in your feature:
@Reducer
struct AppFeature {
  @Reducer
  enum Path {
    case detail(DetailFeature)
    case settings(SettingsFeature)
  }
  
  @ObservableState
  struct State {
    var path = StackState<Path.State>()
  }
  
  enum Action {
    case path(StackActionOf<Path>)
  }
  
  var body: some ReducerOf<Self> {
    Reduce { state, action in
      // Core logic
    }
    .forEach(\.path, action: \.path)
  }
}

Properties

ids

public var ids: OrderedSet<StackElementID>
An ordered set of identifiers, one for each stack element. You can use this to iterate over elements with their IDs:
for (id, element) in zip(state.path.ids, state.path) {
  if element.isDeleted {
    state.path.pop(from: id)
    break
  }
}

Subscripts

Access by ID

public subscript(id id: StackElementID) -> Element?
Access the value associated with a given ID:
if var element = state.path[id: elementID] {
  // Modify element
  state.path[id: elementID] = element
}

Access by Case

public subscript<Case>(
  id id: StackElementID,
  case path: CaseKeyPath<Element, Case>
) -> Case?
Access a specific case of an enum element:
state.path[id: 0, case: \.edit]?.alert = AlertState {
  Text("Delete?")
}

Methods

pop(from:)

public mutating func pop(from id: StackElementID)
Pops the element corresponding to id from the stack, and all elements after it:
state.path.pop(from: elementID)

pop(to:)

public mutating func pop(to id: StackElementID)
Pops all elements that come after the element corresponding to id:
state.path.pop(to: elementID)

Collection Operations

StackState conforms to RandomAccessCollection and RangeReplaceableCollection, so you can use standard collection operations:
// Append elements
state.path.append(.detail(DetailFeature.State()))

// Remove all
state.path.removeAll()

// Count elements
let count = state.path.count

// Iterate
for element in state.path {
  // Process element
}

StackAction

Declaration

public enum StackAction<State, Action>
A wrapper type for actions that can be presented in a navigation stack.

Cases

element

case element(id: StackElementID, action: Action)
An action sent to the associated stack element at a given identifier:
case .path(.element(id: elementID, action: .incrementButtonTapped)):
  // Child handled the action
  return .none

popFrom

case popFrom(id: StackElementID)
An action sent to dismiss the stack element at the given identifier:
case .path(.popFrom(id: elementID)):
  // Element was popped
  return .none

push

case push(id: StackElementID, state: State)
An action sent to present the given state at a given identifier. This is typically sent automatically from the view via NavigationLink(state:).

Usage Example

@Reducer
struct NavigationDemo {
  @Reducer
  enum Path {
    case screenA(ScreenA)
    case screenB(ScreenB)
  }
  
  @ObservableState
  struct State {
    var path = StackState<Path.State>()
  }
  
  enum Action {
    case path(StackActionOf<Path>)
    case popToRoot
  }
  
  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .path(.element(id: _, action: .screenA(.nextButtonTapped))):
        state.path.append(.screenB(ScreenB.State()))
        return .none
        
      case .popToRoot:
        state.path.removeAll()
        return .none
        
      case .path:
        return .none
      }
    }
    .forEach(\.path, action: \.path)
  }
}

StackElementID

Declaration

public struct StackElementID: Hashable, Sendable
An opaque identifier for stack elements. IDs are automatically generated when elements are added to the stack.

Testing with IDs

In tests, you can use integer literals to reference stack element IDs:
@Test
func basics() {
  var path = StackState<Int>()
  path.append(42)
  XCTAssertEqual(path[id: 0], 42)
  path.append(1729)
  XCTAssertEqual(path[id: 1], 1729)
  
  path.removeAll()
  path.append(-1)
  XCTAssertEqual(path[id: 2], -1)  // Generational ID
}
Note that IDs are generational in tests - they keep counting up even after elements are removed.

StackActionOf

Type Alias

public typealias StackActionOf<R: Reducer> = StackAction<R.State, R.Action>
A convenience type alias for referring to a stack action of a given reducer’s domain:
// Instead of:
case path(StackAction<Path.State, Path.Action>)

// You can write:
case path(StackActionOf<Path>)

Key Points

  • Type-Safe Navigation: StackState enforces type safety for all navigation operations
  • Automatic Cleanup: Effects are automatically cancelled when elements are popped from the stack
  • Deterministic IDs: Stack element IDs are predictable in tests for easier testing
  • Parent Intercepts: The parent reducer can intercept and respond to child actions before they’re processed
  • Deep Linking: You can programmatically build deep navigation stacks by appending multiple elements
  • NavigationLink(state:) - SwiftUI view for triggering navigation
  • forEach - Reducer operator for integrating stack state
  • @Presents - Macro for tree-based navigation
  • PresentationState - Type for optional navigation state

See Also

Build docs developers (and LLMs) love