Skip to content
Prepare to Grow

iOS Architecture

State Management and Unidirectional Data Flow

Model features as explicit state, actions, transitions, and effects so updates remain traceable and testable.

Advanced7 min read2 guided activities

Before you begin

What you will understand

  • Identify durable, presentation, and transient UI state.
  • Create one source of truth for each value.
  • Model actions and pure state transitions.
  • Keep asynchronous effects outside deterministic reducers.
01

Learning stage

Concept Explanation

State is information that can change and affects behavior or rendering. Durable domain state may outlive a screen, presentation state describes a feature workflow, and ephemeral state such as focus or an animation phase may remain local to a view. Promote state only when another owner genuinely needs it.

Unidirectional flow follows a repeatable loop: the user or system emits an action, a transition computes new state, effects interact with the outside world, and the view renders the resulting state. One mutation path makes changes easier to trace than several views and callbacks modifying shared values independently.

A reducer is useful when transitions are complex enough to benefit from pure input-output logic. It should not start network work secretly. Instead, the store interprets commands or launches effects through injected dependencies and feeds results back as actions. Cancellation, stale responses, duplicate loads, and main-actor rendering remain explicit parts of the design.

  • Source of truth: authoritative owner for a piece of state.
  • Action: event describing user or system intent.
  • Reducer: deterministic state transition from state and action.
  • Effect: asynchronous or external work that may emit more actions.
  • Derived state: value calculated from authoritative state.
  • Cancellation identity: key used to replace or stop in-flight work.
02

Learning stage

Syntax & Examples

A small store keeps transitions synchronous and sends asynchronous results back through actions.

CourseListStore.swiftswift
@MainActor @Observable
final class CourseListStore {
  struct State: Equatable {
    var courses: [Course] = []
    var isLoading = false
    var errorMessage: String?
  }

  enum Action: Equatable {
    case appeared
    case retryTapped
    case coursesResponse(Result<[Course], CourseLoadError>)
  }

  private let repository: any CourseRepository
  private var loadTask: Task<Void, Never>?
  private(set) var state = State()

  init(repository: any CourseRepository) { self.repository = repository }

  func send(_ action: Action) {
    switch action {
    case .appeared where state.courses.isEmpty, .retryTapped:
      state.isLoading = true
      state.errorMessage = nil
      loadTask?.cancel()
      loadTask = Task { [weak self, repository] in
        do {
          let courses = try await repository.courses(policy: .remoteOnly)
          self?.send(.coursesResponse(.success(courses)))
        } catch is CancellationError {
          return
        } catch {
          self?.send(.coursesResponse(.failure(.unavailable)))
        }
      }
    case let .coursesResponse(.success(courses)):
      state.isLoading = false
      state.courses = courses
    case .coursesResponse(.failure):
      state.isLoading = false
      state.errorMessage = "Courses are unavailable."
    default:
      break
    }
  }
}
Actions are the only mutation entry point, and the loading effect reports its result through another action.
03

Learning stage

Real-World Usage

Connect the architectural concept to maintainable iOS features and real change scenarios.

  • Trace how a button tap becomes a request and then rendered content.
  • Test state transitions without a network or UI framework.
  • Cancel an earlier search when the query changes.
  • Prevent multiple screens from competing to own shared progress.
04

Learning stage

Common Mistakes

Identify abstractions and dependencies that add coupling, ambiguity, or unnecessary ceremony.

  • Moving all application state into one global store.
  • Duplicating derived values and trying to synchronize them manually.
  • Performing hidden side effects inside a pure reducer.
  • Turning every local toggle or focus value into global state.
  • Accepting stale asynchronous results after a newer request has started.
05

Learning stage

Practice Exercises

Apply the design to a small feature and evaluate it through observable change scenarios.

Activity 1

Create a state inventory

Classify course search values as durable domain, feature presentation, derived, or ephemeral UI state. Assign an owner and lifetime to each.

Hint: Search results may be feature state while field focus can remain local.

Activity 2

Test action sequences

Write tests for appeared → success, appeared → failure → retry, duplicate appearance, and cancellation by a newer request.

Hint: Assert each intermediate state, not only the final result.

06

Learning stage

Knowledge Check

Answer each question before revealing the explanation.

QUESTION 1

What is the central rule of unidirectional data flow?

  1. A.Actions enter through a known path, update state, and the UI renders that state
  2. B.Every view mutates shared objects directly
  3. C.Reducers perform arbitrary I/O
  4. D.All state must be global
Reveal answer

Actions enter through a known path, update state, and the UI renders that state

A predictable mutation path makes behavior traceable and tests easier to write.

QUESTION 2

Why avoid storing a value that can be cheaply derived from authoritative state?

Reveal answer

A duplicate stored value can drift out of sync and creates another mutation responsibility.

Compute derived values unless performance evidence justifies a controlled cache.

07

Learning stage

Interview Preparation

Explain the decision, dependency direction, trade-offs, and evidence—not only the pattern name.

What are the benefits and costs of unidirectional data flow?

Short answer: It makes state changes explicit and testable, but adds action and transition structure that small features may not need.

Explore the detailed answer

I use one state owner and one mutation path when workflows have asynchronous effects, several states, or debugging pressure. Actions describe events, transitions update state, and effects return results as actions. I keep local UI state local and avoid a single global store. The trade-off is more vocabulary and boilerplate, so I scale the pattern to feature complexity.

Possible follow-ups

  • • How do you handle cancellation?
  • • What state should stay inside a view?
08

Learning stage

Key Takeaways

Unidirectional flow makes feature behavior observable: actions describe events, one owner transitions state, effects report results, and views render the outcome.

  • Give every value one authoritative owner.
  • Keep ephemeral UI state local when possible.
  • Use actions as a visible mutation path.
  • Separate deterministic transitions from effects.
  • Model cancellation and stale responses deliberately.