# App Architecture State management, dependency injection, and architectural patterns for iOS apps. ## State Management ### @Observable (iOS 17+) The modern approach for shared state: ```swift @Observable class AppState { var items: [Item] = [] var selectedItemID: UUID? var isLoading = false var error: AppError? // Computed properties work naturally var selectedItem: Item? { items.first { $0.id == selectedItemID } } var hasItems: Bool { !items.isEmpty } } // In views - only re-renders when used properties change struct ContentView: View { @Environment(AppState.self) private var appState var body: some View { if appState.isLoading { ProgressView() } else { ItemList(items: appState.items) } } } ``` ### Two-Way Bindings For binding to @Observable properties: ```swift struct SettingsView: View { @Environment(AppState.self) private var appState var body: some View { @Bindable var appState = appState Form { TextField("Username", text: $appState.username) Toggle("Notifications", isOn: $appState.notificationsEnabled) } } } ``` ### State Decision Tree **@State** - View-local UI state - Toggle expanded/collapsed - Text field content - Sheet presentation ```swift struct ItemRow: View { @State private var isExpanded = false var body: some View { VStack { // ... } } } ``` **@Observable in Environment** - Shared app state - User session - Navigation state - Feature flags ```swift @main struct MyApp: App { @State private var appState = AppState() var body: some Scene { WindowGroup { ContentView() .environment(appState) } } } ``` **@Query** - SwiftData persistence - Database entities - Filtered/sorted queries ```swift struct ItemList: View { @Query(sort: \Item.createdAt, order: .reverse) private var items: [Item] var body: some View { List(items) { item in ItemRow(item: item) } } } ``` ## Dependency Injection ### Environment Keys Define environment keys for testable dependencies: ```swift // Protocol for testability protocol NetworkServiceProtocol { func fetch(_ endpoint: Endpoint) async throws -> T } // Live implementation class LiveNetworkService: NetworkServiceProtocol { func fetch(_ endpoint: Endpoint) async throws -> T { // Real implementation } } // Mock for testing class MockNetworkService: NetworkServiceProtocol { var mockResult: Any? var mockError: Error? func fetch(_ endpoint: Endpoint) async throws -> T { if let error = mockError { throw error } return mockResult as! T } } // Environment key struct NetworkServiceKey: EnvironmentKey { static let defaultValue: NetworkServiceProtocol = LiveNetworkService() } extension EnvironmentValues { var networkService: NetworkServiceProtocol { get { self[NetworkServiceKey.self] } set { self[NetworkServiceKey.self] = newValue } } } // Inject at app level @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .environment(\.networkService, LiveNetworkService()) } } } // Use in views struct ItemList: View { @Environment(\.networkService) private var networkService var body: some View { // ... } func loadItems() async { let items: [Item] = try await networkService.fetch(.items) } } ``` ### Dependency Container For complex apps with many dependencies: ```swift @Observable class AppDependencies { let network: NetworkServiceProtocol let storage: StorageServiceProtocol let purchases: PurchaseServiceProtocol let analytics: AnalyticsServiceProtocol init( network: NetworkServiceProtocol = LiveNetworkService(), storage: StorageServiceProtocol = LiveStorageService(), purchases: PurchaseServiceProtocol = LivePurchaseService(), analytics: AnalyticsServiceProtocol = LiveAnalyticsService() ) { self.network = network self.storage = storage self.purchases = purchases self.analytics = analytics } // Convenience for testing static func mock() -> AppDependencies { AppDependencies( network: MockNetworkService(), storage: MockStorageService(), purchases: MockPurchaseService(), analytics: MockAnalyticsService() ) } } // Inject as single environment object @main struct MyApp: App { @State private var dependencies = AppDependencies() var body: some Scene { WindowGroup { ContentView() .environment(dependencies) } } } ``` ## View Models (When Needed) For views with significant logic, use a view-local model: ```swift struct ItemDetailScreen: View { let itemID: UUID @State private var viewModel: ItemDetailViewModel init(itemID: UUID) { self.itemID = itemID self._viewModel = State(initialValue: ItemDetailViewModel(itemID: itemID)) } var body: some View { Form { if viewModel.isLoading { ProgressView() } else if let item = viewModel.item { ItemContent(item: item) } } .task { await viewModel.load() } } } @Observable class ItemDetailViewModel { let itemID: UUID var item: Item? var isLoading = false var error: Error? init(itemID: UUID) { self.itemID = itemID } func load() async { isLoading = true defer { isLoading = false } do { item = try await fetchItem(id: itemID) } catch { self.error = error } } func save() async { // Save logic } } ``` ## Coordinator Pattern For complex navigation flows: ```swift @Observable class OnboardingCoordinator { var currentStep: OnboardingStep = .welcome var isComplete = false enum OnboardingStep { case welcome case permissions case personalInfo case complete } func next() { switch currentStep { case .welcome: currentStep = .permissions case .permissions: currentStep = .personalInfo case .personalInfo: currentStep = .complete isComplete = true case .complete: break } } func back() { switch currentStep { case .welcome: break case .permissions: currentStep = .welcome case .personalInfo: currentStep = .permissions case .complete: currentStep = .personalInfo } } } struct OnboardingFlow: View { @State private var coordinator = OnboardingCoordinator() var body: some View { Group { switch coordinator.currentStep { case .welcome: WelcomeView(onContinue: coordinator.next) case .permissions: PermissionsView(onContinue: coordinator.next, onBack: coordinator.back) case .personalInfo: PersonalInfoView(onContinue: coordinator.next, onBack: coordinator.back) case .complete: CompletionView() } } .animation(.default, value: coordinator.currentStep) } } ``` ## Error Handling ### Structured Error Types ```swift enum AppError: LocalizedError { case networkError(NetworkError) case storageError(StorageError) case validationError(String) case unauthorized case unknown(Error) var errorDescription: String? { switch self { case .networkError(let error): return error.localizedDescription case .storageError(let error): return error.localizedDescription case .validationError(let message): return message case .unauthorized: return "Please sign in to continue" case .unknown(let error): return error.localizedDescription } } var recoverySuggestion: String? { switch self { case .networkError: return "Check your internet connection and try again" case .unauthorized: return "Tap to sign in" default: return nil } } } enum NetworkError: LocalizedError { case noConnection case timeout case serverError(Int) case decodingError var errorDescription: String? { switch self { case .noConnection: return "No internet connection" case .timeout: return "Request timed out" case .serverError(let code): return "Server error (\(code))" case .decodingError: return "Invalid response from server" } } } ``` ### Error Presentation ```swift struct ContentView: View { @Environment(AppState.self) private var appState var body: some View { NavigationStack { // Content } .alert( "Error", isPresented: Binding( get: { appState.error != nil }, set: { if !$0 { appState.error = nil } } ), presenting: appState.error ) { error in Button("OK") { } if error.recoverySuggestion != nil { Button("Retry") { Task { await retry() } } } } message: { error in VStack { Text(error.localizedDescription) if let suggestion = error.recoverySuggestion { Text(suggestion) .font(.caption) } } } } } ``` ## Testing Architecture ### Unit Testing with Mocks ```swift @Test func testLoadItems() async throws { // Arrange let mockNetwork = MockNetworkService() mockNetwork.mockResult = [Item(name: "Test")] let viewModel = ItemListViewModel(networkService: mockNetwork) // Act await viewModel.load() // Assert #expect(viewModel.items.count == 1) #expect(viewModel.items[0].name == "Test") #expect(viewModel.isLoading == false) } @Test func testLoadItemsError() async throws { // Arrange let mockNetwork = MockNetworkService() mockNetwork.mockError = NetworkError.noConnection let viewModel = ItemListViewModel(networkService: mockNetwork) // Act await viewModel.load() // Assert #expect(viewModel.items.isEmpty) #expect(viewModel.error != nil) } ``` ### Preview with Dependencies ```swift #Preview { ContentView() .environment(AppDependencies.mock()) .environment(AppState()) } ```