Document Purpose: Step-by-step guide for iOS app implementation
Target Audience: iOS developers
Prerequisite Reading: "Complete Specification & Motivations"
The following rules apply to all code, logs, examples, tests, and documentation in this project:
If adding examples, use clearly synthetic data: "Device: iPhone-TESTDEVICE", "User: Test User", "2000-01-01".
import HealthKit
class HealthKitManager {
static let shared = HealthKitManager()
let healthStore = HKHealthStore()
let typesToRead: Set<HKSampleType> = [
HKWorkoutType.workoutType(),
HKQuantityType.quantityType(forIdentifier: .heartRate)!,
HKQuantityType.quantityType(forIdentifier: .stepCount)!,
HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKCategoryType.categoryType(forIdentifier: .sleepAnalysis)!,
HKActivitySummaryType.activitySummaryType(),
]
func requestAuthorization(completion: @escaping (Bool, Error?) -> Void) {
healthStore.requestAuthorization(toShare: [], read: typesToRead) { success, error in
completion(success, error)
}
}
}
Purpose: Efficient incremental queries that only fetch changes since last check
class AnchoredQueryManager {
let defaults = UserDefaults(suiteName: "group.com.healthprobe.data")
func loadAnchor(for sampleType: HKSampleType) -> HKQueryAnchor? {
guard let data = defaults?.data(forKey: "anchor_\(sampleType.identifier)") else {
return nil
}
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data)
}
func saveAnchor(_ anchor: HKQueryAnchor, for sampleType: HKSampleType) {
let data = try? NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true)
defaults?.set(data, forKey: "anchor_\(sampleType.identifier)")
}
func executeAnchoredQuery(
sampleType: HKSampleType,
completion: @escaping ([HKSample], [HKDeletedObject], HKQueryAnchor) -> Void
) {
let anchor = loadAnchor(for: sampleType) ?? HKQueryAnchor(byAdding: 0)
let query = HKAnchoredObjectQuery(
type: sampleType,
predicate: nil,
anchor: anchor,
limit: HKObjectQueryNoLimit
) { _, samples, deletedObjects, newAnchor, error in
guard let newAnchor = newAnchor else { return }
self.saveAnchor(newAnchor, for: sampleType)
completion(samples ?? [], deletedObjects ?? [], newAnchor)
}
healthStore.execute(query)
}
}
class HealthKitObserver {
func setupObserverQueries(for types: [HKSampleType], handler: @escaping (HKSampleType) -> Void) {
for sampleType in types {
let query = HKObserverQuery(sampleType: sampleType, predicate: nil) { _, completionHandler, error in
if error == nil {
handler(sampleType)
}
completionHandler()
}
healthStore.execute(query)
// Important: Keep strong reference to prevent query from being deallocated
activeQueries.append(query)
}
}
// Call this when background notification arrives
func backgroundFetch(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// Re-run anchored queries to detect changes
// Update snapshots and detect anomalies
// Persist any findings
completionHandler(.newData)
}
}
HealthProbe uses two storage layers:
Local Archive Store (source of truth)
firstSeen, lastSeen, lastVerified, strict/semantic/fuzzy fingerprints, and integrity hashesSwiftData UI Store (derived/cache layer)
import SwiftData
import Foundation
// MARK: - Core Models
@Model
final class HealthSnapshot {
/// Unique identifier
@Attribute(.unique) var id: String = UUID().uuidString
/// When this snapshot was captured
var capturedAt: Date
/// Sample type (e.g., "HKWorkout", "HKQuantity:HeartRate")
var sampleType: String
/// Source device (e.g., "iPhone 15 Pro", "Apple Watch")
var sourceDevice: String
/// Total samples of this type at capture time
var recordCount: Int
/// MD5 of aggregated sample IDs (for integrity checking)
var integrityChecksum: String
/// Aggregated counts by source: { "iPhone Health": 1200, "Apple Watch": 450 }
var sourceDistribution: [String: Int]
/// Metadata
var iosVersion: String
var appVersion: String
init(
capturedAt: Date,
sampleType: String,
sourceDevice: String,
recordCount: Int,
integrityChecksum: String,
sourceDistribution: [String: Int],
iosVersion: String,
appVersion: String
) {
self.capturedAt = capturedAt
self.sampleType = sampleType
self.sourceDevice = sourceDevice
self.recordCount = recordCount
self.integrityChecksum = integrityChecksum
self.sourceDistribution = sourceDistribution
self.iosVersion = iosVersion
self.appVersion = appVersion
}
}
@Model
final class AuditTrailEntry {
@Attribute(.unique) var id: String = UUID().uuidString
var timestamp: Date
var eventType: String // "snapshot", "sync_event", "anomaly_detected", etc.
var message: String
var context: [String: String] // JSON-serializable context
init(timestamp: Date, eventType: String, message: String, context: [String: String] = [:]) {
self.timestamp = timestamp
self.eventType = eventType
self.message = message
self.context = context
}
}
@Model
final class DetectedAnomaly {
@Attribute(.unique) var id: String = UUID().uuidString
var detectedAt: Date
var type: String // "historical_insertion", "silent_deletion", "duplicate", "divergence"
var severity: String // "info", "warning", "critical"
var sampleType: String
var summary: String
var evidence: [String: String] // Forensic data
var resolved: Bool = false
var resolvedAt: Date?
init(
detectedAt: Date,
type: String,
severity: String,
sampleType: String,
summary: String,
evidence: [String: String] = [:]
) {
self.detectedAt = detectedAt
self.type = type
self.severity = severity
self.sampleType = sampleType
self.summary = summary
self.evidence = evidence
}
}
@Model
final class ContextStateChange {
@Attribute(.unique) var id: String = UUID().uuidString
var timestamp: Date
var previousState: String // "local_only", "icloud_enabled", "icloud_sync_active"
var newState: String
var details: String
init(timestamp: Date, previousState: String, newState: String, details: String = "") {
self.timestamp = timestamp
self.previousState = previousState
self.newState = newState
self.details = details
}
}
// MARK: - Model Container Setup
func createModelContainer() throws -> ModelContainer {
let schema = Schema([
HealthSnapshot.self,
AuditTrailEntry.self,
DetectedAnomaly.self,
ContextStateChange.self,
])
let modelConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
cloudKitDatabase: .none // Local only in MVP
)
return try ModelContainer(for: schema, configurations: [modelConfiguration])
}
The archive store should expose a small service interface rather than leaking SQL/archive details into UI code:
protocol HealthArchiveStore {
func upsertSamples(_ samples: [HKSample], observedAt: Date) async throws -> HealthArchiveWriteSummary
func markVerification(sampleType: HKSampleType, verifiedAt: Date) async throws
func recordDisappearance(sampleUUIDHash: String, sampleTypeIdentifier: String, observedMissingAt: Date) async throws
func records(for request: HealthArchiveRecordRequest) async throws -> [ArchivedHealthRecord]
func exportReport(_ request: HealthArchiveReportRequest) async throws -> URL
}
Archive rows should preserve:
- HealthKit UUID where exposed
- type identifier, start/end date, value, unit
- source, source revision, bundle identifier, version/build/product type where available
- HKDevice fields exposed by HealthKit
- full metadata dictionary as structured data
- relationship keys for workouts, events, and related samples where available
- fingerprints for matching records across HealthProbe, Apple Health XML exports, and backup database extracts
The MVP implementation is SQLiteHealthArchiveStore, an actor-isolated SQLite archive in Application Support. It is populated from HealthKit anchored-query pages before SwiftData receives derived snapshot/index rows.
class AnomalyDetector {
private let modelContext: ModelContext
private let healthKitManager: HealthKitManager
// MARK: - Historical Insertion Detection
func detectHistoricalInsertions(
newSamples: [HKSample],
completion: @escaping ([DetectedAnomaly]) -> Void
) {
var anomalies: [DetectedAnomaly] = []
let now = Date()
for sample in newSamples {
let ageInDays = Calendar.current.dateComponents([.day], from: sample.startDate, to: now).day ?? 0
// Check if sample is older than 7 days but was just added
if ageInDays > 7 {
let anomaly = DetectedAnomaly(
detectedAt: now,
type: "historical_insertion",
severity: "medium",
sampleType: sample.sampleType.identifier,
summary: "Sample from \(ageInDays) days ago appeared in HealthKit",
evidence: [
"original_date": ISO8601DateFormatter().string(from: sample.startDate),
"age_days": String(ageInDays),
"sample_id": sample.uuid.uuidString,
]
)
anomalies.append(anomaly)
}
}
completion(anomalies)
}
// MARK: - Silent Deletion Detection
func detectSilentDeletions(
previousSnapshot: HealthSnapshot,
currentSnapshot: HealthSnapshot,
completion: @escaping ([DetectedAnomaly]) -> Void
) {
var anomalies: [DetectedAnomaly] = []
let previousCount = previousSnapshot.recordCount
let currentCount = currentSnapshot.recordCount
let loss = previousCount - currentCount
if loss > 0 {
let lossPercent = Double(loss) / Double(previousCount) * 100
let severity = lossPercent > 10 ? "critical" : lossPercent > 5 ? "warning" : "info"
let anomaly = DetectedAnomaly(
detectedAt: Date(),
type: "silent_deletion",
severity: severity,
sampleType: previousSnapshot.sampleType,
summary: "\(loss) samples missing (\(String(format: "%.1f", lossPercent))%)",
evidence: [
"previous_count": String(previousCount),
"current_count": String(currentCount),
"loss_count": String(loss),
"loss_percent": String(format: "%.1f", lossPercent),
"time_gap": String(describing: Date().timeIntervalSince(previousSnapshot.capturedAt)),
]
)
anomalies.append(anomaly)
}
completion(anomalies)
}
// MARK: - Duplicate Detection
func detectDuplicates(
samples: [HKSample],
completion: @escaping ([DetectedAnomaly]) -> Void
) {
var anomalies: [DetectedAnomaly] = []
var fingerprints: [String: [HKSample]] = [:]
// Group by fingerprint
for sample in samples {
let fingerprint = createFingerprint(for: sample)
fingerprints[fingerprint, default: []].append(sample)
}
// Find duplicates
for (fingerprint, dupes) in fingerprints where dupes.count > 1 {
let anomaly = DetectedAnomaly(
detectedAt: Date(),
type: "duplicate",
severity: "low",
sampleType: dupes[0].sampleType.identifier,
summary: "\(dupes.count) duplicate records found",
evidence: [
"fingerprint": fingerprint,
"count": String(dupes.count),
]
)
anomalies.append(anomaly)
}
completion(anomalies)
}
// MARK: - Divergence Detection
func detectDivergence(
currentTrend: [Date: Double],
historicalBaseline: [Date: Double],
completion: @escaping ([DetectedAnomaly]) -> Void
) {
// Calculate standard deviations
let baselineStdDev = standardDeviation(values: Array(historicalBaseline.values))
let currentStdDev = standardDeviation(values: Array(currentTrend.values))
if currentStdDev > baselineStdDev * 2.0 {
let anomaly = DetectedAnomaly(
detectedAt: Date(),
type: "divergence",
severity: "medium",
sampleType: "aggregated_metric",
summary: "Unusual trend detected (σ increased \(currentStdDev / baselineStdDev)x)",
evidence: [
"baseline_stddev": String(format: "%.2f", baselineStdDev),
"current_stddev": String(format: "%.2f", currentStdDev),
"ratio": String(format: "%.2f", currentStdDev / baselineStdDev),
]
)
completion([anomaly])
} else {
completion([])
}
}
// MARK: - Helpers
private func createFingerprint(for sample: HKSample) -> String {
let formatter = ISO8601DateFormatter()
let startStr = formatter.string(from: sample.startDate)
let endStr = formatter.string(from: sample.endDate)
let type = sample.sampleType.identifier
let source = sample.sourceRevision.source.name
return "\(type)|\(startStr)|\(endStr)|\(source)".addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? ""
}
private func standardDeviation(values: [Double]) -> Double {
let mean = values.reduce(0, +) / Double(values.count)
let squaredDiffs = values.map { pow($0 - mean, 2) }
let variance = squaredDiffs.reduce(0, +) / Double(values.count)
return sqrt(variance)
}
}
HealthProbe does not sync its own database through iCloud/CloudKit. This service only logs Health/iCloud state as context for later forensic correlation.
class ContextMonitor {
private let modelContext: ModelContext
private let queue = DispatchQueue(label: "com.healthprobe.sync-monitor", qos: .background)
private var previousHealthCloudState: String = "unknown"
func startMonitoring() {
queue.async {
self.monitorContext()
}
}
private func monitorContext() {
// Check iCloud state
let iCloudToken = FileManager.default.ubiquityIdentityToken
let currentState = iCloudToken != nil ? "icloud_enabled" : "local_only"
if currentState != previousHealthCloudState {
logContextChange(from: previousHealthCloudState, to: currentState)
previousHealthCloudState = currentState
// Schedule archive verification on state change
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name("HealthContextChanged"), object: nil)
}
}
}
private func logContextChange(from: String, to: String) {
let change = ContextStateChange(
timestamp: Date(),
previousState: from,
newState: to,
details: "iCloud state changed"
)
do {
modelContext.insert(change)
try modelContext.save()
let auditEntry = AuditTrailEntry(
timestamp: Date(),
eventType: "health_context_change",
message: "Health cloud context: \(from) → \(to)",
context: ["previous": from, "current": to]
)
modelContext.insert(auditEntry)
try modelContext.save()
} catch {
print("Error logging context change: \(error)")
}
}
}
@main
struct HealthProbeApp: App {
@StateObject private var healthKitManager = HealthKitManager.shared
@StateObject private var contextMonitor: ContextMonitor
let modelContainer: ModelContainer
init() {
do {
modelContainer = try createModelContainer()
let context = ModelContext(modelContainer)
_contextMonitor = StateObject(wrappedValue: ContextMonitor(modelContext: context))
} catch {
fatalError("Could not initialize model container: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(modelContainer)
.onAppear {
// Request HealthKit permissions
healthKitManager.requestAuthorization { success, error in
if success {
// Start context monitoring and archive capture
contextMonitor.startMonitoring()
captureInitialSnapshot()
}
}
}
.onReceive(Timer.publish(every: 3600).autoconnect()) { _ in
// Periodic check every hour
refreshHealthData()
}
}
}
private func captureInitialSnapshot() {
// Implement snapshot capture
}
private func refreshHealthData() {
// Implement periodic refresh
}
}
class AnomalyDetectorTests: XCTestCase {
var detector: AnomalyDetector!
override func setUp() {
super.setUp()
detector = AnomalyDetector(...)
}
func testDetectsHistoricalInsertion() {
// Create sample from 30 days ago
// Assert: anomaly detected
}
func testDetectsSilentDeletion() {
// Create two snapshots, second has fewer records
// Assert: anomaly detected with correct loss percentage
}
}
| Operation | Target | Notes |
|---|---|---|
| Anchored query | < 5 sec | Background, user perceives delay > 2s |
| Anomaly detection | < 2 sec | Should not block UI |
| SwiftData cache update | < 1 sec | Can run on main thread only after archive work completes |
| Archive write | Background | Stream large imports; never build full high-frequency datasets in memory |
| Background check | < 30 sec | iOS allows 30 min for background fetch |
HealthProbe Implementation Guide v1.0 — 2026-05-01