Skip to content

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

  1. FastEApp creates a ModelContainer for FastingSession and injects it via .modelContainer()
  2. AppState and FastingService are injected as @EnvironmentObject into the view hierarchy
  3. ContentView passes modelContext to FastingService on appear
  4. Views use @Query for read access and call FastingService methods for writes
  5. Widget reads shared state from App Groups (UserDefaults suite), written by WidgetDataProvider

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 — current ScheduleSelection, persisted to UserDefaults (selectedScheduleV2 key)
  • customSchedules — array of CustomSchedule, persisted to UserDefaults
  • hasCompletedOnboarding — 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 a FastingSession in SwiftData, schedules a notification, updates widget
  • stopFast() — sets endDate, marks completion, cancels notifications, syncs to HealthKit if completed
  • cancelFast() — ends the fast as cancelled
  • deleteFast(_:) — removes a session from SwiftData
  • updateSession(_:startDate:endDate:) — edits an existing session, recalculates completion, reschedules notifications if active
  • calculateStreak(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

ContentView gates on hasCompletedOnboarding:

  • Not onboardedOnboardingView (schedule selection + Get Started)
  • OnboardedMainTabView with 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. Shows TimerRingView with animated progress, elapsed/remaining time, start/stop/cancel buttons, "started earlier" option
  • TimerRingView — circular progress indicator
  • SchedulePickerView — preset + custom schedule selection
  • CustomScheduleEditorView — create/edit custom schedules

History Tab

  • HistoryView@Query-powered list of FastingSession sorted by date
  • FastDetailView — 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 week
  • StreakView — current consecutive-day streak display

Settings Tab

  • SettingsView — schedule selection, notifications, CSV export, Apple Health, CutiE feedback, analytics toggle
  • HealthSettingsView — HealthKit sync toggle, sync past sessions, open Health app
  • NotificationSettingsView — notification permission management

Shared Components

  • FastingSummaryCard — reusable card showing session summary (schedule, duration, status)
  • FeedbackToolbarButtons — CutiE inbox + send feedback toolbar items
  • ProgressRing — generic circular progress indicator

Widget Extension (FastEWidget)

WidgetKit extension (no.invotek.FastE.Widget) displaying fasting status on the home screen.

  • FastEWidgetTimelineProvider that reads state from App Groups
  • FastEWidgetBundle — widget bundle entry point
  • WidgetDataProvider — 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).