learning_ai_clock/ios/ChronoMind/Shared/Sleep/SleepManager.swift
saravanakumardb1 424e804396 fix(ios): resolve Swift compile errors for iOS 26 / Xcode 26
- Fix CloudKitSyncManager deinit concurrency violation
- Replace deprecated List(selection:) with VStack+ForEach sidebar
- Replace removed Animation.none with Animation.linear(duration: 0)
- Fix CountdownRing initializer parameter mismatch
- Unwrap optional timer.duration in ShareableTimerManager and DataExportManager
- Add missing .event case to exhaustive switch
- Change CascadeWarning.scheduledTime from let to var
- Fix CalendarSyncManager CMTimer init (add elapsedBeforePause, remove non-existent params)
- Add missing UserNotifications import in SleepManager
- Remove parameterized App Intents phrases (iOS 26 restriction)
- Temporarily remove watchOS target dependency for iOS build
2026-03-19 14:22:50 -07:00

277 lines
9.6 KiB
Swift

// Sleep Manager
// Bedtime routines, smart alarm, HealthKit sleep data integration
// Ties into AI reschedule for sleep-aware morning adjustments
import Foundation
import HealthKit
import Combine
import UserNotifications
@MainActor
final class SleepManager: ObservableObject {
static let shared = SleepManager()
@Published var bedtimeRoutine: BedtimeRoutine?
@Published var smartAlarmConfig: SmartAlarmConfig?
@Published var lastSleepData: SleepSummary?
@Published var healthKitAuthorized = false
private let healthStore = HKHealthStore()
private let routineKey = "chronomind-bedtime-routine"
private let alarmKey = "chronomind-smart-alarm"
private let sleepKey = "chronomind-last-sleep"
private init() {
loadData()
}
// MARK: - HealthKit Authorization
func requestHealthKitAccess() async -> Bool {
guard HKHealthStore.isHealthDataAvailable() else { return false }
let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
let readTypes: Set<HKObjectType> = [sleepType]
do {
try await healthStore.requestAuthorization(toShare: [], read: readTypes)
healthKitAuthorized = true
return true
} catch {
return false
}
}
// MARK: - Sleep Data (Read-Only from HealthKit)
func fetchLastNightSleep() async -> SleepSummary? {
guard healthKitAuthorized else { return nil }
let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
let calendar = Calendar.current
let now = Date()
let yesterday = calendar.date(byAdding: .day, value: -1, to: calendar.startOfDay(for: now))!
let predicate = HKQuery.predicateForSamples(withStart: yesterday, end: now, options: .strictStartDate)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
return await withCheckedContinuation { continuation in
let query = HKSampleQuery(
sampleType: sleepType,
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: [sortDescriptor]
) { _, samples, error in
guard let samples = samples as? [HKCategorySample], !samples.isEmpty else {
continuation.resume(returning: nil)
return
}
let summary = self.processSleepSamples(samples)
Task { @MainActor in
self.lastSleepData = summary
self.saveSleepData(summary)
}
continuation.resume(returning: summary)
}
healthStore.execute(query)
}
}
private func processSleepSamples(_ samples: [HKCategorySample]) -> SleepSummary {
var totalAsleep: TimeInterval = 0
var totalInBed: TimeInterval = 0
var sleepStart: Date?
var sleepEnd: Date?
for sample in samples {
let duration = sample.endDate.timeIntervalSince(sample.startDate)
switch sample.value {
case HKCategoryValueSleepAnalysis.asleepCore.rawValue,
HKCategoryValueSleepAnalysis.asleepDeep.rawValue,
HKCategoryValueSleepAnalysis.asleepREM.rawValue:
totalAsleep += duration
case HKCategoryValueSleepAnalysis.inBed.rawValue:
totalInBed += duration
default:
break
}
if sleepStart == nil || sample.startDate < sleepStart! {
sleepStart = sample.startDate
}
if sleepEnd == nil || sample.endDate > sleepEnd! {
sleepEnd = sample.endDate
}
}
return SleepSummary(
totalSleepHours: totalAsleep / 3600,
totalInBedHours: max(totalInBed, totalAsleep) / 3600,
sleepEfficiency: totalInBed > 0 ? (totalAsleep / totalInBed) : 0,
bedtime: sleepStart,
wakeTime: sleepEnd,
date: Date()
)
}
// MARK: - Smart Alarm
/// Generate a reschedule suggestion based on sleep data
func sleepAwareRescheduleSuggestion(sleepSummary: SleepSummary) -> String? {
guard let wakeTime = sleepSummary.wakeTime else { return nil }
let sleepHours = sleepSummary.totalSleepHours
if sleepHours < 6 {
let deficit = Int((7 - sleepHours) * 15) // 15 min per hour of deficit
return "You slept \(String(format: "%.0f", sleepHours))h \(Int((sleepHours.truncatingRemainder(dividingBy: 1)) * 60))m. Shift your morning by \(deficit) minutes?"
} else if sleepHours < 7 {
return "You slept \(String(format: "%.0f", sleepHours))h \(Int((sleepHours.truncatingRemainder(dividingBy: 1)) * 60))m. Consider a gentler morning — shift timers by 10 minutes?"
}
return nil
}
// MARK: - Bedtime Routine
func saveBedtimeRoutine(_ routine: BedtimeRoutine) {
bedtimeRoutine = routine
if let data = try? JSONEncoder().encode(routine) {
UserDefaults.standard.set(data, forKey: routineKey)
}
}
func saveSmartAlarmConfig(_ config: SmartAlarmConfig) {
smartAlarmConfig = config
if let data = try? JSONEncoder().encode(config) {
UserDefaults.standard.set(data, forKey: alarmKey)
}
}
// MARK: - Wind-Down Notifications
func scheduleWindDownNotification(bedtime: Date) {
let content = UNMutableNotificationContent()
content.title = "Time to wind down"
content.body = "Bedtime in 30 minutes — start your wind-down routine"
content.sound = .default
content.categoryIdentifier = "WIND_DOWN"
let triggerDate = bedtime.addingTimeInterval(-1800) // 30 min before
guard triggerDate > Date() else { return }
let components = Calendar.current.dateComponents([.hour, .minute], from: triggerDate)
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
let request = UNNotificationRequest(identifier: "wind-down", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
}
// MARK: - Persistence
private func loadData() {
if let data = UserDefaults.standard.data(forKey: routineKey),
let decoded = try? JSONDecoder().decode(BedtimeRoutine.self, from: data) {
bedtimeRoutine = decoded
}
if let data = UserDefaults.standard.data(forKey: alarmKey),
let decoded = try? JSONDecoder().decode(SmartAlarmConfig.self, from: data) {
smartAlarmConfig = decoded
}
if let data = UserDefaults.standard.data(forKey: sleepKey),
let decoded = try? JSONDecoder().decode(SleepSummary.self, from: data) {
lastSleepData = decoded
}
}
private func saveSleepData(_ summary: SleepSummary) {
if let data = try? JSONEncoder().encode(summary) {
UserDefaults.standard.set(data, forKey: sleepKey)
}
}
}
// MARK: - Models
struct SleepSummary: Codable {
let totalSleepHours: Double
let totalInBedHours: Double
let sleepEfficiency: Double // 0-1
let bedtime: Date?
let wakeTime: Date?
let date: Date
var formattedSleep: String {
let hours = Int(totalSleepHours)
let minutes = Int((totalSleepHours - Double(hours)) * 60)
return "\(hours)h \(minutes)m"
}
var qualityLabel: String {
switch totalSleepHours {
case 8...: return "Excellent"
case 7..<8: return "Good"
case 6..<7: return "Fair"
default: return "Poor"
}
}
var qualityColor: String {
switch totalSleepHours {
case 8...: return "success"
case 7..<8: return "accent"
case 6..<7: return "important"
default: return "error"
}
}
}
struct BedtimeRoutine: Codable {
var targetBedtime: Date // Time component only
var windDownMinutes: Int // Default 30
var steps: [RoutineStep]
var enabled: Bool
struct RoutineStep: Codable, Identifiable {
let id: String
var label: String
var durationMinutes: Int
var icon: String
init(label: String, durationMinutes: Int, icon: String) {
self.id = UUID().uuidString
self.label = label
self.durationMinutes = durationMinutes
self.icon = icon
}
}
static let `default` = BedtimeRoutine(
targetBedtime: Calendar.current.date(from: DateComponents(hour: 22, minute: 30))!,
windDownMinutes: 30,
steps: [
RoutineStep(label: "Put devices away", durationMinutes: 5, icon: "iphone.slash"),
RoutineStep(label: "Read or journal", durationMinutes: 15, icon: "book.fill"),
RoutineStep(label: "Breathing exercise", durationMinutes: 5, icon: "wind"),
RoutineStep(label: "Lights out", durationMinutes: 5, icon: "moon.fill"),
],
enabled: false
)
}
struct SmartAlarmConfig: Codable {
var windowStart: Date // e.g. 6:30 AM
var windowEnd: Date // e.g. 7:00 AM
var enabled: Bool
var useSleepData: Bool // Use HealthKit to optimize wake time
static let `default` = SmartAlarmConfig(
windowStart: Calendar.current.date(from: DateComponents(hour: 6, minute: 30))!,
windowEnd: Calendar.current.date(from: DateComponents(hour: 7, minute: 0))!,
enabled: false,
useSleepData: true
)
}