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>
477 lines
34 KiB
Markdown
477 lines
34 KiB
Markdown
# 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](#elevator-pitch)
|
||
- [Feature map](#feature-map)
|
||
- [Core features in depth](#core-features-in-depth)
|
||
- [Daily mood logging](#1-daily-mood-logging)
|
||
- [Day / Month / Year views](#2-day--month--year-views)
|
||
- [Insights (AI)](#3-insights-ai)
|
||
- [Weekly Digest (AI)](#4-weekly-digest-ai)
|
||
- [Auto-tagging (AI)](#5-auto-tagging-ai)
|
||
- [AI Reports + PDF export](#6-ai-reports--pdf-export)
|
||
- [Guided Reflection (CBT / ACT / BA)](#7-guided-reflection--deep-dive)
|
||
- [Widgets](#8-widgets)
|
||
- [Live Activities](#9-live-activities)
|
||
- [Watch app + complications](#10-watch-app--complications)
|
||
- [Control Center, Siri, App Intents, Shortcuts](#11-control-center-siri-app-intents-shortcuts)
|
||
- [HealthKit State of Mind](#12-healthkit-state-of-mind)
|
||
- [WeatherKit correlation](#13-weatherkit-correlation)
|
||
- [Photos on entries](#14-photos-on-entries)
|
||
- [Face ID / Touch ID lock](#15-face-id--touch-id-lock)
|
||
- [Sharing templates](#16-sharing-templates)
|
||
- [Customization](#17-customization)
|
||
- [Notifications + personality packs](#18-notifications--personality-packs)
|
||
- [Export / Import](#19-export--import)
|
||
- [Subscriptions](#20-subscriptions)
|
||
- [Guided Reflection — deep dive](#guided-reflection--deep-dive)
|
||
- [Architecture](#architecture)
|
||
- [Data model](#data-model)
|
||
- [Directory map](#directory-map)
|
||
- [Build, run, test](#build-run-test)
|
||
- [Localization](#localization)
|
||
- [Privacy & security](#privacy--security)
|
||
- [Configuration reference](#configuration-reference)
|
||
|
||
---
|
||
|
||
## 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`](Shared/Models/Mood.swift) |
|
||
| Day view | Chronological list with notes, photos, weather | Diary + heatmap in one scroll | [`Shared/Views/DayView/`](Shared/Views/DayView) |
|
||
| Month view | Calendar heatmap | "What kind of month was this?" in one glance | [`Shared/Views/MonthView/`](Shared/Views/MonthView) |
|
||
| Year view | 12-month heatmap + filters | See multi-month seasonality | [`Shared/Views/YearView/`](Shared/Views/YearView) |
|
||
| AI Insights | Patterns, predictions, advice | Turns data into behavior change | [`Shared/Views/InsightsView/`](Shared/Views/InsightsView) · [`FoundationModelsInsightService.swift`](Shared/Services/FoundationModelsInsightService.swift) |
|
||
| Weekly Digest | AI-written summary of the last 7 days | Sunday-night ritual, builds habit | [`FoundationModelsDigestService.swift`](Shared/Services/FoundationModelsDigestService.swift) |
|
||
| Auto-tagging | AI extracts themes (work, sleep, family…) | Enables mood↔theme correlation | [`FoundationModelsTagService.swift`](Shared/Services/FoundationModelsTagService.swift) |
|
||
| AI Reports + PDF | Clinical multi-week reports | Shareable with therapist | [`ReportPDFGenerator.swift`](Shared/Services/ReportPDFGenerator.swift) |
|
||
| Guided Reflection | CBT Thought Record / ACT Defusion / Behavioral Activation | Evidence-based, not a mood journal | [`GuidedReflectionView.swift`](Shared/Views/GuidedReflectionView.swift) · [`GuidedReflection.swift`](Shared/Models/GuidedReflection.swift) |
|
||
| Cognitive distortion detector | Routes reframes to specific distortion | The mechanism of change in CBT | [`CognitiveDistortionDetector.swift`](Shared/Services/CognitiveDistortionDetector.swift) |
|
||
| Widgets | 6 widget families | Logging from the home screen | [`ReflectWidget/`](ReflectWidget) |
|
||
| Live Activities | Lock Screen + Dynamic Island streak | Keeps the habit present | [`ReflectLiveActivity.swift`](ReflectWidget/ReflectLiveActivity.swift) · [`MoodStreakActivity.swift`](Shared/MoodStreakActivity.swift) |
|
||
| Watch app | Log and view on wrist | Frictionless logging | [`Reflect Watch App/`](Reflect%20Watch%20App) |
|
||
| Complications | Streak / last mood on watch face | Ambient nudge to log | [`ReflectComplication.swift`](Reflect%20Watch%20App/ReflectComplication.swift) |
|
||
| Control Center | iOS 18 Control Center widget | One-tap log from anywhere | [`ReflectMoodControlWidget.swift`](ReflectWidget/ReflectMoodControlWidget.swift) |
|
||
| Siri / Shortcuts | App Intents for "Log mood great" | Hands-free logging | [`AppShortcuts.swift`](Shared/AppShortcuts.swift) · [`SharedMoodIntent.swift`](Shared/SharedMoodIntent.swift) |
|
||
| HealthKit | State of Mind write + activity read | Deep Apple Health integration | [`HealthKitManager.swift`](Shared/HealthKitManager.swift) · [`HealthService.swift`](Shared/Services/HealthService.swift) |
|
||
| WeatherKit | Attach weather to each entry | Mood↔weather correlation in Insights | [`WeatherManager.swift`](Shared/Services/WeatherManager.swift) |
|
||
| Photos | Attach a photo per day | Visual journal | [`PhotoManager.swift`](Shared/Services/PhotoManager.swift) |
|
||
| Biometric lock | Face ID / Touch ID / Optic ID | Private data stays private | [`BiometricAuthManager.swift`](Shared/Services/BiometricAuthManager.swift) |
|
||
| Sharing templates | Mood-style social cards | Organic growth channel | [`Shared/Views/Sharing/`](Shared/Views/Sharing) · [`Shared/Views/SharingTemplates/`](Shared/Views/SharingTemplates) |
|
||
| Custom icon widget | User-designed widget layout | Personalization as retention | [`Shared/Views/CustomWidget/`](Shared/Views/CustomWidget) |
|
||
| Theming | Color sets, icon packs, shapes, personality | Make it feel like yours | [`Shared/Views/CustomizeView/`](Shared/Views/CustomizeView) |
|
||
| Export | CSV / JSON / PDF | No lock-in — builds trust | [`ExportService.swift`](Shared/Services/ExportService.swift) |
|
||
| Subscription | Monthly / yearly + 30-day trial | StoreKit 2, offer codes | [`IAPManager.swift`](Shared/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`](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(...)`](Shared/MoodLogger.swift)**. 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`](Shared/Models/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`](CLAUDE.md#mutation--write-patterns).
|
||
|
||
### 2. Day / Month / Year views
|
||
|
||
Three zoom levels over the same data, selected via the tab bar in [`MainTabView.swift`](Shared/Views/MainTabView.swift).
|
||
|
||
- **[Day](Shared/Views/DayView)** — Chronological list of entries with notes, photo, weather card, and the "open reflection" CTA. Backed by [`DayViewViewModel`](Shared/Views/DayView).
|
||
- **[Month](Shared/Views/MonthView)** — Classic calendar grid, each cell tinted by its mood. Tap to drill into that date. Shape is user-configurable (circle / square / rounded / diamond).
|
||
- **[Year](Shared/Views/YearView)** — 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`](Shared/Services/FoundationModelsInsightService.swift) uses Apple Foundation Models (`LanguageModelSession` with a `@Generable` schema defined in [`AIInsight.swift`](Shared/Models/AIInsight.swift)). The prompt is built by [`MoodDataSummarizer.swift`](Shared/Services/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`](Shared/Services/FoundationModelsDigestService.swift) generates against the [`AIWeeklyDigest`](Shared/Models/AIWeeklyDigest.swift) schema. Scheduled by [`BGTask.runWeeklyDigestTask`](Shared/BGTask.swift) so it's ready on Sunday night without the user opening the app. Delivered via a local notification; rendered in [`WeeklyDigestCardView.swift`](Shared/Views/InsightsView).
|
||
- **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`](Shared/Services/FoundationModelsTagService.swift) returns an [`AIEntryTags`](Shared/Models/AIEntryTags.swift). 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`](Shared/Services/ReportPDFGenerator.swift) renders an HTML template through WKWebView and rasterizes to PDF. Data shape is [`AIReport`](Shared/Models/AIReport.swift); UI lives in [`Shared/Views/InsightsView/ReportsView.swift`](Shared/Views/InsightsView).
|
||
- **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](#guided-reflection--deep-dive).
|
||
|
||
### 8. Widgets
|
||
|
||
All six live in [`ReflectWidget/`](ReflectWidget) and are bundled by [`WidgetBundle.swift`](ReflectWidget/WidgetBundle.swift).
|
||
|
||
| Widget | File | Purpose |
|
||
|---|---|---|
|
||
| Vote | [`ReflectVoteWidget.swift`](ReflectWidget/ReflectVoteWidget.swift) | Interactive 5-button logger — small/medium, no app launch |
|
||
| Timeline | [`ReflectTimelineWidget.swift`](ReflectWidget/ReflectTimelineWidget.swift) | Recent-days strip; pre-vote shows buttons, post-vote shows stats |
|
||
| Graphic | [`ReflectGraphicWidget.swift`](ReflectWidget/ReflectGraphicWidget.swift) | Large mood art widget |
|
||
| Icon | [`ReflectIconWidget.swift`](ReflectWidget/ReflectIconWidget.swift) | User-customized widget (see [CustomWidget](Shared/Views/CustomWidget)) |
|
||
| Mood Control | [`ReflectMoodControlWidget.swift`](ReflectWidget/ReflectMoodControlWidget.swift) | **Control Center** widget — iOS 18 |
|
||
| Live Activity | [`ReflectLiveActivity.swift`](ReflectWidget/ReflectLiveActivity.swift) | Lock Screen + Dynamic Island |
|
||
|
||
Widgets can't reach CloudKit or HealthKit from their process, so they read through [`ExtensionDataProvider.swift`](Shared/Persisence/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`](Shared/MoodStreakActivity.swift) starts the `ActivityKit` activity; [`ReflectLiveActivity.swift`](ReflectWidget/ReflectLiveActivity.swift) is the view. [`LiveActivityScheduler`](Shared/MoodStreakActivity.swift) 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
|
||
|
||
- **[`Reflect Watch App/ContentView.swift`](Reflect%20Watch%20App/ContentView.swift)** — Full voting UI on the wrist. Writes go through the shared CloudKit database; UI refresh for the phone happens over [`WatchConnectivityManager`](Shared/Services/WatchConnectivityManager.swift).
|
||
- **[`ReflectComplication.swift`](Reflect%20Watch%20App/ReflectComplication.swift)** — Circular / rectangular / corner complications showing streak, last mood, or graphic.
|
||
- **Selling point:** the watch is the lowest-friction surface — users raise their wrist, tap, done.
|
||
|
||
### 11. Control Center, Siri, App Intents, Shortcuts
|
||
|
||
- **[`AppShortcuts.swift`](Shared/AppShortcuts.swift)** — Exposes "Log a mood" and "Open Reflect" as App Intents so Siri and Shortcuts can call them.
|
||
- **[`SharedMoodIntent.swift`](Shared/SharedMoodIntent.swift)** — The intent shared across widget buttons and voice actions.
|
||
- **[`ReflectMoodControlWidget.swift`](ReflectWidget/ReflectMoodControlWidget.swift)** — iOS 18 Control Center widget that opens the app pre-scrolled to the log screen.
|
||
|
||
### 12. HealthKit State of Mind
|
||
|
||
- **[`HealthKitManager.swift`](Shared/HealthKitManager.swift)** — Writes each mood into Apple Health's State of Mind store (valence mapped from the 5-point scale).
|
||
- **[`HealthService.swift`](Shared/Services/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
|
||
|
||
- **[`WeatherManager.swift`](Shared/Services/WeatherManager.swift)** — Pulls current conditions from WeatherKit using a one-shot location from [`LocationManager.swift`](Shared/Services/LocationManager.swift) (10-min cache, 15 s timeout) and attaches a [`WeatherData`](Shared/Models/WeatherData.swift) snapshot to the entry.
|
||
- **Retry:** failures queue for [`BGTask.runWeatherRetryTask`](Shared/BGTask.swift) so weather fills in overnight.
|
||
|
||
### 14. Photos on entries
|
||
|
||
- **[`PhotoManager.swift`](Shared/Services/PhotoManager.swift)** — JPEG at 0.8, stored in the App Group container, 200×200 thumbnail cached for list rendering.
|
||
- **Picker:** [`PhotoPickerView.swift`](Shared/Views/PhotoPickerView.swift) / [`ImagePickerGridView.swift`](Shared/Views/ImagePickerGridView.swift).
|
||
|
||
### 15. Face ID / Touch ID lock
|
||
|
||
- **[`BiometricAuthManager.swift`](Shared/Services/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
|
||
|
||
- **[`Shared/Views/Sharing/`](Shared/Views/Sharing)** — The sharing flow.
|
||
- **[`Shared/Views/SharingTemplates/`](Shared/Views/SharingTemplates)** — Visual templates (monthly recap, streak brag, mood pie).
|
||
- **[`SharingScreenshotExporter.swift`](Shared/Services/SharingScreenshotExporter.swift)** — Debug tool that renders every template for marketing.
|
||
- **Why it sells:** Shareable cards are a zero-cost organic-growth channel — each user who shares is a free ad.
|
||
|
||
### 17. Customization
|
||
|
||
[`Shared/Views/CustomizeView/`](Shared/Views/CustomizeView) lets users pick:
|
||
|
||
| Axis | Protocol | Options |
|
||
|---|---|---|
|
||
| Color set | [`MoodTintable`](Shared/Models/MoodTintable.swift) | Default, Pastel, Neon, Ocean, Forest, Sunset |
|
||
| Icon / emoji | [`MoodImagable`](Shared/Models/MoodImagable.swift) | FontAwesome, Emoji, Hand Emoji |
|
||
| Shape | [`Shapes.swift`](Shared/Models/Shapes.swift) | Circle, Square, Diamond, Rounded |
|
||
| Theme | [`Theme.swift`](Shared/Models/Theme.swift) | System / Light / Dark / iFeel gradient |
|
||
| Personality | [`PersonalityPackable`](Shared/Models/PersonalityPackable.swift) | Default, Coach, Zen, Best Friend, Data Analyst |
|
||
| App icon | [`Shared/Views/CustomIcon/`](Shared/Views/CustomIcon) | Multiple home-screen icons |
|
||
|
||
### 18. Notifications + personality packs
|
||
|
||
- **[`LocalNotification.swift`](Shared/LocalNotification.swift)** — Daily reminder with inline mood buttons; notification copy is written by the selected [`PersonalityPackable`](Shared/Models/PersonalityPackable.swift) so the "Nice" pack asks gently and the "Rude" pack heckles you.
|
||
- **[`AppDelegate.swift`](Shared/AppDelegate.swift)** — Handles the action buttons on the notification to route the vote through `MoodLogger`.
|
||
|
||
### 19. Export / Import
|
||
|
||
- **[`ExportService.swift`](Shared/Services/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`](Shared/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`](Shared/Views/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`](Shared/Models/GuidedReflection.swift)
|
||
> — Distortion detector: [`Shared/Services/CognitiveDistortionDetector.swift`](Shared/Services/CognitiveDistortionDetector.swift)
|
||
> — AI feedback: [`Shared/Services/FoundationModelsReflectionService.swift`](Shared/Services/FoundationModelsReflectionService.swift)
|
||
> — Feedback model: [`Shared/Models/AIReflectionFeedback.swift`](Shared/Models/AIReflectionFeedback.swift)
|
||
> — UI: [`Shared/Views/GuidedReflectionView.swift`](Shared/Views/GuidedReflectionView.swift)
|
||
> — Feedback UI: [`Shared/Views/ReflectionFeedbackView.swift`](Shared/Views/ReflectionFeedbackView.swift)
|
||
> — Info sheet: [`Shared/Views/GuidedReflectionInfoView.swift`](Shared/Views/GuidedReflectionInfoView.swift)
|
||
> — Plan doc: [`docs/guided-reflection-cbt-plan.md`](docs/guided-reflection-cbt-plan.md)
|
||
> — Flowchart: [`docs/guided-reflection-flowchart.html`](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:)`](Shared/Models/GuidedReflection.swift) and rendered as a stepped sheet from [`GuidedReflectionView.swift`](Shared/Views/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`](Shared/Models/GuidedReflection.swift) — 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`](Shared/Models/GuidedReflection.swift) 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`](Shared/Services/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`](Reflect/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`](Shared/Models/GuidedReflection.swift) carries optional `preIntensity` and `postIntensity` (0–10). 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`](Shared/Services/FoundationModelsReflectionService.swift) generates an [`AIReflectionFeedback`](Shared/Models/AIReflectionFeedback.swift) with three slots — **affirmation**, **observation**, **takeaway** — rendered by [`ReflectionFeedbackView.swift`](Shared/Views/ReflectionFeedbackView.swift). Tone is driven by the user's selected [`PersonalityPackable`](Shared/Models/PersonalityPackable.swift) (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`](Shared/Models/GuidedReflection.swift) detects the legacy shape so users never lose completed work across updates.
|
||
|
||
### Full flow diagram
|
||
|
||
Open [`docs/guided-reflection-flowchart.html`](docs/guided-reflection-flowchart.html) in a browser for the visual flow. The plan doc in [`docs/guided-reflection-cbt-plan.md`](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`](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`](CLAUDE.md).
|
||
|
||
---
|
||
|
||
## Data model
|
||
|
||
Primary entity: [`MoodEntryModel`](Shared/Models/MoodEntryModel.swift) — a SwiftData `@Model` with:
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| `moodValue` | Int | 0–4 (Horrible → Great), 5 = missing, 6 = placeholder |
|
||
| `forDate` | Date | The *logical* day being rated |
|
||
| `timestamp` | Date | When the entry was written |
|
||
| `weekDay` | Int | 1–7, 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`](Shared/Models/WeatherData.swift) |
|
||
| `reflectionJSON` | String? | [`GuidedReflection`](Shared/Models/GuidedReflection.swift) |
|
||
| `tagsJSON` | String? | [`AIEntryTags`](Shared/Models/AIEntryTags.swift) |
|
||
|
||
All fields have defaults — required by CloudKit. Sync is automatic via SwiftData's built-in CloudKit integration.
|
||
|
||
---
|
||
|
||
## Directory map
|
||
|
||
| Path | Contents |
|
||
|---|---|
|
||
| [`Shared/`](Shared) | All cross-platform code |
|
||
| [`Shared/Models/`](Shared/Models) | Domain types, SwiftData models, AI schemas |
|
||
| [`Shared/Views/`](Shared/Views) | SwiftUI views, grouped by feature |
|
||
| [`Shared/Services/`](Shared/Services) | Singletons for AI, HealthKit, Weather, Export, etc. |
|
||
| [`Shared/Persisence/`](Shared/Persisence) | SwiftData layer *(note: directory name has a typo — intentional, for historic reasons)* |
|
||
| [`Shared/Onboarding/`](Shared/Onboarding) | First-launch flow |
|
||
| [`Shared/Utilities/`](Shared/Utilities) | Small helpers |
|
||
| [`ReflectWidget/`](ReflectWidget) | Widget + Control Center + Live Activity extension |
|
||
| [`Reflect Watch App/`](Reflect%20Watch%20App) | watchOS app + complications |
|
||
| [`Reflect/`](Reflect) | iOS app target assets, Info.plist, Localizable.xcstrings |
|
||
| [`Tests iOS/`](Tests%20iOS) | XCUITest suites — see [`docs/XCUITest-Authoring.md`](docs/XCUITest-Authoring.md) |
|
||
| [`ReflectTests/`](ReflectTests) | XCTest unit tests |
|
||
| [`Tests macOS/`](Tests%20macOS) | macOS target tests |
|
||
| [`landing_page/`](landing_page) | Marketing site |
|
||
| [`docs/`](docs) | Design docs, ASO, QA plan, CBT plan, competitors |
|
||
| [`scripts/`](scripts) | Build/dev utilities |
|
||
| [`ads/`](ads) | Marketing creative |
|
||
|
||
---
|
||
|
||
## Build, run, test
|
||
|
||
```bash
|
||
# 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`](docs/XCUITest-Authoring.md), [`Tests iOS/Helpers/BaseUITestCase.swift`](Tests%20iOS/Helpers/BaseUITestCase.swift), and [`Shared/AccessibilityIdentifiers.swift`](Shared/AccessibilityIdentifiers.swift).
|
||
|
||
---
|
||
|
||
## Localization
|
||
|
||
Format: **String Catalog** at [`Reflect/Localizable.xcstrings`](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](#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`](Shared/Analytics.swift)) captures UI events, never mood content, and the user can opt out in settings.
|
||
- **Optional biometric lock** ([`BiometricAuthManager`](Shared/Services/BiometricAuthManager.swift)) 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`](CLAUDE.md). For ASO strategy and screenshot plans see [`docs/`](docs).*
|