Adopt MV + Environment Services for iOS
Context
Having used React Query for server state management, the goal was to find a similar pattern for SwiftUI — something where views declare data needs and a cache layer handles the rest, rather than Redux/Flux-style global state management. Apollo iOS provides normalized caching that fits this model, though it requires explicit refetch calls after mutations (unlike Apollo React). We evaluated MVVM and Redux-like patterns but both add machinery that SwiftUI's Environment system already provides.
Decision
Adopt an "MV + Environment Services" pattern: SwiftUI's Environment for dependency injection, Observable services for cache lifecycle and business logic.
Core Principles:
- Views are simple, services own complexity: Views focus on presentation using Environment-injected services. Cache lifecycle, offline coordination, and business logic live in Observable services.
- Apollo as source of truth: Use Apollo's normalized cache with watched queries for centralized data updates.
- Views declare data needs, services fulfill them: Views call service methods on
onAppearor user action. Services decide whether to hit the network or serve from cache. This mirrors React Query — the component declares "I need this data" and the cache layer handles the rest. - Views observe, never own data: Views access data via service properties (
tripService.currentTrip), never via passed parameters or local@Statecopies. No prop drilling — that diverges from the cache. Exception: views displaying historical data not in the live cache take data as a parameter. - Co-locate data requirements: Each view queries exactly what it displays (see ADR-0031), not a shared monolithic query.
- Explicit cache and refetch management: Services handle cache clearing on logout and key configuration. Services must call
watcher.refetch()after mutations — Apollo iOS watchers don't automatically react to cache changes, unlike Apollo React.
Consequences
Centralized data updates through watched queries and normalized cache. Offline support builds on Apollo's SQLite persistence. Services are independently testable. Cache lifecycle (especially clearing on logout) is explicit and predictable.
The cost is that you must understand Apollo's normalization behavior and remember the refetch-after-mutation pattern. Services add a layer compared to pure MV. Cache keys need proper @typePolicy configuration to prevent data conflicts between users.