Files
Reflect/README.md
Trey T 7683717240 Add full repo README with feature docs and CBT reflection deep dive
Covers every feature with how-it-works, selling-point framing, and
clickable source links — targeted at both developers and business
partners. Includes an in-depth section on the guided reflection flow
(Socratic templating, cognitive-distortion routing, evidence step).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:08:12 -05:00

34 KiB
Raw Blame History

Reflect

A private, on-device iOS mood tracker that turns a five-second daily check-in into durable self-knowledge. Users rate their day on a 5-point scale, and the app turns that signal into calendars, trends, Apple-Intelligence-powered insights, guided CBT reflections, watch complications, widgets, Live Activities, and shareable summaries.

Platform: iOS 16+ · iPadOS · watchOS · macOS (Catalyst) Language: Swift / SwiftUI Data: SwiftData + CloudKit (private database) AI: Apple Foundation Models (on-device, iOS 26+) Monetization: 30-day free trial → monthly / yearly subscription (StoreKit 2)


Table of Contents


Elevator pitch

Most mood trackers collect data. Reflect converts it into insight. The five-second daily tap is only the entry point — behind it sits a full behavioral-health stack:

  • On-device Apple Intelligence (Foundation Models) writes your weekly digest, surfaces patterns, and runs a cognitive-behavioral-therapy-aligned reflection flow. Nothing leaves the phone.
  • CloudKit sync keeps iPhone and Apple Watch in step without an account.
  • HealthKit State of Mind writes your rating back into Apple Health and reads activity / sleep / HRV so the app can correlate mood with the rest of your life.
  • A CBT-aligned Guided Reflection detects cognitive distortions in your automatic thoughts and rewrites the follow-up question to match the specific distortion — the actual mechanism of change in evidence-based CBT.
  • Four surfaces to log from: app, home/lock-screen widget, Control Center widget, Apple Watch complication. One tap, anywhere.

The thesis: a mood tracker is only useful if the patterns it surfaces can drive behavior change. Reflect is designed from the top down around the CBT/ACT evidence base, not screen time.


Feature map

Area What it is Why it sells Source
5-point mood log Horrible → Great daily rating The lowest-friction signal that still captures variance Shared/Models/Mood.swift
Day view Chronological list with notes, photos, weather Diary + heatmap in one scroll Shared/Views/DayView/
Month view Calendar heatmap "What kind of month was this?" in one glance Shared/Views/MonthView/
Year view 12-month heatmap + filters See multi-month seasonality Shared/Views/YearView/
AI Insights Patterns, predictions, advice Turns data into behavior change Shared/Views/InsightsView/ · FoundationModelsInsightService.swift
Weekly Digest AI-written summary of the last 7 days Sunday-night ritual, builds habit FoundationModelsDigestService.swift
Auto-tagging AI extracts themes (work, sleep, family…) Enables mood↔theme correlation FoundationModelsTagService.swift
AI Reports + PDF Clinical multi-week reports Shareable with therapist ReportPDFGenerator.swift
Guided Reflection CBT Thought Record / ACT Defusion / Behavioral Activation Evidence-based, not a mood journal GuidedReflectionView.swift · GuidedReflection.swift
Cognitive distortion detector Routes reframes to specific distortion The mechanism of change in CBT CognitiveDistortionDetector.swift
Widgets 6 widget families Logging from the home screen ReflectWidget/
Live Activities Lock Screen + Dynamic Island streak Keeps the habit present ReflectLiveActivity.swift · MoodStreakActivity.swift
Watch app Log and view on wrist Frictionless logging Reflect Watch App/
Complications Streak / last mood on watch face Ambient nudge to log ReflectComplication.swift
Control Center iOS 18 Control Center widget One-tap log from anywhere ReflectMoodControlWidget.swift
Siri / Shortcuts App Intents for "Log mood great" Hands-free logging AppShortcuts.swift · SharedMoodIntent.swift
HealthKit State of Mind write + activity read Deep Apple Health integration HealthKitManager.swift · HealthService.swift
WeatherKit Attach weather to each entry Mood↔weather correlation in Insights WeatherManager.swift
Photos Attach a photo per day Visual journal PhotoManager.swift
Biometric lock Face ID / Touch ID / Optic ID Private data stays private BiometricAuthManager.swift
Sharing templates Mood-style social cards Organic growth channel Shared/Views/Sharing/ · Shared/Views/SharingTemplates/
Custom icon widget User-designed widget layout Personalization as retention Shared/Views/CustomWidget/
Theming Color sets, icon packs, shapes, personality Make it feel like yours Shared/Views/CustomizeView/
Export CSV / JSON / PDF No lock-in — builds trust ExportService.swift
Subscription Monthly / yearly + 30-day trial StoreKit 2, offer codes IAPManager.swift

Core features in depth

1. Daily mood logging

  • What: A 5-point ordinal scale — Horrible, Bad, Average, Good, Great — defined in Shared/Models/Mood.swift.
  • How: Every mood entry — from the app, a widget, the watch, Siri, the lock-screen Live Activity, a push-notification action, or an imported CSV row — funnels through a single choke point: MoodLogger.shared.logMood(...). That one method runs the write to SwiftData, the HealthKit sync, streak recomputation, widget-timeline reload, Watch Connectivity nudge, TipKit parameter update, Live Activity refresh, and analytics event.
  • Why 5 points: Enough to capture variance, few enough to fit on a widget. The ordinal scale is what lets us run MoodMetrics.swift over it without arguing about arithmetic on emoji.
  • Source-of-truth rule: never insert into modelContext directly — always go through MoodLogger, or side effects silently break. This invariant is enforced in CLAUDE.md.

2. Day / Month / Year views

Three zoom levels over the same data, selected via the tab bar in MainTabView.swift.

  • Day — Chronological list of entries with notes, photo, weather card, and the "open reflection" CTA. Backed by DayViewViewModel.
  • Month — Classic calendar grid, each cell tinted by its mood. Tap to drill into that date. Shape is user-configurable (circle / square / rounded / diamond).
  • Year — 12 mini calendars stacked for seasonality. Users can filter by weekday (e.g. "only show Mondays") to isolate work-week patterns.

3. Insights (AI)

  • What: An on-device AI pass over your mood history that returns a short, human-readable list of patterns, predictions, advice, and achievements, tinted to the dominant mood.
  • How: FoundationModelsInsightService.swift uses Apple Foundation Models (LanguageModelSession with a @Generable schema defined in AIInsight.swift). The prompt is built by MoodDataSummarizer.swift, which precomputes mood distribution, day-of-week patterns, streaks, weather correlations, and HealthKit correlations so the model only has to narrate facts we already have.
  • Caching: 1-hour result cache — regenerating is one tap.
  • Selling point: Zero network, zero account, zero data leaves the device.

4. Weekly Digest (AI)

  • What: Every week the app writes you a one-paragraph summary with a headline, a highlight ("your best day was Thursday — walk after work"), and an intention for next week.
  • How: FoundationModelsDigestService.swift generates against the AIWeeklyDigest schema. Scheduled by BGTask.runWeeklyDigestTask so it's ready on Sunday night without the user opening the app. Delivered via a local notification; rendered in WeeklyDigestCardView.swift.
  • Why it sells: A weekly "here's what happened" ritual is how habits become durable.

5. Auto-tagging (AI)

  • What: Themes like work / sleep / family / exercise / stress are extracted automatically from each entry's notes and reflection.
  • How: FoundationModelsTagService.swift returns an AIEntryTags. Insights cross-reference the tag with the mood to answer questions like "what happens to your mood on days you write about sleep?".
  • Why it sells: Users don't have to tag manually — the signal is free.

6. AI Reports + PDF export

  • What: A multi-week clinical-style report with overview stats, week-by-week breakdowns, and visualizations, exportable as a PDF to share with a therapist or doctor.
  • How: ReportPDFGenerator.swift renders an HTML template through WKWebView and rasterizes to PDF. Data shape is AIReport; UI lives in Shared/Views/InsightsView/ReportsView.swift.
  • Why it sells: Unlocks clinical use cases — a real moat vs. tap-to-log competitors.

7. Guided Reflection — deep dive

See the dedicated Guided Reflection section below.

8. Widgets

All six live in ReflectWidget/ and are bundled by WidgetBundle.swift.

Widget File Purpose
Vote ReflectVoteWidget.swift Interactive 5-button logger — small/medium, no app launch
Timeline ReflectTimelineWidget.swift Recent-days strip; pre-vote shows buttons, post-vote shows stats
Graphic ReflectGraphicWidget.swift Large mood art widget
Icon ReflectIconWidget.swift User-customized widget (see CustomWidget)
Mood Control ReflectMoodControlWidget.swift Control Center widget — iOS 18
Live Activity ReflectLiveActivity.swift Lock Screen + Dynamic Island

Widgets can't reach CloudKit or HealthKit from their process, so they read through ExtensionDataProvider.swift, which talks to the App Group container directly. When the widget writes a vote via an AppIntent, the main app picks it up and runs the deferred MoodLogger side effects next launch.

9. Live Activities

  • What: A streak card on the Lock Screen and in the Dynamic Island, updating in real time when you log or when the voting window opens.
  • How: MoodStreakActivity.swift starts the ActivityKit activity; ReflectLiveActivity.swift is the view. LiveActivityScheduler decides when the activity should be on-screen (typically after the daily reminder fires until the day is logged).
  • Why it sells: Continuous visible nudge without being a notification — drives logging consistency.

10. Watch app + complications

11. Control Center, Siri, App Intents, Shortcuts

12. HealthKit State of Mind

  • HealthKitManager.swift — Writes each mood into Apple Health's State of Mind store (valence mapped from the 5-point scale).
  • HealthService.swift — Reads activity, exercise time, heart rate, HRV, and sleep and folds them into the Insights prompt for mood↔body correlation.
  • Why it sells: Reflect becomes the primary State-of-Mind logger for users already bought into Apple Health.

13. WeatherKit correlation

14. Photos on entries

15. Face ID / Touch ID lock

  • BiometricAuthManager.swift — Detects Face ID / Touch ID / Optic ID, gracefully falls back to the device passcode, and gates the whole app behind a biometric unlock if the user opts in.

16. Sharing templates

17. Customization

Shared/Views/CustomizeView/ lets users pick:

Axis Protocol Options
Color set MoodTintable Default, Pastel, Neon, Ocean, Forest, Sunset
Icon / emoji MoodImagable FontAwesome, Emoji, Hand Emoji
Shape Shapes.swift Circle, Square, Diamond, Rounded
Theme Theme.swift System / Light / Dark / iFeel gradient
Personality PersonalityPackable Default, Coach, Zen, Best Friend, Data Analyst
App icon Shared/Views/CustomIcon/ Multiple home-screen icons

18. Notifications + personality packs

  • LocalNotification.swift — Daily reminder with inline mood buttons; notification copy is written by the selected PersonalityPackable so the "Nice" pack asks gently and the "Rude" pack heckles you.
  • AppDelegate.swift — Handles the action buttons on the notification to route the vote through MoodLogger.

19. Export / Import

  • ExportService.swift — CSV, JSON, and PDF export of the full entry history with notes, weather, reflections, photos, and entry source preserved.
  • Why it sells: No lock-in. Users know their data can leave, which makes them trust it going in.

20. Subscriptions

  • IAPManager.swift — StoreKit 2, monthly (com.88oakapps.reflect.IAP.subscriptions.monthly) and yearly, 30-day free trial counted from firstLaunchDate, paywall UI in ReflectSubscriptionStoreView.swift.
  • Gated behind paywall: AI Insights, Weekly Digest, AI Reports, Guided Reflection AI feedback, HealthKit correlation insights, Apple Watch premium complications. Core logging + history are free forever.

Guided Reflection — deep dive

Source files: — Model: Shared/Models/GuidedReflection.swift — Distortion detector: Shared/Services/CognitiveDistortionDetector.swift — AI feedback: Shared/Services/FoundationModelsReflectionService.swift — Feedback model: Shared/Models/AIReflectionFeedback.swift — UI: Shared/Views/GuidedReflectionView.swift — Feedback UI: Shared/Views/ReflectionFeedbackView.swift — Info sheet: Shared/Views/GuidedReflectionInfoView.swift — Plan doc: docs/guided-reflection-cbt-plan.md — Flowchart: docs/guided-reflection-flowchart.html

Why this exists

Most mood trackers stop at logging. Reflect's reflection flow is a short, mood-adaptive, CBT-aligned guided exercise. The goal is not to make the user journal more — it's to run a known therapeutic mechanism that has evidence for changing the relationship with a thought.

Three mood tiers → three different evidence-based frameworks:

Mood Framework Mechanism Question count
Great / Good Behavioral Activation (BA) Savor + plan to repeat 3
Average ACT Cognitive Defusion Notice thought, loosen its grip, re-orient to values 4
Bad / Horrible CBT Thought Record Identify situation, surface automatic thought, check for distortion, examine evidence, generate a balanced reframe 5

These are routed from the mood value via MoodCategory(from:) and rendered as a stepped sheet from GuidedReflectionView.swift.

The Socratic template system

The defining feature of Socratic questioning in CBT is that each question builds on the user's previous answer. Static question lists are not Socratic. We implemented this with QuestionTemplate — each question has a text with an optional %@ placeholder and a placeholderRef: Int? pointing at which earlier question's answer to inject.

Example (negative path):

Q1: "What happened today that affected your mood?"
→ user: "My boss criticized my presentation in front of the team"

Q2: "What thought kept coming back about 'My boss criticized my presentation…'?"
→ user: "I'm not cut out for this job"

Q3 (distortion-specific, see below): "Is 'I'm not cut out for this job' something you are, or something you did?"

Q4 (evidence): "What evidence supports 'I'm not cut out for this job', and what challenges it?"

Q5 (reframe): "Looking at 'I'm not cut out for this job' again — what's a more balanced way to see it?"

Injections are truncated to a sentence boundary or 60 characters by GuidedReflection.truncatedForInjection so long answers never break grammar. Templates are fully localized — each language controls its own %@ position so grammar stays natural.

Cognitive distortion detection

CognitiveDistortionDetector.swift classifies the Q2 answer into one of seven types using localized keyword lists (deterministic, offline, privacy-preserving — not ML). This was a deliberate product choice: rule-based is inspectable, predictable, and works with zero latency.

Distortion Example phrasing Q3 reframe prompt
overgeneralization "always", "never", "everyone" "Can you think of one counter-example to '%@'?"
shouldStatement "should", "must", "have to" "Where did the rule 'I should …' come from? Is it still serving you?"
labeling "I am [trait]" "Is '%@' something you are, or something you did?"
personalization "my fault", "because of me" "What other factors, besides you, contributed to this?"
catastrophizing "ruined", "can't recover" "What's the worst case? What's the most likely case?"
mindReading "thinks I'm", "hates me" "What evidence do you have for that interpretation?"
unknown (no match) Falls back to the generic perspective check

Keywords live in Localizable.xcstrings under distortion_*_keywords keys so each locale tunes its own detection rules. Priority order is specific → general so that "I always ruin everything" classifies as catastrophizing first, overgeneralization second.

Evidence examination

The negative path explicitly inserts a dedicated evidence step (Q4) — supporting and contradicting evidence both. This is the single most load-bearing step in a real CBT Thought Record and was previously missing.

Intensity tracking

GuidedReflection carries optional preIntensity and postIntensity (010). CBT literature emphasizes measuring the emotion's intensity before and after the thought work — the delta is the efficacy signal. The AI feedback stage references the shift ("you moved from an 8 to a 5") when it narrates the reflection.

AI feedback stage

On completion FoundationModelsReflectionService generates an AIReflectionFeedback with three slots — affirmation, observation, takeaway — rendered by ReflectionFeedbackView.swift. Tone is driven by the user's selected PersonalityPackable (Coach / Zen / Best Friend / Data Analyst / Default). This is gated behind the subscription + iOS 26 Apple Intelligence — when unavailable, the reflection still saves normally and the feedback view degrades gracefully.

Back-compat

Old reflections saved with 4 negative-path responses (pre-evidence step) still decode and still count as complete. GuidedReflection.isComplete detects the legacy shape so users never lose completed work across updates.

Full flow diagram

Open docs/guided-reflection-flowchart.html in a browser for the visual flow. The plan doc in docs/guided-reflection-cbt-plan.md walks through the phased rationale — phase 1 (templates + intensity), phase 2 (distortion detection + evidence), phase 3 (AI-generated final question).


Architecture

Pattern: MVVM + SwiftUI, singletons for cross-cutting concerns. Entry point: Shared/ReflectApp.swift

Layers

┌─────────────────────────────────────────────────────────────────┐
│  Views (SwiftUI)                     Shared/Views/              │
│  ViewModels (@MainActor)             colocated with views       │
├─────────────────────────────────────────────────────────────────┤
│  Services (singletons)               Shared/Services/           │
│    MoodLogger   AnalyticsManager   IAPManager                   │
│    HealthKitManager   WeatherManager   LocationManager          │
│    FoundationModels{Insight,Digest,Reflection,Tag}Service       │
│    CognitiveDistortionDetector                                  │
├─────────────────────────────────────────────────────────────────┤
│  Persistence                         Shared/Persisence/         │
│    DataController (GET/ADD/UPDATE/DELETE)                       │
│    SharedModelContainer — SwiftData + CloudKit + App Group      │
│    ExtensionDataProvider — widget/watch read path               │
├─────────────────────────────────────────────────────────────────┤
│  Models                              Shared/Models/             │
│    MoodEntryModel (@Model)   Mood   GuidedReflection            │
│    AIInsight   AIWeeklyDigest   AIReflectionFeedback   AIReport │
└─────────────────────────────────────────────────────────────────┘

Data flow for a single mood log

tap → DayViewViewModel.add()
      └─► MoodLogger.shared.logMood()         ← ALL entry points converge here
          ├─► DataController.shared.add()     — SwiftData insert + save
          ├─► HealthKitManager.write()        — State of Mind
          ├─► streak recompute
          ├─► WidgetCenter.reloadAllTimelines()
          ├─► WatchConnectivityManager.nudge()
          ├─► LiveActivityScheduler.update()
          ├─► TipKit parameter update
          └─► AnalyticsManager.shared.track()

Widgets and the watch bypass MoodLogger when the main app isn't running and write through ExtensionDataProvider; MoodLogger.applySideEffects() catches up on next launch. Detailed rules: CLAUDE.md.


Data model

Primary entity: MoodEntryModel — a SwiftData @Model with:

Field Type Notes
moodValue Int 04 (Horrible → Great), 5 = missing, 6 = placeholder
forDate Date The logical day being rated
timestamp Date When the entry was written
weekDay Int 17, denormalized for fast filtering
entryType Int listView / widget / watch / shortcut / filledInMissing / notification / header / siri / controlCenter / liveActivity
notes String? Free text
photoID String? Path into App Group container
weatherJSON String? WeatherData
reflectionJSON String? GuidedReflection
tagsJSON String? AIEntryTags

All fields have defaults — required by CloudKit. Sync is automatic via SwiftData's built-in CloudKit integration.


Directory map

Path Contents
Shared/ All cross-platform code
Shared/Models/ Domain types, SwiftData models, AI schemas
Shared/Views/ SwiftUI views, grouped by feature
Shared/Services/ Singletons for AI, HealthKit, Weather, Export, etc.
Shared/Persisence/ SwiftData layer (note: directory name has a typo — intentional, for historic reasons)
Shared/Onboarding/ First-launch flow
Shared/Utilities/ Small helpers
ReflectWidget/ Widget + Control Center + Live Activity extension
Reflect Watch App/ watchOS app + complications
Reflect/ iOS app target assets, Info.plist, Localizable.xcstrings
Tests iOS/ XCUITest suites — see docs/XCUITest-Authoring.md
ReflectTests/ XCTest unit tests
Tests macOS/ macOS target tests
landing_page/ Marketing site
docs/ Design docs, ASO, QA plan, CBT plan, competitors
scripts/ Build/dev utilities
ads/ Marketing creative

Build, run, test

# Build
xcodebuild -project Reflect.xcodeproj \
  -scheme "Reflect (iOS)" \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build

# Run all tests
xcodebuild -project Reflect.xcodeproj \
  -scheme "Reflect (iOS)" \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' test

# Run one UI suite
xcodebuild -project Reflect.xcodeproj \
  -scheme "Reflect (iOS)" \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
  -only-testing:"Tests iOS/Tests_iOS" test

UI test conventions are mandatory — see docs/XCUITest-Authoring.md, Tests iOS/Helpers/BaseUITestCase.swift, and Shared/AccessibilityIdentifiers.swift.


Localization

Format: String Catalog at Reflect/Localizable.xcstrings.

Shipping languages: English · German · Spanish · French · Japanese · Korean · Portuguese (Brazil).

All user-facing strings use String(localized:). "Reflect" is a brand name — untranslated. Distortion-detection keywords are per-locale (see Cognitive distortion detection).


Privacy & security

  • No account, no backend, no third-party server ever sees mood data. Data lives in SwiftData, synced through the user's own private CloudKit database.
  • AI is 100% on-device — Foundation Models runs locally; nothing is sent to Anthropic, OpenAI, Apple servers, or us.
  • Analytics (PostHog via AnalyticsManager) captures UI events, never mood content, and the user can opt out in settings.
  • Optional biometric lock (BiometricAuthManager) gates the app behind Face ID / Touch ID / Optic ID.
  • HealthKit read/write is permission-gated and only triggered on explicit user opt-in.

Configuration reference

Setting Value
Bundle ID (iOS) com.88oakapps.reflect
App Group (prod) group.com.88oakapps.reflect
App Group (debug) group.com.88oakapps.reflect.debug
CloudKit (prod) iCloud.com.88oakapps.reflect
CloudKit (debug) iCloud.com.88oakapps.reflect.debug
Subscription group 21951685
Monthly product ID com.88oakapps.reflect.IAP.subscriptions.monthly
Yearly product ID com.88oakapps.reflect.IAP.subscriptions.yearly
Free trial 30 days from firstLaunchDate
URL scheme reflect:// (e.g. reflect://subscribe)
BGTask — missing dates com.88oakapps.reflect.dbUpdateMissing
BGTask — weather retry com.88oakapps.reflect.weatherRetry
BGTask — weekly digest com.88oakapps.reflect.weeklyDigest
Logger subsystem com.88oakapps.reflect

For deeper architectural rules, concurrency constraints, data-access invariants, and edge-case gotchas, see CLAUDE.md. For ASO strategy and screenshot plans see docs/.