feat: iOS widgets + fix sleep showing yesterday's data

Widgets:
- HabitsProgressWidget (small/medium): progress ring, completed/total habits, tasks count
- HealthSummaryWidget (small/medium): readiness score, steps, sleep, heart rate
- Shared Keychain access group for app ↔ widget token sharing
- Widget data refreshes every 30 minutes

Sleep fix:
- Changed sleep window from "24 hours back" to "6 PM yesterday → now"
- Captures overnight sleep correctly without showing previous day's data
- Applied to both fetchSleepData (sync) and fetchSleepSegments (detail view)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 14:22:37 +03:00
parent d7d3eec2e5
commit f2580eb69f
9 changed files with 553 additions and 7 deletions

View File

@@ -154,10 +154,16 @@ class HealthKitService: ObservableObject {
private func fetchSleepData(dateFormatter: DateFormatter) async -> [[String: Any]] {
guard let sleepType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis) else { return [] }
// Берём последние 24 часа, чтобы захватить ночной сон
// Ночной сон: с 18:00 вчера до сейчас (захватывает засыпание вечером + пробуждение утром)
let now = Date()
guard let yesterday = Calendar.current.date(byAdding: .hour, value: -24, to: now) else { return [] }
let sleepPredicate = HKQuery.predicateForSamples(withStart: yesterday, end: now)
let cal = Calendar.current
var startComponents = cal.dateComponents([.year, .month, .day], from: now)
startComponents.hour = 18
startComponents.minute = 0
guard let todayEvening = cal.date(from: startComponents),
let yesterdayEvening = cal.date(byAdding: .day, value: -1, to: todayEvening) else { return [] }
let sleepStart = now.timeIntervalSince(todayEvening) > 0 ? todayEvening : yesterdayEvening
let sleepPredicate = HKQuery.predicateForSamples(withStart: sleepStart, end: now)
return await withCheckedContinuation { cont in
let sort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
@@ -328,8 +334,13 @@ class HealthKitService: ObservableObject {
func fetchSleepSegments() async -> [SleepSegment] {
guard let sleepType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis) else { return [] }
let now = Date()
guard let yesterday = Calendar.current.date(byAdding: .hour, value: -24, to: now) else { return [] }
let predicate = HKQuery.predicateForSamples(withStart: yesterday, end: now)
let cal = Calendar.current
var startComp = cal.dateComponents([.year, .month, .day], from: now)
startComp.hour = 18
guard let todayEvening = cal.date(from: startComp),
let yesterdayEvening = cal.date(byAdding: .day, value: -1, to: todayEvening) else { return [] }
let sleepStart = now.timeIntervalSince(todayEvening) > 0 ? todayEvening : yesterdayEvening
let predicate = HKQuery.predicateForSamples(withStart: sleepStart, end: now)
return await withCheckedContinuation { cont in
let sort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)