A comprehensive development plan for the native iOS companion to the MOS Operations Platform, covering every current and future platform feature (Phases 1–5). Governed end-to-end by the iOS App Development Constitution (July 1, 2026 — Final): Swift + SwiftUI, strict MVVM, SOLID, Apple-native frameworks only, zero third-party dependencies.
MOS Field is the native iOS companion to the MOS Operations Platform. It is not a wrapper around the web app — it is a first-class client of the same FastAPI + Supabase backend, built for the people who spend their day away from a desk. The web app remains the primary surface for office staff and heavy PM workflows; the iOS app starts field-first and grows to cover every role as platform phases ship.
Today's jobs, navigation to site, photo capture, notes tied to the field book, daily reports, clock in/out — designed for one-handed use, gloves, and weak rural signal.
Job status at a glance, crew locations, approving field reports, pushing documents to a crew mid-job.
KPI dashboard, profitability and AR snapshots, historical search — the executive view in a pocket.
Scheduling adjustments and customer lookups when away from the desk; the web app remains their main tool.
Every pillar of the constitution, mapped to the concrete decision in this plan. One deviation exists, and it is explicitly authorized by the product owner (constitution §preamble: “Do not deviate unless explicitly instructed by the user”).
| Constitutional pillar | Decision in this plan | Status |
|---|---|---|
| Stack — Swift, SwiftUI, iOS 17+, Xcode | Swift (latest stable), SwiftUI-only UI, min deployment iOS 17.0. UIKit touched only where SwiftUI cannot do the job (haptics via UIImpactFeedbackGenerator, as the constitution itself prescribes). | Compliant |
| Zero third-party dependencies | No Supabase Swift SDK, no Alamofire, no image libraries. Supabase Auth is consumed as plain HTTPS REST (GoTrue endpoints) via URLSession. Maps = MapKit. Charts = Swift Charts. Camera = AVFoundation/PhotosUI. Speech = Speech framework. | Compliant |
| MVVM — strict layer separation | One @MainActor ViewModel per screen; Models are UI-free Codable structs; Views are declarative and logic-free; all I/O behind Services/Repositories (§03). | Compliant |
| SOLID | Protocol-first services (AuthServiceProtocol, NetworkServiceProtocol, JobRepositoryProtocol…), initializer injection everywhere, small role-scoped protocols, no concrete dependencies in ViewModels (§03). | Compliant |
| Authentication — Sign in with Apple primary | Deviation (owner-authorized): primary auth is email/password against the existing Supabase employee accounts, because roles are admin-assigned to company identities and must match the web app. Face ID/Touch ID re-entry via LocalAuthentication is kept exactly as mandated. Sign in with Apple can be layered on later as a linked provider without architectural change. | Authorized deviation |
| Keychain-only secrets | Access + refresh tokens live in Keychain via a typed KeychainManager with kSecAttrAccessibleWhenUnlockedThisDeviceOnly. Nothing sensitive in @AppStorage/UserDefaults. | Compliant |
| Networking — URLSession, HTTPS, typed | Single protocol-driven layer, async throws only, typed Endpoint/AppError, certificate pinning on the production API host, explicit CodingKeys, ATS fully on (§05). | Compliant |
| Persistence — SwiftData + Keychain | SwiftData @Model entities behind repositories; offline write-queue for field data; Keychain for secrets; @AppStorage only for benign flags (§06). | Compliant |
| HIG — native, accessible | NavigationStack + role-aware TabView, SF Pro type scale, semantic colors with brand Color Sets (light/dark), SF Symbols, Dynamic Type, VoiceOver labels/hints, 44pt targets (§07). | Compliant |
| Concurrency — async/await, @MainActor | All async paths use Swift Concurrency; no DispatchQueue.main.async, no completion handlers in new code; task cancellation on view disappear. | Compliant |
| Testing — XCTest, >80% logic coverage | Protocol mocks for every injected dependency; unit tests per ViewModel/Service; UI tests for sign-in, jobs, photo upload (§10). | Compliant |
Strict MVVM with a Services/Repositories core, instantiated with MOS feature names. The folder structure below is the constitution's mandated skeleton filled in for this product — Phase A folders are created at kickoff; later-phase folders are created only when their phase begins (no speculative scaffolding, per constitution §4).
/App MOSFieldApp.swift ← @main entry; injects root environment AppCoordinator.swift ← session state → Login vs role-aware TabView /Features /Auth /Views /ViewModels /Models /Jobs /Views /ViewModels /Models ← list, detail, status updates /Notes /Views /ViewModels /Models /Documents /Views /ViewModels /Models /Photos /Views /ViewModels /Models ← Phase A /FieldReports /Views /ViewModels /Models ← Phase A /TimeTracking /Views /ViewModels /Models ← Phase B /Mileage /Views /ViewModels /Models ← Phase B /Equipment /Views /ViewModels /Models ← Phase B /Dashboard /Views /ViewModels /Models ← Phase C (owner/PM) /Search /Views /ViewModels /Models ← Phase C /VoiceReports /Views /ViewModels /Models ← Phase D /Core /Services AuthService.swift · NetworkService.swift · SyncService.swift PushRegistrationService.swift · LocationService.swift /Repositories JobRepository.swift · NoteRepository.swift · DocumentRepository.swift TimeEntryRepository.swift · UploadQueueRepository.swift /Persistence PersistenceController.swift ← SwiftData ModelContainer /Security KeychainManager.swift · BiometricAuthManager.swift · CertificatePinningDelegate.swift /Shared /Components PrimaryButton.swift · InputField.swift · StatusPill.swift · RoleBadge.swift · DataStateView.swift /Extensions View+Extensions.swift · String+Extensions.swift /Constants AppConstants.swift · APIConstants.swift /Utilities Logger.swift · Validator.swift /Resources Assets.xcassets ← MOS brand Color Sets (light/dark variants) Localizable.strings
StatusPill,
RoleBadge, and DataStateView mirror the primitives already shipped in
apps/web (Day 3), and brand colors come from the approved design-system tokens as Asset Catalog
Color Sets with light/dark variants — semantic colors only in views, hex lives in the catalog, exactly as the
constitution requires.| Layer | Rules applied |
|---|---|
| Model | Plain Codable structs mirroring API schemas (Job, JobNote, UserProfile, RoleKey enum). No import SwiftUI. Domain logic (e.g., allowed status transitions Assigned → In Progress → Waiting → Completed) lives here as pure functions. |
| ViewModel | One per screen, final class, @MainActor, ObservableObject, @Published state + errorMessage: String?. Depends only on protocols, injected via initializer. Never touches URLSession or SwiftData directly. |
| View | Pure SwiftUI; owns its ViewModel with @StateObject; calls async methods inside Task { }; renders loading/empty/error/populated through DataStateView; zero business logic. |
| Service / Repository | Services own one domain each (auth, network, sync, push, location). Repositories mediate SwiftData + API per aggregate. Protocols are split by role when consumers need less (JobReadable vs JobWritable) — interface segregation. |
// The DI pattern used by every ViewModel in the app @MainActor final class JobListViewModel: ObservableObject { @Published private(set) var jobs: [Job] = [] @Published var errorMessage: String? private let jobRepository: any JobReadable init(jobRepository: any JobReadable) { self.jobRepository = jobRepository } func loadAssignedJobs() async { do { jobs = try await jobRepository.assignedJobs() } catch let error as AppError { errorMessage = error.errorDescription } catch { errorMessage = AppError.unknown.errorDescription } } }
Because the no-dependency rule forbids the Supabase Swift SDK, the app consumes Supabase
Auth (GoTrue) as what it really is: a plain HTTPS REST API. This keeps the entire auth stack inside
URLSession + Keychain + LocalAuthentication — all Apple-native.
| Step | Mechanism |
|---|---|
| 1 · Sign in | POST /auth/v1/token?grant_type=password on the Supabase project URL with the publishable (anon) key header. Response: ES256 access_token (~1h) + refresh_token. Decoded into a typed SessionTokens model. |
| 2 · Store | Both tokens → KeychainManager (kSecAttrAccessibleWhenUnlockedThisDeviceOnly). The anon key is a publishable client identifier (safe in the binary); real secrets never ship in source. |
| 3 · Call the API | Every FastAPI request carries Authorization: Bearer <access_token>. The backend already verifies signature via JWKS (ES256) — no shared secret anywhere, including the app. |
| 4 · Refresh | AuthService refreshes proactively before expiry and reactively on a 401 (grant_type=refresh_token), serialized through an actor so concurrent requests trigger exactly one refresh. |
| 5 · Re-entry | On foreground/app-launch with a stored session: Face ID / Touch ID via BiometricAuthManager (LocalAuthentication) gates access before tokens are read. Passcode fallback; sign-out wipes Keychain. |
| 6 · Roles | After sign-in, GET /api/me + GET /api/me/roles (already live on the backend) hydrate UserProfile and [RoleKey]. Roles gate navigation and actions in the UI — but authorization is always re-enforced server-side; the client never trusts itself. |
AppCoordinator maps session state to root view: .signedOut → LoginView, .locked → BiometricGateView, .active(roles) → MainTabView(roles).AuthServiceProtocol and an
account-linking step — no changes to token storage, refresh, or RBAC.One protocol-driven layer, exactly per constitution §7 — the snippets below are the constitution's own contracts extended with the MOS endpoint catalog.
protocol NetworkServiceProtocol { func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T } struct Endpoint { let path: String let method: HTTPMethod // .get / .post / .put / .delete let headers: [String: String] let body: Encodable? } enum AppError: LocalizedError { case networkUnavailable, unauthorized, forbidden, notFound case decodingFailed, serverError(Int) case keychainFailure(OSStatus), biometricUnavailable case validationFailed(String), syncConflict, unknown }
NSAllowsArbitraryLoads = false, no exceptions.CertificatePinningDelegate (public-key pinning in urlSession(_:didReceive:)); disabled only in DEBUG against localhost.JSONDecoder with explicit CodingKeys (snake_case API → camelCase Swift); raw Data never reaches a ViewModel; Any is banned..unauthorized; 403 → .forbidden (role gate); 404 → .notFound; 422 → .validationFailed; 5xx → .serverError.async throws; no completion handlers anywhere in new code.APIConstants: DEBUG → local FastAPI; RELEASE → the production API domain. No keys in source.Survey crews work where signal doesn't. The app treats the local SwiftData store as the read source of truth for the UI and queues writes when offline — the single most important product decision in this plan.
| @Model | Purpose | Sync behavior |
|---|---|---|
JobEntity | Assigned jobs cache: number, client, site, status, due, crew | Pull-refresh + on-launch delta fetch; server wins on conflict |
JobNoteEntity | Notes authored on device | Queued write; marked pending → synced |
PhotoUploadEntity | Captured photos + job link + GPS + timestamp | Background URLSession upload queue, survives app termination |
DailyReportEntity | Field report drafts | Draft locally; explicit submit; queued if offline |
TimeEntryEntity | Clock in/out pairs (Phase B) | Timestamped locally at tap-time; reconciled on sync |
MileageEntity | Trip logs (Phase B) | Queued write |
NWPathMonitor) or on BGAppRefreshTask; failures retry with backoff and surface as a quiet "pending sync" badge, never a blocking alert.URLSessionConfiguration so multi-MB site photos finish even if the crew pockets the phone.QuickLook; files cached with NSFileProtectionComplete; nothing sensitive in caches beyond the protection class.@AppStorage only for benign preferences (e.g., preferred jobs sort). Constitution §8/§11 exactly.Every platform feature from the product roadmap (Phases 1–5), mapped to iOS delivery phases. Each feature names its MVVM units and the Apple-native framework that satisfies the zero-dependency rule. Later phases are planned here but scaffolded only when they begin.
A field crew member can run their whole day from the phone: see jobs, get to site, capture evidence, file the daily report.
| Feature | MVVM units | Apple frameworks |
|---|---|---|
| Sign in + biometric re-entry | LoginView/ViewModel, BiometricGateView, AuthService, KeychainManager | AuthenticationServices-ready, LocalAuthentication, Security |
| My Jobs list + detail | JobListView/ViewModel, JobDetailView/ViewModel, JobRepository | SwiftUI, SwiftData |
| Status updates (Assigned → In Progress → Waiting → Completed) | Transition rules in Job model; JobDetailViewModel.updateStatus() | — (pure domain logic) |
| Job notes — built Jul 14 | JobDetailView/ViewModel, JobNoteRepository, JobNoteEntity | SwiftUI, SwiftData |
| Document viewing | DocumentListView/ViewModel, DocumentRepository | QuickLook |
| Photo capture + upload (GPS + timestamp stamped) — built Jul 14 | PhotoCaptureView, JobDetailViewModel, PhotoEvidenceStamp, PhotoLocationProvider, DocumentRepository | UIKit camera bridge, CoreLocation, SwiftUI |
| Site maps + navigation hand-off | JobMapView inside job detail | MapKit, CoreLocation |
| Daily field reports — built Jul 14 | JobDetailView/ViewModel, FieldReportRepository, FieldReportEntity | SwiftUI, SwiftData |
| Push notifications (assignment, status, due-date) — built Jul 14 | PushRegistrationService, AppDelegate; deep-link into job detail | UserNotifications, APNs |
The phone becomes the timesheet and the equipment ledger.
| Feature | MVVM units | Apple frameworks |
|---|---|---|
| Clock in/out with GPS evidence and job-site arrival/leave reminders — built Jul 14 | TimeClockView/ViewModel, TimeEntryRepository, JobGeofenceService | CoreLocation, UserNotifications, SwiftData |
| Time Off request form, status history, and offline read cache — built Jul 14 | TimeOffView/ViewModel, TimeOffRepository, TimeOffEntity | SwiftUI, SwiftData |
| Mileage tracking with start/end odometer validation and calculated trip miles — built Jul 14 | TimeClockView/ViewModel, TimeEntryRepository | SwiftUI, CoreLocation |
| Equipment checkout/return with assignment and double-checkout protection — built Jul 14 | EquipmentListView/ViewModel, EquipmentRepository | SwiftUI |
| Payroll-ready summaries (read-only current-week hours and completed entries) — built Jul 14 | TimeClockView/ViewModel | SwiftUI |
Owner and office roles get real value beyond the web parity baseline.
| Feature | MVVM units | Apple frameworks |
|---|---|---|
| Executive dashboard (active jobs, revenue, margin, AR) — built Jul 14 | InsightsView/ViewModel | Swift Charts |
| Reports (server-rendered daily-report PDFs viewed natively) — built Jul 14 | ReportLibraryView/ViewModel | QuickLook |
| Historical search across jobs/clients/docs/notes — built Jul 14 | InsightsView/ViewModel, debounced query to backend | SwiftUI |
| Office scheduling adjustments — built Jul 6 | DispatchListView/ViewModel | SwiftUI |
| Home-screen at-a-glance — built Jul 8 | MOSFieldWidget (jobs due / status) | WidgetKit |
Heavy AI runs on the backend; the app contributes what only a phone can — voice, camera, and context.
| Feature | MVVM units | Apple frameworks |
|---|---|---|
| Voice-to-report — on-device dictation followed by backend OpenAI structuring — built Jul 15 | SpeechTranscriptionService, AIRepository, JobDetailViewModel | Speech, AVFoundation |
| Document summaries — private PDF/text extraction and professional survey summary — built Jul 15 | DocumentListViewModel, AIRepository | SwiftUI; backend-computed |
| Intelligent search — natural-language queries interpreted by the backend before cross-entity search — built Jul 15 | InsightsViewModel, AIRepository | SwiftUI |
| Smart capture — document scanning with perspective correction for field paperwork — built Jul 15 | DocumentScannerView | VisionKit (VNDocumentCameraViewController) |
The iOS app consumes the platform API — these are the FastAPI additions each iOS phase needs. They are platform-side work items, not iOS work, and most are already on the platform roadmap (the web app needs them too).
| Needed by | Backend addition | Notes |
|---|---|---|
| Phase A | Jobs CRUD + status transition endpoints (/api/jobs…) | Already Day-5 scope for the web vertical slice; shared as-is |
| Phase A | Notes + documents endpoints; presigned upload/download URLs (Supabase Storage brokered by FastAPI) | App never holds storage credentials — it receives short-lived signed URLs |
| Phase A | Device-token registration (POST /api/devices) + APNs push service | Server-side APNs auth key; push on assignment/status/due events |
| Phase B | Time-entry, mileage, and equipment endpoints | Same tables feed payroll export on the web |
| Phase C | Dashboard aggregates + search endpoint | Server-computed; app only renders |
| Phase D | AI endpoints: report structuring from transcript, doc summaries, NL search | Platform Phase 5 scope |
Model structs are written against that schema (mirroring the typed-client approach
planned for the web). One source of truth, three clients.KeychainManager (save/retrieve/delete throwing methods, KeychainKey enum), kSecAttrAccessibleWhenUnlockedThisDeviceOnly; tokens never in UserDefaults, @AppStorage, or logs.NSAllowsArbitraryLoads = false; zero ATS exceptions; DEBUG-only localhost config never ships (build-configuration separated).URLSessionDelegate; pin rotation documented alongside cert renewal.CodingKeys; no Any, no untyped dictionaries, no raw Data past the network layer.NSFileProtectionComplete entitlement; SwiftData store and cached documents covered.Logger (os.Logger-backed) that redacts emails/tokens/coordinates in RELEASE; diagnostics behind #if DEBUG.Validator utility with typed rules (lengths, formats) applied before values reach ViewModels or the wire.require_roles()).photo/field documents
scoped to the active job. Missing location is labeled explicitly rather than fabricated. The full scheme passed 89 tests
with zero failures or skips on iPhone 17 / iOS 26 simulator on July 14, 2026, followed by a clean Debug build.MockAuthService: AuthServiceProtocol…); >80% coverage on business logic.ModelContainer; sync-queue ordering and conflict cases covered.MARKETING_VERSION per phase, auto-incremented builds) from day one.iOS App Development Constitution · July 1, 2026 · Status: Final. The governing document for all iOS work on this project, reproduced in full.
You are an expert iOS engineer specializing in SaaS application development. This document is your constitution — a set of non-negotiable principles and patterns you must follow precisely when building any iOS application in this project. Read every rule carefully. Apply every rule consistently. Do not deviate unless explicitly instructed by the user.
Every feature must follow strict Model-View-ViewModel separation. No exceptions.
import SwiftUI is forbidden in Model files). Codable conformance where persistence or networking is required. Business logic lives here, not in the View or ViewModel.@MainActor for all UI-bound state. Conforms to ObservableObject; uses @Published for state properties. Handles all data transformation, validation, and state management. Never imports or references SwiftUI views directly. Calls into Services/Repositories — never performs networking or persistence inline.@StateObject (owner) or @ObservedObject (passed in). Contains zero business logic. No direct API calls, database calls, or heavy computation.Folder structure: /App (AppEntry.swift, AppCoordinator.swift) · /Features/[FeatureName]/{Views, ViewModels, Models} · /Core/{Services, Repositories, Persistence, Security} · /Shared/{Components, Extensions, Constants, Utilities} · /Resources (Assets.xcassets, Localizable.strings)
.font(.title), .font(.body), etc. Never hardcode font sizes. Maintain clear visual hierarchy: Title → Headline → Body → Caption.Color(.systemBackground), Color(.label), Color(.secondaryLabel) etc. Define brand colors in Assets.xcassets as Color Sets with light/dark variants. Never use hardcoded hex values inline in views..accessibilityLabel and .accessibilityHint. VoiceOver order via .accessibilitySortPriority. .accessibilityElement(children: .combine) for grouped content. Test with Accessibility Inspector before considering any screen complete.kSecAttrAccessibleWhenUnlockedThisDeviceOnly as default accessibility. All Keychain operations wrapped in a dedicated KeychainManager with typed methods (save/retrieve/delete for KeychainKey).#if DEBUG guards around diagnostic logging.Only URLSession, structured through a typed, protocol-driven service layer (NetworkServiceProtocol.request<T: Decodable>, Endpoint, HTTPMethod). All calls async throws — Swift Concurrency only, no completion handlers. All HTTP error codes handled explicitly, mapped to typed AppError cases. Responses decoded into typed models immediately.
@StateObject — ViewModel owned by a View@ObservedObject — ViewModel passed into a View@EnvironmentObject — App-wide shared state (sparingly, truly global only)@State — Local, ephemeral view state only@AppStorage — Non-sensitive user preferences only@AppStorage or @StateTyped AppError enum conforming to LocalizedError (networkUnavailable, unauthorized, decodingFailed, keychainFailure(OSStatus), validationFailed(String)…). Every throwing function propagates typed errors — no empty catch. ViewModels expose errorMessage: String?. Errors shown via native .alert.
async/await for all asynchronous operations. ViewModels @MainActor. Task { } in .onAppear / button actions. TaskGroup for parallel operations. Never DispatchQueue.main.async. Cancel tasks appropriately on disappear.
SwiftData (iOS 17+) as primary local persistence. Keychain for sensitive data. @AppStorage only for non-sensitive flags. Clear @Model classes. Never persist directly from a View — route through the Repository layer.
Unit tests for every ViewModel and Service (XCTest). Protocol-based mocks for all injected dependencies. >80% coverage on business logic. UI tests for critical flows (login, onboarding, core feature). No production code ships with failing tests.
Swift API Design Guidelines naming. Default to private. final by default. No force unwrapping, no try!/as!. guard for early exit. Extensions for protocol conformance. Standard file header comment (filename, project, creation date) in every file.
This constitution governs all iOS development in this project. When in doubt, choose the simpler, more secure, more native solution.