Skip to content
All essays
iOSMarch 27, 202512 min

SwiftUI State Management: Advanced Patterns

Master state management in SwiftUI. Environment, dependency injection, and reactive patterns.

Ü
Ümit Uz
Mobile & Full Stack Developer

Effective state management is key to SwiftUI apps. Learn advanced patterns for complex applications.

State Management Patterns

Local State

swift
struct CounterView: View {
  @State private var count = 0

  var body: some View {
    VStack {
      Text("Count: \(count)")
      Button("Increment") {
        count += 1
      }
    }
  }
}

Shared State

swift
class AppState: ObservableObject {
  @Published var isLoggedIn = false
  @Published var currentUser: User?
  @Published var theme: Theme = .light

  func login(_ user: User) {
    currentUser = user
    isLoggedIn = true
  }

  func logout() {
    currentUser = nil
    isLoggedIn = false
  }

  func toggleTheme() {
    theme = theme == .light ? .dark : .light
  }
}

struct RootView: View {
  @StateObject private var state = AppState()

  var body: some View {
    ContentView()
      .environmentObject(state)
  }
}

struct ContentView: View {
  @EnvironmentObject private var state: AppState

  var body: some View {
    VStack {
      if state.isLoggedIn {
        Text("Welcome, \(state.currentUser?.name ?? "")")
        Button("Logout") {
          state.logout()
        }
      } else {
        LoginView()
      }
    }
  }
}

Dependency Injection

Environment Keys

swift
struct APIKey: EnvironmentKey {
  static var defaultValue: APIService = APIService()
}

struct DatabaseKey: EnvironmentKey {
  static var defaultValue: DatabaseService = DatabaseService()
}

extension EnvironmentValues {
  var api: APIService {
    get { self[APIKey.self] }
    set { self[APIKey.self] = newValue }
  }

  var database: DatabaseService {
    get { self[DatabaseKey.self] }
    set { self[DatabaseKey.self] = newValue }
  }
}

// Usage
struct ContentView: View {
  @Environment(.api) private var api
  @Environment(.database) private var database

  var body: some View {
    VStack {
      Text("Content")
    }
    .task {
      await fetchData()
    }
  }

  func fetchData() async {
    let data = try? await api.fetchData()
    // Use data
  }
}

Service Injection

swift
protocol DataService {
  func fetchItems() async throws -> [Item]
}

class APIDataService: DataService {
  func fetchItems() async throws -> [Item] {
    // API call
    return []
  }
}

class MockDataService: DataService {
  func fetchItems() async throws -> [Item] {
    return [Item(id: "1", name: "Mock")]
  }
}

struct ItemsView: View {
  @State private var items: [Item] = []
  let service: DataService

  var body: some View {
    List(items) { item in
      Text(item.name)
    }
    .task {
      try? await loadItems()
    }
  }

  func loadItems() async throws {
    items = try await service.fetchItems()
  }
}

// Usage
ItemsView(service: APIDataService())

Complex State

State Objects

swift
class FormState: ObservableObject {
  @Published var username = ""
  @Published var email = ""
  @Published var password = ""
  @Published var isLoading = false
  @Published var errorMessage: String?

  var isValid: Bool {
    !username.isEmpty && !email.isEmpty && !password.isEmpty
  }

  var isEmailValid: Bool {
    email.contains("@")
  }

  func reset() {
    username = ""
    email = ""
    password = ""
    errorMessage = nil
  }
}

struct RegistrationView: View {
  @StateObject private var formState = FormState()

  var body: some View {
    Form {
      TextField("Username", text: $formState.username)
      TextField("Email", text: $formState.email)
      SecureField("Password", text: $formState.password)

      if let error = formState.errorMessage {
        Text(error)
          .foregroundColor(.red)
      }

      Button("Register") {
        Task {
          await register()
        }
      }
      .disabled(!formState.isValid || formState.isLoading)
    }
  }

  func register() async {
    formState.isLoading = true
    defer { formState.isLoading = false }

    do {
      try await APIService().register(
        username: formState.username,
        email: formState.email,
        password: formState.password
      )
    } catch {
      formState.errorMessage = error.localizedDescription
    }
  }
}
swift
class NavigationState: ObservableObject {
  @Published var path = NavigationPath()

  func push(_ screen: Screen) {
    path.append(screen)
  }

  func pop() {
    if !path.isEmpty {
      path.removeLast()
    }
  }

  func popToRoot() {
    path.removeLast(path.count)
  }
}

enum Screen: Hashable {
  case home
  case profile
  case settings
  case detail(id: String)
}

struct AppNavigationView: View {
  @StateObject private var navState = NavigationState()

  var body: some View {
    NavigationStack(path: $navState.path) {
      HomeView()
        .navigationDestination(for: Screen.self) { screen in
          switch screen {
          case .home:
            HomeView()
          case .profile:
            ProfileView()
          case .settings:
            SettingsView()
          case .detail(let id):
            DetailView(id: id)
          }
        }
    }
    .environmentObject(navState)
  }
}

Reactive Patterns

Combine Integration

swift
import Combine

class SearchViewModel: ObservableObject {
  @Published var searchQuery = ""
  @Published var results: [SearchResult] = []
  @Published var isSearching = false

  private var cancellables = Set<AnyCancellable>()
  private let service = SearchService()

  init() {
    setupSearchPipeline()
  }

  private func setupSearchPipeline() {
    $searchQuery
      .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
      .removeDuplicates()
      .flatMap { query in
        self.search(query)
      }
      .sink { [weak self] results in
        self?.results = results
        self?.isSearching = false
      }
      .store(in: &cancellables)
  }

  private func search(_ query: String) -> AnyPublisher<[SearchResult], Never> {
    guard !query.isEmpty else {
      return Just([]).eraseToAnyPublisher()
    }

    isSearching = true

    return service.search(query)
      .replaceError(with: [])
      .eraseToAnyPublisher()
  }
}

TCA Pattern

swift
import ComposableArchitecture

struct CounterState: Equatable {
  var count = 0
}

enum CounterAction: Equatable {
  case increment
  case decrement
  case reset
}

struct CounterEnvironment {
  var analytics: AnalyticsService
}

let counterReducer = Reducer<
  CounterState,
  CounterAction,
  CounterEnvironment
> { state, action, environment in
  switch action {
  case .increment:
    state.count += 1
    environment.analytics.track("counter_increment")
    return .none

  case .decrement:
    state.count -= 1
    environment.analytics.track("counter_decrement")
    return .none

  case .reset:
    state.count = 0
    environment.analytics.track("counter_reset")
    return .none
  }
}

struct CounterView: View {
  let store: Store<CounterState, CounterAction>

  var body: some View {
    WithViewStore(store) { viewStore in
      HStack {
        Button("-") {
          viewStore.send(.decrement)
        }

        Text("\(viewStore.count)")

        Button("+") {
          viewStore.send(.increment)
        }

        Button("Reset") {
          viewStore.send(.reset)
        }
      }
    }
  }
}

Persistence

State Restoration

swift
class PersistentState: ObservableObject {
  @Published var items: [Item] {
    didSet {
      saveItems()
    }
  }

  private let saveKey = "saved_items"

  init() {
    self.items = loadItems()
  }

  private func loadItems() -> [Item] {
    guard let data = UserDefaults.standard.data(forKey: saveKey),
          let items = try? JSONDecoder().decode([Item].self, from: data) else {
      return []
    }
    return items
  }

  private func saveItems() {
    guard let data = try? JSONEncoder().encode(items) else { return }
    UserDefaults.standard.set(data, forKey: saveKey)
  }

  func addItem(_ item: Item) {
    items.append(item)
  }

  func removeItem(at index: Int) {
    items.remove(at: index)
  }
}

Best Practices

  1. 1Single Source: Keep single source of truth
  2. 2Immutable Data: Use immutable state when possible
  3. 3Derived State: Compute derived state
  4. 4Isolation: Isolate state concerns
  5. 5Testing: Test state management
  6. 6Performance: Optimize state updates
  7. 7Simplicity: Keep it simple
  8. 8Documentation: Document state flow

Master state management for better apps!

Related essays

Next essay
Swift Testing Advanced: Mocking, Spies, and Fakes