Skip to content

Redux Connectors (SwiftUI)

A SwiftUI guide for coding agents. Also covers redux connector pattern, view state mapping, derived store, single source of truth swiftui, testable view formatting, unidirectional data flow ios.

In a single-store app, a view that reads Store<AppState, AppAction> directly drags the whole app state into itself: formatting logic hides in the body, previews need a full store, and the view cannot move to its own Swift Package. A Connector maps app state to a small view state, and view actions back to app actions.

The Connector Protocol

Two pure functions: state in, view state out; view action in, app action out. connect(using:) wraps the store's existing derived mechanism.

protocol Connector {
  associatedtype State
  associatedtype Action
  associatedtype ViewState: Equatable
  associatedtype ViewAction: Equatable

  func connect(state: State) -> ViewState
  func connect(action: ViewAction) -> Action
}

extension Store {
  func connect<C: Connector>(
    using connector: C
  ) -> Store<C.ViewState, C.ViewAction> where C.State == State, C.Action == Action {
    derived(
      deriveState: connector.connect(state:),
      embedAction: connector.connect(action:)
    )
  }
}

View State Owned by the View

The view declares exactly the data it renders and the only actions it can send. Wrong-action bugs disappear, and the view no longer knows the app state exists.

extension HistoryView {
  struct State: Equatable {
    let posters: [Poster]

    struct Poster: Hashable {
      let ids: Ids
      let imageURL: URL?
      let title: String
      let subtitle: String
    }
  }

  enum Action: Equatable {
    case markAsWatched(episode: Ids)
  }

  typealias ViewModel = Store<State, Action>
}

The body becomes dumb rendering plus viewModel.send(...). Previews stub the store with a reducer that does nothing, so covering loading and empty states is cheap.

The Connector Does the Formatting

All lookup and formatting moves into a value type, which unit tests can call directly.

struct WatchedHistoryConnector: Connector {
  func connect(state: AppState) -> HistoryView.State {
    .init(posters: state.watchedHistory.compactMap { ids in
      let episode = state.episodes[ids]
      return HistoryView.State.Poster(
        ids: ids,
        imageURL: state.showImages[ids]?.tvPosters?.first?.url,
        title: episode?.title ?? "",
        subtitle: DateFormatter.shortDate.string(for: episode?.firstAired) ?? ""
      )
    })
  }

  func connect(action: HistoryView.Action) -> AppAction {
    switch action {
    case let .markAsWatched(episode):
      return AppAction.markAsWatched(episode: episode, watched: true)
    }
  }
}

A container view binds the two: HistoryView(viewModel: store.connect(using: Connectors.WatchedHistoryConnector())). The pattern comes from Redux and The Composable Architecture; part 4 of the series.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-redux-like-state-container-part4" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems