Add HealthKit backfill to sync all existing moods

When user enables HealthKit integration, automatically syncs all
previously recorded mood entries to Apple Health. Features:
- Progress tracking with visual indicator in Settings
- Batched saves with throttling to avoid overwhelming HealthKit
- Success/failure logging with EventLogger

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-20 00:55:25 -06:00
parent 356ce9ea62
commit 373d481613
2 changed files with 106 additions and 0 deletions

View File

@@ -7,15 +7,21 @@
import Foundation
import HealthKit
import os.log
@MainActor
class HealthKitManager: ObservableObject {
static let shared = HealthKitManager()
private let healthStore = HKHealthStore()
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.tt.ifeel", category: "HealthKit")
@Published var isAuthorized = false
@Published var authorizationError: Error?
@Published var isSyncing = false
@Published var syncProgress: Double = 0
@Published var syncedCount: Int = 0
@Published var totalToSync: Int = 0
// State of Mind sample type
private var stateOfMindType: HKSampleType? {
@@ -80,6 +86,76 @@ class HealthKitManager: ObservableObject {
try await healthStore.save(stateOfMind)
}
// MARK: - Sync All Existing Moods to HealthKit
/// Syncs all existing mood entries from the app to HealthKit
/// Call this after HealthKit authorization is granted to backfill historical data
func syncAllMoods() async {
guard isHealthKitAvailable else {
logger.warning("HealthKit not available, skipping sync")
return
}
guard checkAuthorizationStatus() == .sharingAuthorized else {
logger.warning("HealthKit not authorized for sharing, skipping sync")
return
}
isSyncing = true
syncProgress = 0
syncedCount = 0
// Fetch all mood entries
let allEntries = DataController.shared.getData(
startDate: Date(timeIntervalSince1970: 0),
endDate: Date(),
includedDays: [1, 2, 3, 4, 5, 6, 7]
)
// Filter out missing and placeholder entries
let validEntries = allEntries.filter { entry in
entry.mood != .missing && entry.mood != .placeholder
}
totalToSync = validEntries.count
logger.info("Starting HealthKit sync for \(validEntries.count) mood entries")
guard totalToSync > 0 else {
logger.info("No mood entries to sync")
isSyncing = false
return
}
var successCount = 0
var failCount = 0
for (index, entry) in validEntries.enumerated() {
do {
try await saveMood(entry.mood, for: entry.forDate, note: entry.notes)
successCount += 1
} catch {
failCount += 1
logger.error("Failed to sync mood for \(entry.forDate): \(error.localizedDescription)")
}
syncedCount = index + 1
syncProgress = Double(syncedCount) / Double(totalToSync)
// Add a small delay to avoid overwhelming HealthKit
if index % 10 == 0 && index > 0 {
try? await Task.sleep(nanoseconds: 50_000_000) // 50ms
}
}
isSyncing = false
logger.info("HealthKit sync complete: \(successCount) succeeded, \(failCount) failed")
EventLogger.log(event: "healthkit_sync_complete", withData: [
"total": totalToSync,
"success": successCount,
"failed": failCount
])
}
// MARK: - Read Mood from HealthKit
func fetchMoods(from startDate: Date, to endDate: Date) async throws -> [HKStateOfMind] {

View File

@@ -238,6 +238,8 @@ struct SettingsContentView: View {
// Request write permissions for State of Mind sync
do {
try await HealthKitManager.shared.requestAuthorization()
// Sync all existing moods to HealthKit
await HealthKitManager.shared.syncAllMoods()
} catch {
print("HealthKit write authorization failed: \(error)")
}
@@ -259,6 +261,19 @@ struct SettingsContentView: View {
}
}
.padding()
// Show sync progress
if HealthKitManager.shared.isSyncing {
VStack(spacing: 8) {
ProgressView(value: HealthKitManager.shared.syncProgress)
.tint(.red)
Text("Syncing moods to Health... \(HealthKitManager.shared.syncedCount)/\(HealthKitManager.shared.totalToSync)")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.horizontal)
.padding(.bottom, 8)
}
}
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
@@ -708,6 +723,8 @@ struct SettingsView: View {
// Request write permissions for State of Mind sync
do {
try await HealthKitManager.shared.requestAuthorization()
// Sync all existing moods to HealthKit
await HealthKitManager.shared.syncAllMoods()
} catch {
print("HealthKit write authorization failed: \(error)")
}
@@ -729,6 +746,19 @@ struct SettingsView: View {
}
}
.padding()
// Show sync progress
if HealthKitManager.shared.isSyncing {
VStack(spacing: 8) {
ProgressView(value: HealthKitManager.shared.syncProgress)
.tint(.red)
Text("Syncing moods to Health... \(HealthKitManager.shared.syncedCount)/\(HealthKitManager.shared.totalToSync)")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.horizontal)
.padding(.bottom, 8)
}
}
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])