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 Team•4 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