Skip to main content

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

CriterionReason
Apple HIG complianceSystem components (TabView, NavigationStack, sheets) get Liquid Glass, Dynamic Type, VoiceOver, and future OS updates for free
PerformanceNo JS bridge; direct Metal/UIKit rendering; zero Hermes overhead
SecurityLocalAuthentication, Keychain, Secure Enclave — direct API access, no RN wrappers
PHI sensitivityApp Transport Security (ATS) enforced at OS level; no third-party runtime
iPad-firstSwiftUI NavigationSplitView gives native master-detail with zero extra code
App Store reviewFirst-class compliance; no grey-area Expo workarounds

2. System Architecture

Layer responsibilities

LayerWhat it ownsWhat it never does
ViewRender state, capture gestures, navigationBusiness logic, network calls
ViewModelTransform domain → display model, hold @Published stateDirect DB/network access
Use CaseOne business operation (BookAppointment, CancelAppointment)UI concerns
RepositoryChoose cache vs. network, map DTOs to entitiesBusiness rules
Remote SourceURLSession calls, request signing, response decodingCaching, mapping
Local SourceSwiftData CRUD + NSCache read-throughSync 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:
  • @Observable macro replaces ObservableObject + @Published — compiler enforced, zero boilerplate (iOS 17+; gate with #if canImport for iOS 16 fallback on the small tail of users)
  • @MainActor on all ViewModels — Swift 6 strict concurrency makes this non-optional
  • async/await + structured concurrency — no Combine pipelines; Task { } for fire-and-forget
  • actor for 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

OdontoX/
├── App/
│   ├── OdontoXApp.swift              ← @main, dependency container setup
│   └── AppRouter.swift               ← root NavigationStack path binding

├── Core/
│   ├── Network/
│   │   ├── APIClient.swift           ← URLSession actor, request building, JWT injection
│   │   ├── RequestInterceptor.swift  ← silent refresh on 401
│   │   ├── Endpoint.swift            ← enum of all API routes
│   │   └── NetworkMonitor.swift      ← NWPathMonitor (offline detection)
│   ├── Cache/
│   │   ├── MemoryCache.swift         ← NSCache wrapper, generic, thread-safe
│   │   ├── DiskCache.swift           ← FileManager + Codable, TTL-aware
│   │   └── CachePolicy.swift         ← stale-while-revalidate enum
│   ├── Auth/
│   │   ├── KeychainService.swift     ← JWT, refreshToken, mPIN hash
│   │   ├── BiometricService.swift    ← LAContext, Face ID / Touch ID
│   │   ├── PINService.swift          ← SHA-256 + per-device salt
│   │   └── SessionManager.swift     ← @Observable, session state machine
│   ├── Security/
│   │   └── ScreenshotPrevention.swift ← UITextField.isSecureTextEntry trick
│   └── Extensions/
│       ├── Date+Formatting.swift
│       └── URLSession+Async.swift

├── Features/
│   ├── Auth/
│   │   ├── LoginView.swift + LoginViewModel.swift
│   │   ├── PINEntryView.swift + PINViewModel.swift
│   │   ├── SetPINView.swift
│   │   └── BiometricPromptView.swift
│   ├── Patient/
│   │   ├── Home/
│   │   ├── Appointments/
│   │   ├── Records/
│   │   ├── Bills/
│   │   ├── Chat/
│   │   └── Profile/
│   ├── Doctor/
│   ├── Admin/
│   ├── Receptionist/
│   └── Shared/
│       ├── Components/               ← AppointmentCard, StatusBadge, etc.
│       └── ViewModifiers/

├── Domain/
│   ├── Entities/                     ← Appointment.swift, Patient.swift, etc.
│   ├── UseCases/
│   │   ├── BookAppointmentUseCase.swift
│   │   ├── CancelAppointmentUseCase.swift
│   │   └── …
│   └── Repositories/
│       ├── AppointmentRepositoryProtocol.swift
│       └── …

├── Data/
│   ├── Remote/
│   │   ├── DTOs/                     ← Codable structs matching API JSON
│   │   └── Mappers/                  ← DTO → Entity
│   ├── Local/
│   │   ├── SwiftData/
│   │   │   ├── AppointmentSDModel.swift  ← @Model class
│   │   │   └── ModelContainer+App.swift
│   │   └── Cache/                    ← NSCache instances per entity type
│   └── Repositories/                 ← Concrete implementations

├── Resources/
│   ├── Assets.xcassets
│   └── Localizable.strings

└── OdontoX.xcodeproj

5. Navigation Architecture

Apple HIG mandates: tab bar for top-level switching, NavigationStack for hierarchical drill-down, sheets for transient tasks. Implementation notes:
  • TabView on iPhone, NavigationSplitView on iPad (same codebase, horizontalSizeClass switch)
  • Each tab root is a NavigationStack with a NavigationPath for 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):
ConcernImplementation
Token storagekSecAttrAccessibleAfterFirstUnlockThisDeviceOnly — never migrates to new device
mPIN storageSHA-256(pin + UUID-derived device salt) — Keychain, never UserDefaults
Biometric gateLAPolicy.deviceOwnerAuthenticationWithBiometrics — fallback to PIN, not password
Session lockScenePhase.background + 15-min timer via Task.sleep; ScenePhase.active triggers re-auth
5 PIN failuresForce full logout, wipe JWT from Keychain
ATSNSAllowsArbitraryLoads = false in Info.plist — only api.odontox.io, assets.odontox.io allowed
Jailbreak signalUIApplication.shared.canOpenURL(URL(string:"cydia://")!) — log, warn, restrict
ScreenshotKeywindow overlay UITextField(isSecureTextEntry: true) during sensitive screens

7. Three-Tier Caching Strategy

Apple’s URLCache + 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)

  • Codable structs serialized to JSON → Library/Caches/odontox/{entity}/{id}.cache
  • Each file includes expiresAt: Date header
  • 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)
  • CacheCleanupActor sweeps expired files on app foreground

Tier 3 — URLCache (HTTP layer)

  • Configured URLSession with URLCache(memoryCapacity: 10_MB, diskCapacity: 50_MB)
  • Respects Cache-Control headers 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

On Repository.fetch():
  1. Return cached data immediately (if exists and < 2× TTL)
  2. Kick off background Task to fetch fresh data from API
  3. Update cache and publish to ViewModel via AsyncStream
  4. UI updates automatically — no spinner for returning users

PHI caching rules

Data typeCache tierTTLNotes
Appointment listMemory + Disk2 minHigh change rate
Patient profileMemory + Disk30 minChanges infrequently
Clinical notesMemory only5 minNever to disk (PHI)
PrescriptionsMemory only5 minNever to disk (PHI)
Invoice listMemory + Disk10 minBilling data
PDF signed URLsMemory only4 minURL expires in 5 min
Clinic info / slotsMemory + Disk24 hRarely changes
Push tokenKeychainPermanentUntil re-enrollment

8. Networking Layer

URLSession configuration:
  • URLSessionConfiguration.default with HTTP/2 enabled (default on iOS)
  • waitsForConnectivity = true — queues requests until network available
  • timeoutIntervalForRequest = 30s, timeoutIntervalForResource = 60s
  • Certificate pinning: validate api.odontox.io leaf certificate SHA-256 hash in URLSession(_: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)
PHI never written to SwiftData — memory + disk cache only, purged on logout. @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.requestAuthorization after PIN success on first login
  • UIApplication.registerForRemoteNotifications() → APNs token → POST /user-devices
  • UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:) — suppress OS banner, show custom SwiftUI overlay instead
  • UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:) — parse data.url, push to NavigationPath
  • Notification types: appointment.reminder, appointment.status_changed, message.new, payment.received

11. Offline Detection & UX

NWPathMonitor (Network framework) — reactive offline state:
  • NetworkMonitor actor publishes isConnected: Bool via AsyncStream
  • 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

ScreenRoute / NavNotes
HomeTab 1 rootNext appointment card, quick-book CTA
Appointments listTab 2 root → NavigationStackUpcoming + Past sections
Book appointment.sheet from appointments3-step: date → slot → confirm
Appointment detailPush from listCancel button if > 48h
Medical recordsTab 3 root → NavigationStackList by type
Treatment plan detailPushRead-only
Prescription detailPushRead-only
File viewer (PDF/image)PushNative QuickLook / PDFKit
Bills listTab 4 root → NavigationStackInvoice rows
Invoice detailPushPDF view inline
Chat inboxTab 5 rootClinic message threads
Chat threadPushiMessage-style bubbles
Profile + securitySheet from HomeBiometric toggle, PIN change
NotificationsSheet from bell iconList with unread dots

Doctor

ScreenNotes
Home (today’s schedule)Filtered by doctorId, timeline view
Appointments listNavigationStack, date filter
Appointment detail + actionsMark in-progress → completed; quick note
Patient searchDebounced search field
Patient profileVitals, notes, prescriptions, files tabs
Add clinical noteTextEditor, voice dictation via AVFoundation
New prescriptionForm with drug search
Chat + NotificationsSame as patient

Admin

All doctor screens plus:
ScreenNotes
Stats dashboardKPI strip, weekly bar chart (Charts framework)
Finance — invoicesFull clinic invoice list
Create invoiceMulti-line items, patient search
Record paymentAmount + method
Mobile permissions (read-only)View current clinic mobile access settings

Receptionist

ScreenNotes
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.
ElementImplementation
ColorColor("BrandIndigo") #5048E5 primary; semantic colors from asset catalog (adapts dark/light)
Typography.title, .headline, .body, .caption — Dynamic Type respected everywhere
IconsSF Symbols 6 — weight: .medium, never custom icon font
Spacing8-pt grid — Spacer(), .padding(.horizontal, 16)
Touch targetsMinimum 44×44 pt on all tappable elements
ListsList with listStyle(.insetGrouped) — system look, native swipe actions
Buttons.buttonStyle(.borderedProminent) for primary, .bordered for secondary
LoadingContentUnavailableView for empty states; .redacted(reason: .placeholder) for skeleton
ChartsSwift Charts (import Charts) — bar, line for stats; no third-party chart library
FormsSwiftUI Form + Section — automatic inset grouped style
AccessibilityaccessibilityLabel, accessibilityHint on all interactive elements; .accessibilityAddTraits(.isButton)

14. Desktop-Only Features (not in iOS app)

FeatureReason
Dental chart write modeSelecting individual teeth on 6” screen is unusable
Treatment plan authoringMulti-service tables require large viewport
DICOM workstationWindowing, pixel-level zoom = desktop
Staff/user managementSecurity-sensitive admin config
Clinic settingsOperating hours, plan config
Report builderFilter/export UI requires desktop
PayrollRequires desktop

15. Security Checklist

  • kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly on all Keychain items
  • ATS: NSAllowsArbitraryLoads = false, only explicit odontox.io domains whitelisted
  • Certificate pinning on api.odontox.io (leaf cert SHA-256)
  • FLAG_SECURE equivalent: blank snapshot on app switcher via UIView overlay in SceneDelegate.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.canEvaluatePolicy before 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):
PatternRule
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
ListsList over LazyVStack for tappable rows — system selection, swipe actions, accessibility built in
ImagesAsyncImage with URLCache for non-PHI; manual Task download to UIImage for signed-URL PHI assets
NavigationNavigationPath (type-erased) for programmatic deep linking from push notifications
ConcurrencySwift 6 strict concurrency: @MainActor on all ViewModels; domain layer nonisolated or dedicated actor
Privacy ManifestsPrivacyInfo.xcprivacy required listing all third-party SDKs and data usage — mandatory for App Store
TestingXCTest + #Preview macros + Swift Testing framework (new, preferred over XCTest for unit tests)
Memory leaksUse structured concurrency (async let, TaskGroup) — [weak self] only where Combine/closures unavoidable

17. Build & Distribution

StageTool
DevelopmentXcode 26 (mandatory since Apr 28 2026 for App Store); run on iPhone 16 simulator + physical device
Code signingXcode Automatic Signing (team OdontoX)
CIXcode Cloud (GitHub integration, parallel builds per PR)
TestFlightArchive → Xcode Cloud uploads → internal tester group (same-day)
App StoreSubmit from TestFlight build; typical review 24–48 h
VersionCFBundleShortVersionString MAJOR.MINOR.PATCH; bump on each release
Minimum OSiOS 16.0 deployment target (covers 95%+ of active devices; built with iOS 26 SDK per Apr 2026 App Store mandate)

18. Dependencies (minimal)

LibraryVersionPurposeSource
None — all Apple frameworks
Swift Package Manager only
Apple frameworks used directly:
  • SwiftUI — UI
  • SwiftData — local persistence
  • URLSession — networking
  • LocalAuthentication — biometrics
  • Security (Keychain) — token storage
  • UserNotifications — push
  • PDFKit — PDF rendering (no third-party PDF lib)
  • QuickLook — file previews
  • Charts — analytics charts
  • Network (NWPathMonitor) — connectivity
  • AVFoundation — voice dictation in clinical notes
  • CryptoKit — SHA-256 PIN hashing, AES-GCM for cache encryption
Zero third-party dependencies for core features. If a UI component is needed that requires a library, evaluate against PackageIndex.swift.org and require: Swift 6 concurrency compatible, active maintenance, < 500 KB binary impact.

19. Mermaid Summary — Full Request Lifecycle