OdontoX iOS — Native SwiftUI Production Spec
Date: 2026-05-09 Status: Draft for review Platform: iOS 16+ deployment target / iPadOS 16+ · Built with iOS 26 SDK · Xcode 26 (mandatory for App Store since Apr 28 2026) · Swift 6 Roles in scope: Patient · Doctor · Admin · Receptionist Backend: Existing Hono/Cloudflare Workers API (api.odontox.io/api/v1/*)
1. Why Native SwiftUI
| Criterion | Reason |
|---|---|
| Apple HIG compliance | System components (TabView, NavigationStack, sheets) get Liquid Glass, Dynamic Type, VoiceOver, and future OS updates for free |
| Performance | No JS bridge; direct Metal/UIKit rendering; zero Hermes overhead |
| Security | LocalAuthentication, Keychain, Secure Enclave — direct API access, no RN wrappers |
| PHI sensitivity | App Transport Security (ATS) enforced at OS level; no third-party runtime |
| iPad-first | SwiftUI NavigationSplitView gives native master-detail with zero extra code |
| App Store review | First-class compliance; no grey-area Expo workarounds |
2. System Architecture
Layer responsibilities
| Layer | What it owns | What it never does |
|---|---|---|
| View | Render state, capture gestures, navigation | Business logic, network calls |
| ViewModel | Transform domain → display model, hold @Published state | Direct DB/network access |
| Use Case | One business operation (BookAppointment, CancelAppointment) | UI concerns |
| Repository | Choose cache vs. network, map DTOs to entities | Business rules |
| Remote Source | URLSession calls, request signing, response decoding | Caching, mapping |
| Local Source | SwiftData CRUD + NSCache read-through | Sync decisions |
3. Architecture Pattern — Clean MVVM
Apple’s WWDC 2025 sessions (the most recent as of May 2026) converge on @Observable + async/await + actors as the modern Swift pattern, with Swift 6 strict concurrency now the compiler default. No third-party architecture framework is required. Key Swift 6 / iOS 17+ decisions:@Observablemacro replacesObservableObject+@Published— compiler enforced, zero boilerplate (iOS 17+; gate with#if canImportfor iOS 16 fallback on the small tail of users)@MainActoron all ViewModels — Swift 6 strict concurrency makes this non-optionalasync/await + structured concurrency— no Combine pipelines;Task { }for fire-and-forgetactorfor shared mutable state (token store, cache coordinator)- Protocol-driven repositories — swap remote for mock in tests
- Privacy Manifests (
PrivacyInfo.xcprivacy) — mandatory for App Store submission as of 2026
4. Project Structure
5. Navigation Architecture
Apple HIG mandates: tab bar for top-level switching, NavigationStack for hierarchical drill-down, sheets for transient tasks. Implementation notes:TabViewon iPhone,NavigationSplitViewon iPad (same codebase,horizontalSizeClassswitch)- Each tab root is a
NavigationStackwith aNavigationPathfor deep linking - Sheets (
.sheet,.fullScreenCover) for booking flow, filters, detail overlays - Apple Zoom Transition (iOS 18+) on list-to-detail for appointment cards
- SF Symbols 6 throughout — no custom icon library
6. Auth Flow
Security rules (Apple best practices):| Concern | Implementation |
|---|---|
| Token storage | kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly — never migrates to new device |
| mPIN storage | SHA-256(pin + UUID-derived device salt) — Keychain, never UserDefaults |
| Biometric gate | LAPolicy.deviceOwnerAuthenticationWithBiometrics — fallback to PIN, not password |
| Session lock | ScenePhase.background + 15-min timer via Task.sleep; ScenePhase.active triggers re-auth |
| 5 PIN failures | Force full logout, wipe JWT from Keychain |
| ATS | NSAllowsArbitraryLoads = false in Info.plist — only api.odontox.io, assets.odontox.io allowed |
| Jailbreak signal | UIApplication.shared.canOpenURL(URL(string:"cydia://")!) — log, warn, restrict |
| Screenshot | Keywindow overlay UITextField(isSecureTextEntry: true) during sensitive screens |
7. Three-Tier Caching Strategy
Apple’sURLCache + NSCache + FileManager pattern — no third-party caching library.
Tier 1 — Memory (NSCache)
NSCache<NSString, CacheEntry<T>>— auto-evicts under memory pressure- Keyed by
"{entity-type}/{id}/{version}"— e.g."appointment/abc123/v1" - TTL: 5 min for lists, 15 min for detail records
- Never persisted; lost on app terminate (by design — no stale data across sessions)
Tier 2 — Disk (FileManager)
Codablestructs serialized to JSON →Library/Caches/odontox/{entity}/{id}.cache- Each file includes
expiresAt: Dateheader - TTL policy: appointments 2 min, patient profile 30 min, static (clinic info) 24 h
- Purged by OS under storage pressure (Caches dir — not backed up to iCloud)
CacheCleanupActorsweeps expired files on app foreground
Tier 3 — URLCache (HTTP layer)
- Configured URLSession with
URLCache(memoryCapacity: 10_MB, diskCapacity: 50_MB) - Respects
Cache-Controlheaders from Hono responses - Used for static assets (clinic logos, presigned R2 thumbnails)
- Not used for PHI API responses — those bypass URLCache (ephemeral policy)
Stale-While-Revalidate
OnRepository.fetch():
- Return cached data immediately (if exists and < 2× TTL)
- Kick off background
Taskto fetch fresh data from API - Update cache and publish to ViewModel via
AsyncStream - UI updates automatically — no spinner for returning users
PHI caching rules
| Data type | Cache tier | TTL | Notes |
|---|---|---|---|
| Appointment list | Memory + Disk | 2 min | High change rate |
| Patient profile | Memory + Disk | 30 min | Changes infrequently |
| Clinical notes | Memory only | 5 min | Never to disk (PHI) |
| Prescriptions | Memory only | 5 min | Never to disk (PHI) |
| Invoice list | Memory + Disk | 10 min | Billing data |
| PDF signed URLs | Memory only | 4 min | URL expires in 5 min |
| Clinic info / slots | Memory + Disk | 24 h | Rarely changes |
| Push token | Keychain | Permanent | Until re-enrollment |
8. Networking Layer
URLSession configuration:URLSessionConfiguration.defaultwith HTTP/2 enabled (default on iOS)waitsForConnectivity = true— queues requests until network availabletimeoutIntervalForRequest = 30s,timeoutIntervalForResource = 60s- Certificate pinning: validate
api.odontox.ioleaf certificate SHA-256 hash inURLSession(_:didReceive:completionHandler:)— protects against MitM on clinic WiFi
9. Local Persistence — SwiftData
SwiftData (iOS 17+, covered by 95%+ of target devices) replaces Core Data for structural persistence. Used for:- Offline appointment queue (appointments user cannot load without network)
- Unread notification badge count (survives app kill)
- User preferences (theme, notification opt-ins)
@Model classes are non-PHI only. If a model would contain patient-identifiable data, store in disk cache (encrypted) instead.
10. Push Notifications
iOS implementation:UNUserNotificationCenter.requestAuthorizationafter PIN success on first loginUIApplication.registerForRemoteNotifications()→ APNs token → POST/user-devicesUNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:)— suppress OS banner, show custom SwiftUI overlay insteadUNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:)— parsedata.url, push toNavigationPath- Notification types:
appointment.reminder,appointment.status_changed,message.new,payment.received
11. Offline Detection & UX
NWPathMonitor (Network framework) — reactive offline state:
NetworkMonitoractor publishesisConnected: BoolviaAsyncStream- Views subscribe via
@Environment(\.networkMonitor) - Persistent top banner: “You’re offline — showing cached data” (yellow, non-blocking)
- Mutations (book, cancel, send message) queued locally with
needsSync = true→ auto-retry when online
12. Roles & Screen Map
Patient
| Screen | Route / Nav | Notes |
|---|---|---|
| Home | Tab 1 root | Next appointment card, quick-book CTA |
| Appointments list | Tab 2 root → NavigationStack | Upcoming + Past sections |
| Book appointment | .sheet from appointments | 3-step: date → slot → confirm |
| Appointment detail | Push from list | Cancel button if > 48h |
| Medical records | Tab 3 root → NavigationStack | List by type |
| Treatment plan detail | Push | Read-only |
| Prescription detail | Push | Read-only |
| File viewer (PDF/image) | Push | Native QuickLook / PDFKit |
| Bills list | Tab 4 root → NavigationStack | Invoice rows |
| Invoice detail | Push | PDF view inline |
| Chat inbox | Tab 5 root | Clinic message threads |
| Chat thread | Push | iMessage-style bubbles |
| Profile + security | Sheet from Home | Biometric toggle, PIN change |
| Notifications | Sheet from bell icon | List with unread dots |
Doctor
| Screen | Notes |
|---|---|
| Home (today’s schedule) | Filtered by doctorId, timeline view |
| Appointments list | NavigationStack, date filter |
| Appointment detail + actions | Mark in-progress → completed; quick note |
| Patient search | Debounced search field |
| Patient profile | Vitals, notes, prescriptions, files tabs |
| Add clinical note | TextEditor, voice dictation via AVFoundation |
| New prescription | Form with drug search |
| Chat + Notifications | Same as patient |
Admin
All doctor screens plus:| Screen | Notes |
|---|---|
| Stats dashboard | KPI strip, weekly bar chart (Charts framework) |
| Finance — invoices | Full clinic invoice list |
| Create invoice | Multi-line items, patient search |
| Record payment | Amount + method |
| Mobile permissions (read-only) | View current clinic mobile access settings |
Receptionist
| Screen | Notes |
|---|---|
| Home (check-in queue) | Today’s appointments sorted by time, Check-In button |
| Appointments + New appointment | |
| Patient search + New patient | |
| Finance (invoices + payments) |
13. UI Design System
Follows Apple Human Interface Guidelines 2026 — Liquid Glass era (introduced with iOS 26, Sept 2025). This is the first major visual design shift since iOS 7.| Element | Implementation |
|---|---|
| Color | Color("BrandIndigo") #5048E5 primary; semantic colors from asset catalog (adapts dark/light) |
| Typography | .title, .headline, .body, .caption — Dynamic Type respected everywhere |
| Icons | SF Symbols 6 — weight: .medium, never custom icon font |
| Spacing | 8-pt grid — Spacer(), .padding(.horizontal, 16) |
| Touch targets | Minimum 44×44 pt on all tappable elements |
| Lists | List with listStyle(.insetGrouped) — system look, native swipe actions |
| Buttons | .buttonStyle(.borderedProminent) for primary, .bordered for secondary |
| Loading | ContentUnavailableView for empty states; .redacted(reason: .placeholder) for skeleton |
| Charts | Swift Charts (import Charts) — bar, line for stats; no third-party chart library |
| Forms | SwiftUI Form + Section — automatic inset grouped style |
| Accessibility | accessibilityLabel, accessibilityHint on all interactive elements; .accessibilityAddTraits(.isButton) |
14. Desktop-Only Features (not in iOS app)
| Feature | Reason |
|---|---|
| Dental chart write mode | Selecting individual teeth on 6” screen is unusable |
| Treatment plan authoring | Multi-service tables require large viewport |
| DICOM workstation | Windowing, pixel-level zoom = desktop |
| Staff/user management | Security-sensitive admin config |
| Clinic settings | Operating hours, plan config |
| Report builder | Filter/export UI requires desktop |
| Payroll | Requires desktop |
15. Security Checklist
-
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnlyon all Keychain items - ATS:
NSAllowsArbitraryLoads = false, only explicitodontox.iodomains whitelisted - Certificate pinning on
api.odontox.io(leaf cert SHA-256) -
FLAG_SECUREequivalent: blank snapshot on app switcher viaUIViewoverlay inSceneDelegate.sceneWillResignActive - No PHI in
os_log/ Crashlytics (filter[PHI]tagged fields) - No PHI in SwiftData — memory/disk cache only, cleared on logout
- mPIN brute-force: 5 attempts → full logout + Keychain wipe
- Biometric availability check:
LAContext.canEvaluatePolicybefore enrolling - Jailbreak heuristic: log to backend; restrict PHI display if detected
-
ScenePhase.background→ start 15-min countdown →.active→ if elapsed > 15 min, require re-auth
16. SwiftUI-Specific Best Practices (Apple WWDC 2023/2024/2025)
Sourced from Apple WWDC 2025 (most recent conference as of May 2026):| Pattern | Rule |
|---|---|
| State ownership | @State for view-local, @Bindable for ViewModel properties, @Environment for global |
| ViewModel lifecycle | .task {} modifier on root view — auto-cancels on disappear; never onAppear for async work |
| Lists | List over LazyVStack for tappable rows — system selection, swipe actions, accessibility built in |
| Images | AsyncImage with URLCache for non-PHI; manual Task download to UIImage for signed-URL PHI assets |
| Navigation | NavigationPath (type-erased) for programmatic deep linking from push notifications |
| Concurrency | Swift 6 strict concurrency: @MainActor on all ViewModels; domain layer nonisolated or dedicated actor |
| Privacy Manifests | PrivacyInfo.xcprivacy required listing all third-party SDKs and data usage — mandatory for App Store |
| Testing | XCTest + #Preview macros + Swift Testing framework (new, preferred over XCTest for unit tests) |
| Memory leaks | Use structured concurrency (async let, TaskGroup) — [weak self] only where Combine/closures unavoidable |
17. Build & Distribution
| Stage | Tool |
|---|---|
| Development | Xcode 26 (mandatory since Apr 28 2026 for App Store); run on iPhone 16 simulator + physical device |
| Code signing | Xcode Automatic Signing (team OdontoX) |
| CI | Xcode Cloud (GitHub integration, parallel builds per PR) |
| TestFlight | Archive → Xcode Cloud uploads → internal tester group (same-day) |
| App Store | Submit from TestFlight build; typical review 24–48 h |
| Version | CFBundleShortVersionString MAJOR.MINOR.PATCH; bump on each release |
| Minimum OS | iOS 16.0 deployment target (covers 95%+ of active devices; built with iOS 26 SDK per Apr 2026 App Store mandate) |
18. Dependencies (minimal)
| Library | Version | Purpose | Source |
|---|---|---|---|
| None — all Apple frameworks | — | — | — |
| Swift Package Manager only | — | — | — |
SwiftUI— UISwiftData— local persistenceURLSession— networkingLocalAuthentication— biometricsSecurity(Keychain) — token storageUserNotifications— pushPDFKit— PDF rendering (no third-party PDF lib)QuickLook— file previewsCharts— analytics chartsNetwork(NWPathMonitor) — connectivityAVFoundation— voice dictation in clinical notesCryptoKit— SHA-256 PIN hashing, AES-GCM for cache encryption
PackageIndex.swift.org and require: Swift 6 concurrency compatible, active maintenance, < 500 KB binary impact.

