1 contributor
import Foundation
struct DataTypeSnapshotContext: Equatable, Sendable {
let observationID: Int64
let observedAt: Date
}
@Observable
final class DataTypesViewModel {
var filter: DiffFilter = .all
var comparisonMode: ComparisonMode = .previous
var observationRows: [CachedArchiveObservationRow] = []
var observationRowsError: String?
var archiveDiffs: [TypeDiff]?
var archiveDiffError: String?
var observationContexts: [DataTypeSnapshotContext] {
observationRows
.sorted { $0.observedAt > $1.observedAt }
.map {
DataTypeSnapshotContext(
observationID: $0.observationID,
observedAt: $0.observedAt
)
}
}
func diffs() -> [TypeDiff] {
guard let archiveDiffs else { return [] }
return apply(filter: filter, to: archiveDiffs)
}
func baseline(for snapshot: DataTypeSnapshotContext, in snapshots: [DataTypeSnapshotContext]) -> DataTypeSnapshotContext? {
resolveBaseline(for: snapshot, in: snapshots)
}
func clearArchiveDiffs() {
archiveDiffs = nil
archiveDiffError = nil
}
@MainActor
func loadArchiveRows(limit: Int = 200) async {
do {
let cache = try CoreDataArchiveCacheStore()
observationRows = try cache.observationRows(limit: limit)
observationRowsError = nil
} catch {
observationRows = []
observationRowsError = error.localizedDescription
}
}
@MainActor
func loadArchiveDiffs(current: DataTypeSnapshotContext?, snapshots: [DataTypeSnapshotContext]) async {
guard let current,
let baseline = resolveBaseline(for: current, in: snapshots) else {
clearArchiveDiffs()
return
}
do {
let cache = try CoreDataArchiveCacheStore()
let currentSummaries = try cache.typeSummaries(observationID: current.observationID)
let baselineSummaries = try cache.typeSummaries(observationID: baseline.observationID)
let currentByType = Dictionary(uniqueKeysWithValues: currentSummaries.map { ($0.sampleTypeIdentifier, $0) })
let baselineByType = Dictionary(uniqueKeysWithValues: baselineSummaries.map { ($0.sampleTypeIdentifier, $0) })
let allTypeIdentifiers = Set(currentByType.keys).union(baselineByType.keys)
var archiveRows: [TypeDiff] = []
archiveRows.reserveCapacity(allTypeIdentifiers.count)
for typeIdentifier in allTypeIdentifiers {
let summary = currentByType[typeIdentifier]
let baselineSummary = baselineByType[typeIdentifier]
let diff = try await SQLiteHealthArchiveStore.shared.diffSummary(HealthArchiveDiffRequest(
fromObservationID: baseline.observationID,
toObservationID: current.observationID,
sampleTypeIdentifier: typeIdentifier
))
archiveRows.append(TypeDiff(
id: typeIdentifier,
typeIdentifier: typeIdentifier,
displayName: summary?.displayName ?? baselineSummary?.displayName ?? typeIdentifier,
currentCount: summary?.visibleRecordCount ?? 0,
previousCount: baselineSummary?.visibleRecordCount ?? 0,
previousTracked: baselineSummary != nil,
appearedCount: diff.appearedCount,
disappearedCount: diff.disappearedCount,
representationChangedCount: diff.representationChangedCount
))
}
archiveDiffs = archiveRows.sorted {
$0.displayName.localizedCompare($1.displayName) == .orderedAscending
}
archiveDiffError = nil
} catch {
archiveDiffs = nil
archiveDiffError = error.localizedDescription
}
}
private func resolveBaseline(
for snapshot: DataTypeSnapshotContext,
in snapshots: [DataTypeSnapshotContext]
) -> DataTypeSnapshotContext? {
let sorted = snapshots.sorted { $0.observedAt > $1.observedAt }
switch comparisonMode {
case .previous:
return sorted.first { $0.observedAt < snapshot.observedAt }
case .selected:
return nil // DataTypesView uses .previous by default; selection lives in SnapshotsTab
case .relativeTime(let interval):
let target = snapshot.observedAt.addingTimeInterval(-interval)
return snapshots
.filter { $0.observedAt <= target }
.max { $0.observedAt < $1.observedAt }
}
}
private func apply(filter: DiffFilter, to diffs: [TypeDiff]) -> [TypeDiff] {
switch filter {
case .all: return diffs
case .changed: return diffs.filter { $0.previousTracked && $0.hasChanges }
case .increased: return diffs.filter { $0.previousTracked && $0.delta > 0 }
case .decreased: return diffs.filter { $0.previousTracked && $0.delta < 0 }
}
}
}