Context
Users currently must manually type bedtime/wake time in the schedule UI. Apple Health's Sleep Schedule (set in Health app → Sleep) already stores this — we should read it. Separately, the app classifies sleep stages (wake/light/deep/REM) via SleepAnalyzer but the results stay local. Writing them to HealthKit lets the data appear in Apple's Health app Sleep section.
Important limitation: Apple Health has no native sleep score type. There is no HKQuantityTypeIdentifier for a numeric sleep quality score. We can only write sleep stage samples (HKCategorySample with .sleepAnalysis). Apple's Health app will display the stages and compute its own summary from what we provide. Our 0-100 quality score stays app-local only.
Feature 1: Read Sleep Schedule from Apple Health
Read the user's bedtime/wake time from Apple Health instead of forcing manual entry.
- Apple's Sleep Schedule creates future-dated
.inBed samples in HealthKit
- Existing partial implementation in
SmartCurveView.swift (lines 470-575) already reads these — extract into reusable service
- Manual button: Always-visible "Sync from Health" button in
SleepTimeCardView
- Auto-sync toggle: When enabled in settings, auto-pulls schedule every time Schedule tab loads
- Returns
HH:mm strings matching PowerSchedule.on / AlarmSchedule.time format
- Falls back to 7-day historical sleep data if no future schedule exists
Feature 2: Export Sleep Stages to Apple Health
After SleepAnalyzer classifies a night's sleep, automatically write the results to HealthKit.
Stage mapping:
| App Stage |
HealthKit Value |
.wake |
HKCategoryValueSleepAnalysis.awake |
.light |
.asleepCore |
.deep |
.asleepDeep |
.rem |
.asleepREM |
| overall session |
.inBed (enteredBedDate → leftBedDate) |
Write behavior:
- Automatic after analysis completes (when enabled in settings)
- Merge consecutive same-stage epochs into contiguous
HKCategorySample blocks (not 60s micro-samples)
- Deduplication: check last-written record ID (UserDefaults) + query HealthKit for overlapping samples before writing
- No-op in demo mode
Implementation Plan
Step 1: Add Log.health category
File: Services/Log.swift
static let health = Logger(subsystem: "com.sleepypod.ios", category: "health")
Step 2: Create Services/HealthKitService.swift
New @MainActor @Observable class (matches existing service patterns).
Authorization: Request both read+write for HKCategoryType(.sleepAnalysis). Triggered lazily on first use.
Read — fetchSleepSchedule() async -> (bedtime: String, wake: String)?
- Queries future-dated
.inBed samples
- Falls back to 7-day history
- Returns
HH:mm strings
Write — writeSleepData(epochs:sleepRecord:) async throws
- Takes
[SleepAnalyzer.SleepEpoch] + SleepRecord
- Writes one
.inBed sample spanning full session
- Merges consecutive same-stage epochs into contiguous
HKCategorySample blocks
- Batch-saves via
HKHealthStore.save([HKObject])
Deduplication: UserDefaults fast-path (last record ID) + HealthKit overlap query.
Preferences (UserDefaults-backed):
healthKitSyncScheduleEnabled: Bool
healthKitWriteSleepEnabled: Bool
Step 3: Wire into app environment
File: SleepypodApp.swift
- Add
@State private var healthKitService = HealthKitService()
- Add
.environment(healthKitService) to chain
Step 4: Refactor SmartCurveView
File: Views/Schedule/SmartCurveView.swift
- Remove
importFromHealth(), queryScheduleSamples(), applyTimes() (~100 lines of dead HealthKit code)
- Remove
healthSynced/healthError state vars
Step 5: Schedule sync UI
File: Views/Schedule/SleepTimeCardView.swift
- Add "Sync from Health" button (
heart.text.clipboard icon) below bedtime/wake row
- On tap:
fetchSleepSchedule() → update PowerSchedule.on + AlarmSchedule.time
- Auto-sync via
.task modifier when toggle is enabled
Step 6: Auto-write sleep stages
File: Views/Data/HealthScreen.swift (after sleepAnalyzer.analyze())
if healthKitService.healthKitWriteSleepEnabled,
!sleepAnalyzer.stages.isEmpty,
let record = metricsManager.selectedDayRecord {
try? await healthKitService.writeSleepData(
epochs: sleepAnalyzer.stages, sleepRecord: record
)
}
Step 7: Settings toggles
File: Views/Settings/DeviceSettingsCardView.swift
Add "Apple Health" section:
- "Sync Sleep Schedule" toggle →
healthKitSyncScheduleEnabled
- "Export Sleep Stages" toggle →
healthKitWriteSleepEnabled
- First enable triggers
requestAuthorization(); revert if denied
Step 8: Tests
File: SleepypodTests/HealthKitServiceTests.swift
- Epoch merging (consecutive same-stage → single block)
- Stage mapping correctness
HH:mm formatting from Date components
Files Summary
| Action |
File |
| Create |
Services/HealthKitService.swift |
| Create |
SleepypodTests/HealthKitServiceTests.swift |
| Modify |
Services/Log.swift — add health category |
| Modify |
SleepypodApp.swift — add to environment |
| Modify |
Views/Schedule/SmartCurveView.swift — remove ~100 lines dead HealthKit code |
| Modify |
Views/Schedule/SleepTimeCardView.swift — add sync button + auto-sync |
| Modify |
Views/Data/HealthScreen.swift — add write after analysis |
| Modify |
Views/Settings/DeviceSettingsCardView.swift — add toggles |
Already configured (no changes needed): Sleepypod.entitlements (HealthKit enabled), Info.plist (read/write usage descriptions)
Edge Cases & Notes
requestAuthorization returns success: true even when user denies specific types (Apple privacy design). Must handle errors on actual read/write, not rely on auth callback.
- If user has never set a Sleep Schedule in Health, no future-dated samples exist. Fallback to historical data handles this.
- Write timing: happens when user views Health tab (retrospective analysis needs full night's data). Background processing is a future enhancement.
- Deduplication prevents duplicates when viewing the same night multiple times.
🤖 Generated with Claude Code
Context
Users currently must manually type bedtime/wake time in the schedule UI. Apple Health's Sleep Schedule (set in Health app → Sleep) already stores this — we should read it. Separately, the app classifies sleep stages (wake/light/deep/REM) via
SleepAnalyzerbut the results stay local. Writing them to HealthKit lets the data appear in Apple's Health app Sleep section.Important limitation: Apple Health has no native sleep score type. There is no
HKQuantityTypeIdentifierfor a numeric sleep quality score. We can only write sleep stage samples (HKCategorySamplewith.sleepAnalysis). Apple's Health app will display the stages and compute its own summary from what we provide. Our 0-100 quality score stays app-local only.Feature 1: Read Sleep Schedule from Apple Health
Read the user's bedtime/wake time from Apple Health instead of forcing manual entry.
.inBedsamples in HealthKitSmartCurveView.swift(lines 470-575) already reads these — extract into reusable serviceSleepTimeCardViewHH:mmstrings matchingPowerSchedule.on/AlarmSchedule.timeformatFeature 2: Export Sleep Stages to Apple Health
After
SleepAnalyzerclassifies a night's sleep, automatically write the results to HealthKit.Stage mapping:
.wakeHKCategoryValueSleepAnalysis.awake.light.asleepCore.deep.asleepDeep.rem.asleepREM.inBed(enteredBedDate → leftBedDate)Write behavior:
HKCategorySampleblocks (not 60s micro-samples)Implementation Plan
Step 1: Add
Log.healthcategoryFile:
Services/Log.swiftStep 2: Create
Services/HealthKitService.swiftNew
@MainActor @Observableclass (matches existing service patterns).Authorization: Request both read+write for
HKCategoryType(.sleepAnalysis). Triggered lazily on first use.Read —
fetchSleepSchedule() async -> (bedtime: String, wake: String)?.inBedsamplesHH:mmstringsWrite —
writeSleepData(epochs:sleepRecord:) async throws[SleepAnalyzer.SleepEpoch]+SleepRecord.inBedsample spanning full sessionHKCategorySampleblocksHKHealthStore.save([HKObject])Deduplication: UserDefaults fast-path (last record ID) + HealthKit overlap query.
Preferences (UserDefaults-backed):
healthKitSyncScheduleEnabled: BoolhealthKitWriteSleepEnabled: BoolStep 3: Wire into app environment
File:
SleepypodApp.swift@State private var healthKitService = HealthKitService().environment(healthKitService)to chainStep 4: Refactor SmartCurveView
File:
Views/Schedule/SmartCurveView.swiftimportFromHealth(),queryScheduleSamples(),applyTimes()(~100 lines of dead HealthKit code)healthSynced/healthErrorstate varsStep 5: Schedule sync UI
File:
Views/Schedule/SleepTimeCardView.swiftheart.text.clipboardicon) below bedtime/wake rowfetchSleepSchedule()→ updatePowerSchedule.on+AlarmSchedule.time.taskmodifier when toggle is enabledStep 6: Auto-write sleep stages
File:
Views/Data/HealthScreen.swift(aftersleepAnalyzer.analyze())Step 7: Settings toggles
File:
Views/Settings/DeviceSettingsCardView.swiftAdd "Apple Health" section:
healthKitSyncScheduleEnabledhealthKitWriteSleepEnabledrequestAuthorization(); revert if deniedStep 8: Tests
File:
SleepypodTests/HealthKitServiceTests.swiftHH:mmformatting from Date componentsFiles Summary
Services/HealthKitService.swiftSleepypodTests/HealthKitServiceTests.swiftServices/Log.swift— add health categorySleepypodApp.swift— add to environmentViews/Schedule/SmartCurveView.swift— remove ~100 lines dead HealthKit codeViews/Schedule/SleepTimeCardView.swift— add sync button + auto-syncViews/Data/HealthScreen.swift— add write after analysisViews/Settings/DeviceSettingsCardView.swift— add togglesAlready configured (no changes needed):
Sleepypod.entitlements(HealthKit enabled),Info.plist(read/write usage descriptions)Edge Cases & Notes
requestAuthorizationreturnssuccess: trueeven when user denies specific types (Apple privacy design). Must handle errors on actual read/write, not rely on auth callback.🤖 Generated with Claude Code