Pass raw health metrics to AI instead of hardcoded correlations
- Replace HealthService.analyzeCorrelations() with computeHealthAverages() - Remove hardcoded threshold-based correlation analysis (8k steps, 7hrs sleep, etc.) - Pass raw averages (steps, exercise, sleep, HRV, HR, mindfulness, calories) to AI - Let Apple Intelligence find nuanced multi-variable patterns naturally - Update MoodDataSummarizer to format raw health data for AI prompts - Simplifies code by ~200 lines while improving insight quality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,14 +30,35 @@ class HealthService: ObservableObject {
|
||||
|
||||
// MARK: - Data Types
|
||||
|
||||
// Core activity metrics
|
||||
private let stepCountType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
|
||||
private let exerciseTimeType = HKQuantityType.quantityType(forIdentifier: .appleExerciseTime)!
|
||||
private let activeEnergyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
|
||||
private let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!
|
||||
|
||||
// Heart & stress indicators
|
||||
private let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)!
|
||||
private let restingHeartRateType = HKQuantityType.quantityType(forIdentifier: .restingHeartRate)!
|
||||
private let hrvType = HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!
|
||||
|
||||
// Sleep & recovery
|
||||
private let sleepAnalysisType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis)!
|
||||
|
||||
// Mindfulness
|
||||
private let mindfulSessionType = HKCategoryType.categoryType(forIdentifier: .mindfulSession)!
|
||||
|
||||
// State of Mind
|
||||
private let stateOfMindType = HKSampleType.stateOfMindType()
|
||||
|
||||
// Workouts
|
||||
private let workoutType = HKWorkoutType.workoutType()
|
||||
|
||||
private var readTypes: Set<HKObjectType> {
|
||||
[stepCountType, exerciseTimeType, heartRateType, sleepAnalysisType, stateOfMindType]
|
||||
[
|
||||
stepCountType, exerciseTimeType, activeEnergyType, distanceType,
|
||||
heartRateType, restingHeartRateType, hrvType,
|
||||
sleepAnalysisType, mindfulSessionType, stateOfMindType, workoutType
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
@@ -71,13 +92,25 @@ class HealthService: ObservableObject {
|
||||
|
||||
struct DailyHealthData {
|
||||
let date: Date
|
||||
|
||||
// Activity metrics
|
||||
let steps: Int?
|
||||
let exerciseMinutes: Int?
|
||||
let activeCalories: Int?
|
||||
let distanceKm: Double?
|
||||
|
||||
// Heart metrics
|
||||
let averageHeartRate: Double?
|
||||
let restingHeartRate: Double?
|
||||
let hrv: Double? // Heart Rate Variability in ms
|
||||
|
||||
// Recovery metrics
|
||||
let sleepHours: Double?
|
||||
let mindfulMinutes: Int?
|
||||
|
||||
var hasData: Bool {
|
||||
steps != nil || exerciseMinutes != nil || averageHeartRate != nil || sleepHours != nil
|
||||
steps != nil || exerciseMinutes != nil || averageHeartRate != nil ||
|
||||
sleepHours != nil || hrv != nil || mindfulMinutes != nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,17 +119,28 @@ class HealthService: ObservableObject {
|
||||
let startOfDay = calendar.startOfDay(for: date)
|
||||
let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)!
|
||||
|
||||
// Fetch all metrics concurrently
|
||||
async let steps = fetchSteps(start: startOfDay, end: endOfDay)
|
||||
async let exercise = fetchExerciseMinutes(start: startOfDay, end: endOfDay)
|
||||
async let activeCalories = fetchActiveCalories(start: startOfDay, end: endOfDay)
|
||||
async let distance = fetchDistance(start: startOfDay, end: endOfDay)
|
||||
async let heartRate = fetchAverageHeartRate(start: startOfDay, end: endOfDay)
|
||||
async let restingHR = fetchRestingHeartRate(start: startOfDay, end: endOfDay)
|
||||
async let hrv = fetchHRV(start: startOfDay, end: endOfDay)
|
||||
async let sleep = fetchSleepHours(for: date)
|
||||
async let mindful = fetchMindfulMinutes(start: startOfDay, end: endOfDay)
|
||||
|
||||
return await DailyHealthData(
|
||||
date: date,
|
||||
steps: steps,
|
||||
exerciseMinutes: exercise,
|
||||
activeCalories: activeCalories,
|
||||
distanceKm: distance,
|
||||
averageHeartRate: heartRate,
|
||||
sleepHours: sleep
|
||||
restingHeartRate: restingHR,
|
||||
hrv: hrv,
|
||||
sleepHours: sleep,
|
||||
mindfulMinutes: mindful
|
||||
)
|
||||
}
|
||||
|
||||
@@ -231,6 +275,153 @@ class HealthService: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Active Calories
|
||||
|
||||
private func fetchActiveCalories(start: Date, end: Date) async -> Int? {
|
||||
guard isAuthorized else { return nil }
|
||||
|
||||
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: .strictStartDate)
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
let query = HKStatisticsQuery(
|
||||
quantityType: activeEnergyType,
|
||||
quantitySamplePredicate: predicate,
|
||||
options: .cumulativeSum
|
||||
) { _, result, error in
|
||||
guard error == nil,
|
||||
let sum = result?.sumQuantity() else {
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
|
||||
let calories = Int(sum.doubleValue(for: .kilocalorie()))
|
||||
continuation.resume(returning: calories)
|
||||
}
|
||||
|
||||
healthStore.execute(query)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Distance
|
||||
|
||||
private func fetchDistance(start: Date, end: Date) async -> Double? {
|
||||
guard isAuthorized else { return nil }
|
||||
|
||||
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: .strictStartDate)
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
let query = HKStatisticsQuery(
|
||||
quantityType: distanceType,
|
||||
quantitySamplePredicate: predicate,
|
||||
options: .cumulativeSum
|
||||
) { _, result, error in
|
||||
guard error == nil,
|
||||
let sum = result?.sumQuantity() else {
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
|
||||
let km = sum.doubleValue(for: .meterUnit(with: .kilo))
|
||||
continuation.resume(returning: km)
|
||||
}
|
||||
|
||||
healthStore.execute(query)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Resting Heart Rate
|
||||
|
||||
private func fetchRestingHeartRate(start: Date, end: Date) async -> Double? {
|
||||
guard isAuthorized else { return nil }
|
||||
|
||||
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: .strictStartDate)
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
let query = HKStatisticsQuery(
|
||||
quantityType: restingHeartRateType,
|
||||
quantitySamplePredicate: predicate,
|
||||
options: .discreteAverage
|
||||
) { _, result, error in
|
||||
guard error == nil,
|
||||
let avg = result?.averageQuantity() else {
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
|
||||
let bpm = avg.doubleValue(for: HKUnit.count().unitDivided(by: .minute()))
|
||||
continuation.resume(returning: bpm)
|
||||
}
|
||||
|
||||
healthStore.execute(query)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Heart Rate Variability (HRV)
|
||||
|
||||
private func fetchHRV(start: Date, end: Date) async -> Double? {
|
||||
guard isAuthorized else { return nil }
|
||||
|
||||
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: .strictStartDate)
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
let query = HKStatisticsQuery(
|
||||
quantityType: hrvType,
|
||||
quantitySamplePredicate: predicate,
|
||||
options: .discreteAverage
|
||||
) { _, result, error in
|
||||
guard error == nil,
|
||||
let avg = result?.averageQuantity() else {
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
|
||||
// HRV is measured in milliseconds
|
||||
let ms = avg.doubleValue(for: .secondUnit(with: .milli))
|
||||
continuation.resume(returning: ms)
|
||||
}
|
||||
|
||||
healthStore.execute(query)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mindful Minutes
|
||||
|
||||
private func fetchMindfulMinutes(start: Date, end: Date) async -> Int? {
|
||||
guard isAuthorized else { return nil }
|
||||
|
||||
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: .strictStartDate)
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
let query = HKSampleQuery(
|
||||
sampleType: mindfulSessionType,
|
||||
predicate: predicate,
|
||||
limit: HKObjectQueryNoLimit,
|
||||
sortDescriptors: nil
|
||||
) { _, samples, error in
|
||||
guard error == nil,
|
||||
let mindfulSamples = samples as? [HKCategorySample] else {
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Sum up all mindful session durations
|
||||
var totalSeconds: TimeInterval = 0
|
||||
for sample in mindfulSamples {
|
||||
totalSeconds += sample.endDate.timeIntervalSince(sample.startDate)
|
||||
}
|
||||
|
||||
if totalSeconds > 0 {
|
||||
let minutes = Int(totalSeconds / 60)
|
||||
continuation.resume(returning: minutes)
|
||||
} else {
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
}
|
||||
|
||||
healthStore.execute(query)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Batch Fetch for Insights
|
||||
|
||||
func fetchHealthData(for entries: [MoodEntryModel]) async -> [Date: DailyHealthData] {
|
||||
@@ -259,121 +450,78 @@ class HealthService: ObservableObject {
|
||||
return results
|
||||
}
|
||||
|
||||
// MARK: - Correlation Analysis
|
||||
// MARK: - Health Averages for AI Analysis
|
||||
|
||||
struct HealthMoodCorrelation {
|
||||
let metric: String
|
||||
let correlation: String // "positive", "negative", or "none"
|
||||
let insight: String
|
||||
let averageWithHighMetric: Double
|
||||
let averageWithLowMetric: Double
|
||||
/// Aggregated health metrics for a period - passed to AI for correlation analysis
|
||||
struct HealthAverages {
|
||||
// Activity
|
||||
let avgSteps: Int?
|
||||
let avgExerciseMinutes: Int?
|
||||
let avgActiveCalories: Int?
|
||||
let avgDistanceKm: Double?
|
||||
|
||||
// Heart health
|
||||
let avgHeartRate: Double?
|
||||
let avgRestingHeartRate: Double?
|
||||
let avgHRV: Double? // milliseconds
|
||||
|
||||
// Recovery
|
||||
let avgSleepHours: Double?
|
||||
let avgMindfulMinutes: Int?
|
||||
|
||||
// Sample sizes (for AI context)
|
||||
let daysWithHealthData: Int
|
||||
let totalMoodEntries: Int
|
||||
|
||||
var hasData: Bool {
|
||||
daysWithHealthData > 0
|
||||
}
|
||||
}
|
||||
|
||||
func analyzeCorrelations(entries: [MoodEntryModel], healthData: [Date: DailyHealthData]) -> [HealthMoodCorrelation] {
|
||||
var correlations: [HealthMoodCorrelation] = []
|
||||
/// Compute aggregate health averages for a set of mood entries
|
||||
/// The AI will analyze these alongside mood data to find correlations
|
||||
func computeHealthAverages(entries: [MoodEntryModel], healthData: [Date: DailyHealthData]) -> HealthAverages {
|
||||
let calendar = Calendar.current
|
||||
|
||||
// Prepare data pairs
|
||||
var stepsAndMoods: [(steps: Int, mood: Int)] = []
|
||||
var exerciseAndMoods: [(minutes: Int, mood: Int)] = []
|
||||
var sleepAndMoods: [(hours: Double, mood: Int)] = []
|
||||
var heartRateAndMoods: [(bpm: Double, mood: Int)] = []
|
||||
var steps: [Int] = []
|
||||
var exercise: [Int] = []
|
||||
var calories: [Int] = []
|
||||
var distance: [Double] = []
|
||||
var heartRate: [Double] = []
|
||||
var restingHR: [Double] = []
|
||||
var hrv: [Double] = []
|
||||
var sleep: [Double] = []
|
||||
var mindful: [Int] = []
|
||||
|
||||
for entry in entries {
|
||||
let date = calendar.startOfDay(for: entry.forDate)
|
||||
guard let health = healthData[date] else { continue }
|
||||
|
||||
let moodValue = entry.moodValue + 1 // Use 1-5 scale
|
||||
|
||||
if let steps = health.steps {
|
||||
stepsAndMoods.append((steps, moodValue))
|
||||
}
|
||||
if let exercise = health.exerciseMinutes {
|
||||
exerciseAndMoods.append((exercise, moodValue))
|
||||
}
|
||||
if let sleep = health.sleepHours {
|
||||
sleepAndMoods.append((sleep, moodValue))
|
||||
}
|
||||
if let hr = health.averageHeartRate {
|
||||
heartRateAndMoods.append((hr, moodValue))
|
||||
}
|
||||
if let s = health.steps { steps.append(s) }
|
||||
if let e = health.exerciseMinutes { exercise.append(e) }
|
||||
if let c = health.activeCalories { calories.append(c) }
|
||||
if let d = health.distanceKm { distance.append(d) }
|
||||
if let h = health.averageHeartRate { heartRate.append(h) }
|
||||
if let r = health.restingHeartRate { restingHR.append(r) }
|
||||
if let v = health.hrv { hrv.append(v) }
|
||||
if let sl = health.sleepHours { sleep.append(sl) }
|
||||
if let m = health.mindfulMinutes { mindful.append(m) }
|
||||
}
|
||||
|
||||
// Analyze steps correlation
|
||||
if stepsAndMoods.count >= 5 {
|
||||
let threshold = 8000
|
||||
let highSteps = stepsAndMoods.filter { $0.steps >= threshold }
|
||||
let lowSteps = stepsAndMoods.filter { $0.steps < threshold }
|
||||
let daysWithData = Set(healthData.keys).count
|
||||
|
||||
if !highSteps.isEmpty && !lowSteps.isEmpty {
|
||||
let avgHigh = Double(highSteps.map { $0.mood }.reduce(0, +)) / Double(highSteps.count)
|
||||
let avgLow = Double(lowSteps.map { $0.mood }.reduce(0, +)) / Double(lowSteps.count)
|
||||
let diff = avgHigh - avgLow
|
||||
|
||||
if abs(diff) >= 0.3 {
|
||||
correlations.append(HealthMoodCorrelation(
|
||||
metric: "Steps",
|
||||
correlation: diff > 0 ? "positive" : "negative",
|
||||
insight: diff > 0
|
||||
? "Your mood averages \(String(format: "%.1f", diff)) points higher on days with 8k+ steps"
|
||||
: "Interestingly, your mood is slightly lower on high-step days",
|
||||
averageWithHighMetric: avgHigh,
|
||||
averageWithLowMetric: avgLow
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze sleep correlation
|
||||
if sleepAndMoods.count >= 5 {
|
||||
let threshold = 7.0
|
||||
let goodSleep = sleepAndMoods.filter { $0.hours >= threshold }
|
||||
let poorSleep = sleepAndMoods.filter { $0.hours < threshold }
|
||||
|
||||
if !goodSleep.isEmpty && !poorSleep.isEmpty {
|
||||
let avgGood = Double(goodSleep.map { $0.mood }.reduce(0, +)) / Double(goodSleep.count)
|
||||
let avgPoor = Double(poorSleep.map { $0.mood }.reduce(0, +)) / Double(poorSleep.count)
|
||||
let diff = avgGood - avgPoor
|
||||
|
||||
if abs(diff) >= 0.3 {
|
||||
correlations.append(HealthMoodCorrelation(
|
||||
metric: "Sleep",
|
||||
correlation: diff > 0 ? "positive" : "negative",
|
||||
insight: diff > 0
|
||||
? "7+ hours of sleep correlates with \(String(format: "%.1f", diff)) point higher mood"
|
||||
: "Sleep duration doesn't seem to strongly affect your mood",
|
||||
averageWithHighMetric: avgGood,
|
||||
averageWithLowMetric: avgPoor
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze exercise correlation
|
||||
if exerciseAndMoods.count >= 5 {
|
||||
let threshold = 30
|
||||
let active = exerciseAndMoods.filter { $0.minutes >= threshold }
|
||||
let inactive = exerciseAndMoods.filter { $0.minutes < threshold }
|
||||
|
||||
if !active.isEmpty && !inactive.isEmpty {
|
||||
let avgActive = Double(active.map { $0.mood }.reduce(0, +)) / Double(active.count)
|
||||
let avgInactive = Double(inactive.map { $0.mood }.reduce(0, +)) / Double(inactive.count)
|
||||
let diff = avgActive - avgInactive
|
||||
|
||||
if abs(diff) >= 0.3 {
|
||||
correlations.append(HealthMoodCorrelation(
|
||||
metric: "Exercise",
|
||||
correlation: diff > 0 ? "positive" : "negative",
|
||||
insight: diff > 0
|
||||
? "30+ minutes of exercise correlates with \(String(format: "%.1f", diff)) point mood boost"
|
||||
: "Exercise doesn't show a strong mood correlation for you",
|
||||
averageWithHighMetric: avgActive,
|
||||
averageWithLowMetric: avgInactive
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return correlations
|
||||
return HealthAverages(
|
||||
avgSteps: steps.isEmpty ? nil : steps.reduce(0, +) / steps.count,
|
||||
avgExerciseMinutes: exercise.isEmpty ? nil : exercise.reduce(0, +) / exercise.count,
|
||||
avgActiveCalories: calories.isEmpty ? nil : calories.reduce(0, +) / calories.count,
|
||||
avgDistanceKm: distance.isEmpty ? nil : distance.reduce(0, +) / Double(distance.count),
|
||||
avgHeartRate: heartRate.isEmpty ? nil : heartRate.reduce(0, +) / Double(heartRate.count),
|
||||
avgRestingHeartRate: restingHR.isEmpty ? nil : restingHR.reduce(0, +) / Double(restingHR.count),
|
||||
avgHRV: hrv.isEmpty ? nil : hrv.reduce(0, +) / Double(hrv.count),
|
||||
avgSleepHours: sleep.isEmpty ? nil : sleep.reduce(0, +) / Double(sleep.count),
|
||||
avgMindfulMinutes: mindful.isEmpty ? nil : mindful.reduce(0, +) / mindful.count,
|
||||
daysWithHealthData: daysWithData,
|
||||
totalMoodEntries: entries.count
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user