learn()
{ start }
build++
const tutorial

Modern iOS App Architecture - Part 2: State Management & Navigation

Learn advanced state management patterns, navigation coordination, and data flow in modern iOS applications with SwiftUI and Combine.

Cuppa Team4 min read
iosswiftswiftuistate-managementnavigationcombine

In Part 2, we explore state management patterns and navigation strategies for building robust iOS applications.

State Management Patterns

1. ObservableObject Pattern

The primary pattern for managing state in SwiftUI.

@MainActor
final class AppState: ObservableObject {
    @Published var user: User?
    @Published var isAuthenticated = false
    @Published var settings: AppSettings

    init() {
        self.settings = AppSettings()
    }

    func signIn(_ user: User) {
        self.user = user
        self.isAuthenticated = true
    }

    func signOut() {
        self.user = nil
        self.isAuthenticated = false
    }
}

2. @StateObject vs @ObservedObject

struct RootView: View {
    // ✅ Create and own the state
    @StateObject private var appState = AppState()

    var body: some View {
        ChildView(appState: appState)
    }
}

struct ChildView: View {
    // ✅ Observe existing state
    @ObservedObject var appState: AppState

    var body: some View {
        Text("User: \(appState.user?.name ?? "Guest")")
    }
}

3. Environment Objects

Share state across the view hierarchy:

@main
struct MyApp: App {
    @StateObject private var appState = AppState()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appState)
        }
    }
}

struct ProfileView: View {
    @EnvironmentObject private var appState: AppState

    var body: some View {
        Text("Hello, \(appState.user?.name ?? "Guest")")
    }
}

Navigation Coordination

AppCoordinator Pattern

Centralize navigation logic:

@MainActor
final class AppCoordinator: ObservableObject {
    enum AppState {
        case loading
        case onboarding
        case authentication
        case authenticated(userId: String)
    }

    @Published private(set) var state: AppState = .loading

    func start() async {
        // Check authentication status
        if let userId = await checkAuthentication() {
            state = .authenticated(userId: userId)
        } else if hasCompletedOnboarding() {
            state = .authentication
        } else {
            state = .onboarding
        }
    }

    func completeOnboarding() {
        state = .authentication
    }

    func handleAuthentication(userId: String) {
        state = .authenticated(userId: userId)
    }
}

NavigationStack with Paths

@MainActor
final class NavigationCoordinator: ObservableObject {
    @Published var path = NavigationPath()

    func navigate(to route: Route) {
        path.append(route)
    }

    func goBack() {
        guard !path.isEmpty else { return }
        path.removeLast()
    }

    func popToRoot() {
        path = NavigationPath()
    }
}

enum Route: Hashable {
    case profile(userId: String)
    case settings
    case detail(itemId: String)
}

Usage in Views

struct ContentView: View {
    @StateObject private var coordinator = NavigationCoordinator()

    var body: some View {
        NavigationStack(path: $coordinator.path) {
            HomeView()
                .navigationDestination(for: Route.self) { route in
                    destination(for: route)
                }
        }
        .environmentObject(coordinator)
    }

    @ViewBuilder
    private func destination(for route: Route) -> some View {
        switch route {
        case .profile(let userId):
            ProfileView(userId: userId)
        case .settings:
            SettingsView()
        case .detail(let itemId):
            DetailView(itemId: itemId)
        }
    }
}

Unidirectional Data Flow

ViewIntent Pattern

enum ProfileIntent {
    case loadProfile
    case updateName(String)
    case updateAvatar(URL)
    case deleteAccount
}

@MainActor
final class ProfileViewModel: ObservableObject {
    @Published private(set) var profile: UserProfile?
    @Published private(set) var isLoading = false

    func send(_ intent: ProfileIntent) async {
        switch intent {
        case .loadProfile:
            await loadProfile()
        case .updateName(let name):
            await updateName(name)
        case .updateAvatar(let url):
            await updateAvatar(url)
        case .deleteAccount:
            await deleteAccount()
        }
    }

    private func loadProfile() async {
        isLoading = true
        defer { isLoading = false }
        // Implementation
    }
}

Usage

struct ProfileView: View {
    @StateObject private var viewModel = ProfileViewModel()

    var body: some View {
        VStack {
            if let profile = viewModel.profile {
                ProfileContent(profile: profile)
            }
        }
        .task {
            await viewModel.send(.loadProfile)
        }
    }
}

Error Handling

Centralized Error Handling

enum AppError: LocalizedError {
    case network(Error)
    case authentication
    case notFound
    case unknown(Error)

    var errorDescription: String? {
        switch self {
        case .network(let error):
            return "Network error: \(error.localizedDescription)"
        case .authentication:
            return "Please sign in again"
        case .notFound:
            return "Resource not found"
        case .unknown(let error):
            return "An error occurred: \(error.localizedDescription)"
        }
    }
}

@MainActor
final class ErrorHandler: ObservableObject {
    @Published var currentError: AppError?

    func handle(_ error: Error) {
        if let appError = error as? AppError {
            currentError = appError
        } else {
            currentError = .unknown(error)
        }
    }
}

Performance Optimization

1. Lazy Loading

struct ListView: View {
    let items: [Item]

    var body: some View {
        LazyVStack {
            ForEach(items) { item in
                ItemRow(item: item)
            }
        }
    }
}

2. Task Cancellation

@MainActor
final class DataViewModel: ObservableObject {
    private var loadTask: Task<Void, Never>?

    func loadData() {
        loadTask?.cancel()
        loadTask = Task {
            do {
                let data = try await fetchData()
                self.data = data
            } catch is CancellationError {
                return
            } catch {
                self.error = error
            }
        }
    }
}

Next Steps

In Part 3, we'll cover:

  • Testing strategies
  • Dependency injection patterns
  • Performance monitoring
  • Real-world examples

Resources