Architecture¶
C4 Diagrams¶
Level 1 — System Context¶
C4Context
title System Context — Fast-E
Person(user, "User", "Tracks intermittent fasting windows on iPhone")
System(faste, "Fast-E", "iOS app for intermittent fasting. Tracks fasts, streaks, and history. Local-first, no backend.")
System_Ext(healthkit, "Apple HealthKit", "Apple health data platform. Stores completed fasting sessions as Mind & Body workouts.")
System_Ext(notifications, "Apple Notifications", "iOS notification system. Delivers reminders when fasting target duration is reached.")
System_Ext(widget, "iOS Widget (FastEWidget)", "WidgetKit home screen extension. Displays live fasting status and progress ring.")
System_Ext(cutie, "CutiE SDK", "Third-party in-app feedback and analytics SDK.")
Rel(user, faste, "Starts, stops, and reviews fasts")
Rel(faste, healthkit, "Writes completed fasts as workouts", "HealthKit API (opt-in)")
Rel(faste, notifications, "Schedules goal-reached reminder", "UNUserNotificationCenter")
Rel(faste, widget, "Writes fasting state", "App Groups UserDefaults")
Rel(faste, cutie, "Sends feedback events", "SPM SDK")
Rel(widget, user, "Shows fasting progress on home screen")
Rel(notifications, user, "Notifies when fasting goal is reached")
Level 2 — Containers¶
C4Container
title Container Diagram — Fast-E iOS App
Person(user, "User", "iPhone user tracking fasting windows")
System_Boundary(app, "Fast-E iOS App (no.invotek.FastE)") {
Container(views, "SwiftUI Views", "SwiftUI", "Timer, History, Stats, Settings tabs. Reads data via @Query. Calls FastingService for writes.")
Container(fastingservice, "FastingService", "Swift / ObservableObject (@MainActor)", "Core fasting logic: start/stop/cancel fasts, streak calculation, session editing. Orchestrates all other services.")
Container(appstate, "AppState", "Swift / ObservableObject", "Schedule selection (preset + custom), onboarding flag. Persisted to UserDefaults.")
ContainerDb(swiftdata, "SwiftData Store", "SwiftData / SQLite", "Persists FastingSession records: startDate, endDate, targetDuration, schedule, isCompleted, isCancelled.")
Container(notifservice, "NotificationService", "Swift / UNUserNotificationCenter", "Schedules local notification when fast reaches its target duration. Cancels on stop or cancel.")
Container(hkservice, "HealthKitService", "Swift / HealthKit", "Writes completed FastingSessions to HealthKit as Mind & Body workout samples. Opt-in.")
Container(widgetprovider, "WidgetDataProvider", "Swift / UserDefaults (App Groups)", "Writes active fasting state (status, startDate, targetDuration, schedule, streak) to shared App Group suite. Triggers WidgetCenter reload.")
Container(exportservice, "ExportService", "Swift / UIActivityViewController", "Exports fasting history as CSV for sharing.")
Container(cutieservice, "CutiEService", "Swift / CutiE SDK", "In-app feedback, analytics consent, inbox sheet.")
}
System_Boundary(widget, "FastEWidget Extension (no.invotek.FastE.Widget)") {
Container(widgetprovider_ext, "WidgetDataProvider (read-only)", "Swift / UserDefaults (App Groups)", "Reads fasting state from the shared App Group suite.")
Container(widgetviews, "Widget Views", "SwiftUI / WidgetKit", "SmallWidgetView and MediumWidgetView render the timer progress ring and status.")
}
System_Ext(healthkit, "Apple HealthKit", "Stores workout data")
System_Ext(notifications, "Apple Notifications", "Delivers local notifications")
System_Ext(cutiesdk, "CutiE SDK", "Feedback + analytics")
Rel(user, views, "Interacts with")
Rel(views, fastingservice, "Calls for writes (startFast, stopFast, etc.)")
Rel(views, swiftdata, "Reads via @Query")
Rel(fastingservice, swiftdata, "Creates / updates / deletes FastingSession")
Rel(fastingservice, notifservice, "Schedules / cancels notifications")
Rel(fastingservice, hkservice, "Syncs completed fast on stopFast()")
Rel(fastingservice, widgetprovider, "Updates widget state after every state change")
Rel(fastingservice, exportservice, "Triggers CSV export on request")
Rel(hkservice, healthkit, "Writes workout samples", "HealthKit API")
Rel(notifservice, notifications, "Schedules reminders", "UNUserNotificationCenter")
Rel(widgetprovider, widgetprovider_ext, "Writes JSON to shared UserDefaults suite", "App Groups")
Rel(widgetprovider_ext, widgetviews, "Supplies fasting state")
Rel(cutieservice, cutiesdk, "Sends events", "SPM")
Rel(views, cutieservice, "Presents feedback inbox")
Level 3 — Components (iOS App)¶
C4Component
title Component Diagram — Fast-E iOS App (Internal Components)
Container_Boundary(app_boundary, "Fast-E iOS Application (no.invotek.FastE)") {
Component(timer_view, "TimerView\n(Views/Timer/TimerView.swift)", "SwiftUI View\n@EnvironmentObject", "Primary screen. Shows ProgressRing\naround elapsed/remaining time.\nStart / Stop / Cancel buttons.\nDisplays current schedule name\nand elapsed hours:minutes:seconds.\nTimer display driven by 1-second\nSwiftUI timer Publisher (.onReceive).\nDeep-links from widget land here.")
Component(schedule_picker, "SchedulePickerView\n(Views/Timer/SchedulePickerView.swift)", "SwiftUI View", "Selects fasting schedule.\nPresets: 16:8, 18:6, 20:4, 23:1 (OMAD).\nCustom schedules from AppState.\nOpens CustomScheduleEditorView\nfor creating/editing custom hours.\nSelection stored in AppState.")
Component(custom_editor, "CustomScheduleEditorView\n(Views/Timer/CustomScheduleEditorView.swift)", "SwiftUI View", "Stepper for fasting hours (1–23).\nCalculates eating window automatically\n(eatingHours = 24 - fastingHours).\nCreates/updates CustomSchedule\nvia AppState.addCustomSchedule /\nupdateCustomSchedule.")
Component(history_view, "HistoryView\n(Views/History/HistoryView.swift)", "SwiftUI View\n@Query", "List of FastingSession records.\nQueries SwiftData with\n@Query(sort: \\startDate, desc).\nOpens FastDetailView for edit.\nDelete via modelContext.delete.")
Component(fast_detail, "FastDetailView\n(Views/History/FastDetailView.swift)", "SwiftUI View", "Edit startDate + endDate\nfor a completed session.\nCalls FastingService.updateSession.\nRecalculates isCompleted after edit.")
Component(stats_view, "StatsView\n(Views/Stats/StatsView.swift)", "SwiftUI View\n@Query", "Summary statistics:\ntotal fasts, completion rate,\naverage duration.\nEmbeds WeeklyChartView\nand StreakView.")
Component(streak_view, "StreakView\n(Views/Stats/StreakView.swift)", "SwiftUI View", "Calls FastingService.calculateStreak\nwith all sessions.\nShows current streak badge\nand longest-streak record.")
Component(weekly_chart, "WeeklyChartView\n(Views/Stats/WeeklyChartView.swift)", "SwiftUI View\nSwift Charts", "Bar chart of fast durations\nfor the last 7 days.\nGroups sessions by calendar day.\nColour-coded: completed vs partial.")
Component(settings_view, "SettingsView\n(Views/Settings/SettingsView.swift)", "SwiftUI View", "Sub-views: HealthSettingsView,\nNotificationSettingsView.\nExport history (ExportService).\nCutiE feedback inbox.\nApp version / onboarding reset.")
Component(health_settings, "HealthSettingsView\n(Views/Settings/HealthSettingsView.swift)", "SwiftUI View", "Toggle HealthKit sync.\nCalls HealthKitService.requestAuthorization.\nShows authorizationStatus.\nBackfill button: sync historical\ncompleted sessions via\nHealthKitService.backfillSessions.")
Component(notif_settings, "NotificationSettingsView\n(Views/Settings/NotificationSettingsView.swift)", "SwiftUI View", "Shows current notification\npermission status.\nRequest permission via\nNotificationService.requestPermission.\nLinks to iOS Settings app if denied.")
Component(fasting_service, "FastingService\n(Services/FastingService.swift)", "@MainActor ObservableObject\nSwiftData + WidgetKit", "CORE ORCHESTRATOR\n\nstartFast(schedule, startDate)\n • Guard: no active fast\n • Insert FastingSession in SwiftData\n • activeFast = session\n • Calculate remaining = targetDuration\n - (now - startDate), min 0\n • NotificationService.scheduleFastComplete\n (only if remaining >= 1s)\n • WidgetDataProvider.updateWidgetData\n (isActive:true + streak)\n\nstopFast()\n • endDate = now\n • isCompleted = reachedTarget\n • activeFast = nil, save\n • NotificationService.cancelPending\n • WidgetDataProvider.updateWidgetData\n (isActive:false)\n • If isCompleted: HealthKitService\n .saveFastingSession (async Task)\n\ncancelFast()\n • endDate = now, isCancelled = true\n • isCompleted = false\n • activeFast = nil, save\n • cancelPending + updateWidgetData\n\nupdateSession(session, startDate, endDate)\n • Mutate session dates\n • Recalculate isCompleted from\n actual >= target && !isCancelled\n • If still active: reschedule\n notification for new remaining time\n • Reload widget timelines\n\ndeleteFast(session)\n • modelContext.delete\n • WidgetCenter.reloadAllTimelines\n\ncalculateStreak(sessions) → Int\n • Filter: isCompleted == true\n • Sort descending by startDate\n • Streak starts only if most recent\n completed is today OR yesterday\n • Walk backward day-by-day, count\n consecutive calendar days with\n at least one completed session\n • Break on first gap\n\nloadActiveFast()\n • Fetch: endDate == nil && !isCancelled\n • Restores activeFast after cold launch")
Component(app_state, "AppState\n(Models/AppState.swift)", "@MainActor ObservableObject\nUserDefaults", "selectedSchedule: ScheduleSelection\n (.preset(FastingSchedule) |\n .custom(CustomSchedule))\n Persisted as JSON ('selectedScheduleV2')\n Migration from legacy string key\n Validates custom selection still\n exists in customSchedules array\n\ncustomSchedules: [CustomSchedule]\n Persisted as JSON array\n\nhasCompletedOnboarding: Bool\n Persisted as Bool\n\naddCustomSchedule(fastingHours)\nupdateCustomSchedule(schedule)\ndeleteCustomSchedule(schedule)")
Component(notification_svc, "NotificationService\n(Services/NotificationService.swift)", "Singleton\nUNUserNotificationCenter", "requestPermission() → Bool\n Requests .alert + .sound + .badge\n\ncheckPermissionStatus() → UNAuthorizationStatus\n Used by NotificationSettingsView\n\nscheduleFastComplete(in: duration,\n scheduleName: String)\n Content: 'Fast Complete!'\n 'You\\'ve completed your X fast.'\n Trigger: UNTimeIntervalNotification-\n Trigger(timeInterval: duration,\n repeats: false)\n Identifier: 'fastComplete'\n (replaces any pending notification\n with same ID)\n\ncancelPendingNotifications()\n Removes 'fastComplete' from\n pending queue")
Component(healthkit_svc, "HealthKitService\n(Services/HealthKitService.swift)", "Singleton\nHKHealthStore", "isAvailable: Bool\n HKHealthStore.isHealthDataAvailable()\n\nisSyncEnabled: Bool\n UserDefaults 'healthKitSyncEnabled'\n\nrequestAuthorization()\n Share: HKObjectType.workoutType()\n Read: empty set (write-only)\n\nsaveFastingSession(FastingSessionData)\n Guards: isSyncEnabled, isAvailable,\n isCompleted, !isCancelled, endDate ≠ nil\n hasDuplicate check (HKSampleQuery\n on source + date window, limit 1)\n saveWorkout:\n HKWorkoutConfiguration\n activityType: .mindAndBody\n HKWorkoutBuilder (async)\n addMetadata:\n 'no.invotek.FastE.schedule'\n 'no.invotek.FastE.targetDuration'\n finishWorkout()\n\nbackfillSessions([FastingSessionData]) → Int\n Iterates sessions, skips duplicates,\n returns count of newly synced")
Component(widget_provider_app, "WidgetDataProvider\n(Services/WidgetDataProvider.swift)", "Singleton\nApp Group UserDefaults Writer", "Suite: group.no.invotek.FastE\nKey: 'widgetData'\n\nWidgetData struct (Codable):\n isActive: Bool\n startDate: Date?\n targetDuration: TimeInterval?\n schedule: String?\n streak: Int\n\nupdateWidgetData(isActive, startDate,\n targetDuration, schedule, streak)\n Encodes WidgetData as JSON\n Writes to App Group UserDefaults\n WidgetCenter.shared.reloadAllTimelines()\n\nclearWidgetData()\n Calls updateWidgetData(isActive:false)\n\nreadWidgetData() → WidgetData\n (Main app also reads for consistency;\n widget extension has a read-only copy)")
Component(export_svc, "ExportService\n(Services/ExportService.swift)", "Static struct", "generateCSV(sessions: [FastingSession]) → String\n Header: Start Date, End Date, Schedule,\n Duration (hours), Target (hours), Completed\n Sorted descending by startDate\n ISO8601 date formatting\n Duration in decimal hours (1 dp)\n\ncsvFileURL(sessions) → URL?\n Writes CSV to tmp directory as\n 'fast-e-export.csv'\n Returns URL for UIActivityViewController")
Component(cutie_svc, "CutiEService\n(Services/CutiEService.swift)", "Singleton\nCutiE SDK wrapper", "Initialises CutiE SDK with App ID.\nPresentInboxSheet() for Settings.\nAnalytics consent handling.")
Component(swiftdata_store, "SwiftData Store\n(FastingSession @Model)", "SwiftData / SQLite\niOS 17+", "FastingSession entity:\n startDate: Date\n endDate: Date? (nil = active)\n targetDuration: TimeInterval (seconds)\n schedule: String (e.g. '16:8')\n isCompleted: Bool\n isCancelled: Bool\n\nComputed (not stored):\n actualDuration: endDate - startDate\n progress: elapsed / target (0–1)\n elapsedDuration: now - start (or end - start)\n remainingDuration: max(target - elapsed, 0)\n isActive: endDate == nil && !isCancelled\n reachedTarget: elapsed >= target\n fastingSchedule: FastingSchedule?(rawValue)")
}
Container_Boundary(widget_boundary, "FastEWidget Extension") {
Component(widget_provider_ext, "WidgetDataProvider\n(WidgetDataProvider.swift)", "Read-only copy\nApp Group UserDefaults Reader", "Reads 'widgetData' from\ngroup.no.invotek.FastE.\nDuplicated — widget extension\ncannot import main app module.\nFallback: inactive WidgetData\nif no data has been written yet.")
Component(widget_timeline, "FastEProvider\n(FastEWidget.swift)", "TimelineProvider\nWidgetKit", "getTimeline()\n Reads WidgetData via WidgetDataProvider\n Computes from live dates:\n elapsed = now - startDate\n progress = elapsed / targetDuration\n remaining = max(target - elapsed, 0)\n Returns single FastEEntry\n Refresh policy:\n .after(now + 60s) when active\n .after(now + 900s) when idle\n Active refresh drives the\n live countdown in the widget.")
Component(small_widget, "SmallWidgetView\n(Views/SmallWidgetView.swift)", "SwiftUI\n.systemSmall", "ProgressRing around remaining time.\nSchedule name + elapsed/total.\nStreak badge if streak > 0.\nGrey ring + 'Not fasting' when idle.")
Component(medium_widget, "MediumWidgetView\n(Views/MediumWidgetView.swift)", "SwiftUI\n.systemMedium", "Larger ring (left) +\ntext details (right):\n schedule, elapsed, remaining,\n streak count.\nStart prompt when idle.")
}
System_Ext(healthkit_ext, "Apple HealthKit", "HKWorkoutBuilder")
System_Ext(apns_ext, "Apple APNs", "Local notifications")
Rel(timer_view, fasting_service, "startFast(schedule)\nstopFast()\ncancelFast()")
Rel(timer_view, app_state, "Reads selectedSchedule\n@EnvironmentObject")
Rel(schedule_picker, app_state, "Sets selectedSchedule\nReads customSchedules")
Rel(schedule_picker, custom_editor, "Opens sheet for\nnew/edit custom")
Rel(custom_editor, app_state, "addCustomSchedule /\nupdateCustomSchedule")
Rel(history_view, swiftdata_store, "Reads via @Query\n(sort: startDate desc)")
Rel(history_view, fast_detail, "Opens on tap")
Rel(fast_detail, fasting_service, "updateSession\n(startDate, endDate)")
Rel(stats_view, swiftdata_store, "Reads via @Query\n(all sessions)")
Rel(stats_view, streak_view, "Embeds")
Rel(stats_view, weekly_chart, "Embeds")
Rel(streak_view, fasting_service, "calculateStreak(sessions)")
Rel(settings_view, health_settings, "Navigates to")
Rel(settings_view, notif_settings, "Navigates to")
Rel(settings_view, export_svc, "csvFileURL(sessions)\nvia UIActivityViewController")
Rel(settings_view, cutie_svc, "presentInboxSheet()")
Rel(health_settings, healthkit_svc, "requestAuthorization()\nbackfillSessions()")
Rel(notif_settings, notification_svc, "requestPermission()\ncheckPermissionStatus()")
Rel(fasting_service, swiftdata_store, "Insert/update/delete\nFastingSession")
Rel(fasting_service, notification_svc, "scheduleFastComplete\ncancelPendingNotifications")
Rel(fasting_service, healthkit_svc, "saveFastingSession\n(async, completed only)")
Rel(fasting_service, widget_provider_app, "updateWidgetData after\nevery state change")
Rel(healthkit_svc, healthkit_ext, "HKWorkoutBuilder\nasync write", "HealthKit API")
Rel(notification_svc, apns_ext, "UNNotificationRequest\n(timeInterval trigger)", "UserNotifications")
Rel(widget_provider_app, widget_provider_ext, "JSON in App Group\nUserDefaults", "group.no.invotek.FastE")
Rel(widget_provider_ext, widget_timeline, "readWidgetData()")
Rel(widget_timeline, small_widget, "Renders")
Rel(widget_timeline, medium_widget, "Renders")
Pattern¶
MVVM with a service layer, built on SwiftUI and SwiftData. Requires iOS 17+ (SwiftData dependency). Fully local-first — no backend, no Firebase, no network calls except the CutiE SDK for in-app feedback.
FastEApp (entry point)
├── ModelContainer (SwiftData)
├── AppState (ObservableObject — schedule, onboarding)
└── FastingService (ObservableObject — timer logic, CRUD)
├── NotificationService
├── HealthKitService
├── WidgetDataProvider (App Groups)
└── ExportService
Data Flow¶
FastEAppcreates aModelContainerforFastingSessionand injects it via.modelContainer()AppStateandFastingServiceare injected as@EnvironmentObjectinto the view hierarchyContentViewpassesmodelContexttoFastingServiceon appear- Views use
@Queryfor read access and callFastingServicemethods for writes - Widget reads shared state from App Groups (
UserDefaultssuite), written byWidgetDataProvider
Models¶
FastingSession (@Model)¶
The core persisted entity, stored in SwiftData.
| Property | Type | Purpose |
|---|---|---|
startDate |
Date |
When the fast began |
endDate |
Date? |
When the fast ended (nil = active) |
targetDuration |
TimeInterval |
Goal duration in seconds |
schedule |
String |
Schedule label (e.g. "16:8") |
isCompleted |
Bool |
Reached target duration |
isCancelled |
Bool |
User cancelled early |
Computed properties: progress, elapsedDuration, remainingDuration, isActive, reachedTarget.
FastingSchedule (enum)¶
Preset fasting schedules: 16:8, 18:6, 20:4, 23:1 (OMAD). Each case provides fastingHours, eatingHours, displayName, description, and fastingDuration.
CustomSchedule (struct, Codable)¶
User-defined schedule with a custom fasting hours value (1-23). Persisted to UserDefaults via AppState.
ScheduleSelection (enum, Codable)¶
Wraps either .preset(FastingSchedule) or .custom(CustomSchedule). Provides a unified interface for displayName, fastingHours, eatingHours, and fastingDuration.
AppState (ObservableObject)¶
Global app state managing:
selectedSchedule— currentScheduleSelection, persisted to UserDefaults (selectedScheduleV2key)customSchedules— array ofCustomSchedule, persisted to UserDefaultshasCompletedOnboarding— boolean, persisted to UserDefaults
Provides CRUD for custom schedules with automatic validation (if a deleted custom schedule was selected, falls back to 16:8).
Services¶
FastingService¶
The core service managing fasting sessions. @MainActor, ObservableObject.
startFast(schedule:startDate:)— creates aFastingSessionin SwiftData, schedules a notification, updates widgetstopFast()— setsendDate, marks completion, cancels notifications, syncs to HealthKit if completedcancelFast()— ends the fast as cancelleddeleteFast(_:)— removes a session from SwiftDataupdateSession(_:startDate:endDate:)— edits an existing session, recalculates completion, reschedules notifications if activecalculateStreak(sessions:)— counts consecutive days with a completed fast (from today or yesterday backward)
Holds a reference to ModelContext (configured on ContentView.onAppear), and publishes the current activeFast.
NotificationService¶
Schedules local notifications via UNUserNotificationCenter when a fast reaches its target duration. Cancels pending notifications on stop/cancel.
HealthKitService¶
Writes completed fasting sessions to HealthKit as Mind & Body workout samples. Opt-in, configured in Settings.
ExportService¶
Exports fasting history as CSV via UIActivityViewController.
CutiEService¶
Configures and manages the CutiE SDK for in-app feedback. Handles analytics consent prompts, inbox presentation, and unread badge counts.
WidgetDataProvider¶
Writes fasting state (active status, start date, target duration, schedule, streak) to a shared UserDefaults suite (App Groups) so the widget extension can read it.
Views¶
Navigation Structure¶
ContentView gates on hasCompletedOnboarding:
- Not onboarded →
OnboardingView(schedule selection + Get Started) - Onboarded →
MainTabViewwith 4 tabs:
| Tab | View | Purpose |
|---|---|---|
| Timer | TimerView |
Start/stop fasting, animated progress ring, schedule picker |
| History | HistoryView |
List of past fasts with FastDetailView for editing |
| Stats | StatsView |
Weekly chart (WeeklyChartView), streak (StreakView), totals |
| Settings | SettingsView |
Schedule, notifications, health sync, export, feedback |
Timer Tab¶
TimerView— main fasting screen. ShowsTimerRingViewwith animated progress, elapsed/remaining time, start/stop/cancel buttons, "started earlier" optionTimerRingView— circular progress indicatorSchedulePickerView— preset + custom schedule selectionCustomScheduleEditorView— create/edit custom schedules
History Tab¶
HistoryView—@Query-powered list ofFastingSessionsorted by dateFastDetailView— detail/edit view for a session (edit start/end times)
Stats Tab¶
StatsView— aggregated stats (total fasts, average duration, completion rate, weekly count)WeeklyChartView— Swift Charts bar chart of the past weekStreakView— current consecutive-day streak display
Settings Tab¶
SettingsView— schedule selection, notifications, CSV export, Apple Health, CutiE feedback, analytics toggleHealthSettingsView— HealthKit sync toggle, sync past sessions, open Health appNotificationSettingsView— notification permission management
Shared Components¶
FastingSummaryCard— reusable card showing session summary (schedule, duration, status)FeedbackToolbarButtons— CutiE inbox + send feedback toolbar itemsProgressRing— generic circular progress indicator
Widget Extension (FastEWidget)¶
WidgetKit extension (no.invotek.FastE.Widget) displaying fasting status on the home screen.
FastEWidget—TimelineProviderthat reads state from App GroupsFastEWidgetBundle— widget bundle entry pointWidgetDataProvider— reads shared UserDefaults (mirrors the app-side writer)- Views/ — widget-specific SwiftUI views
Communication is one-way: the app writes to App Groups, the widget reads. WidgetCenter.shared.reloadAllTimelines() triggers widget refresh after state changes.
Persistence Strategy¶
| Data | Storage | Why |
|---|---|---|
| Fasting sessions | SwiftData (FastingSession @Model) |
Structured, queryable, iOS 17 native |
| Selected schedule | UserDefaults (JSON-encoded) | Lightweight preference |
| Custom schedules | UserDefaults (JSON-encoded) | Small dataset, no relationships |
| Onboarding flag | UserDefaults (boolean) | Simple flag |
| Widget state | App Groups UserDefaults | Cross-process sharing |
Dependencies¶
| Dependency | Purpose |
|---|---|
CutiE SDK (SPM, >= 2.3.0) |
In-app feedback and analytics |
| HealthKit.framework | Fasting session sync |
| WidgetKit.framework | Home screen widget |
| SwiftUI.framework | UI |
| SwiftData | Persistence |
| Swift Charts | Stats visualization |
Build System¶
XcodeGen (project.yml) generates the Xcode project. Targets: FastE (app), FastETests (unit), FastEUITests (UI), FastEWidget (extension).