HealthProbe / HealthProbe / ViewModels / DataTypesViewModel.swift
Newer Older
130 lines | 5.213kb
Bogdan Timofte authored a month ago
1
import Foundation
2

            
Bogdan Timofte authored a week ago
3
struct DataTypeSnapshotContext: Equatable, Sendable {
Bogdan Timofte authored a week ago
4
    let observationID: Int64
Bogdan Timofte authored a week ago
5
    let observedAt: Date
6
}
7

            
Bogdan Timofte authored a month ago
8
@Observable
9
final class DataTypesViewModel {
10
    var filter: DiffFilter = .all
11
    var comparisonMode: ComparisonMode = .previous
Bogdan Timofte authored a week ago
12
    var observationRows: [CachedArchiveObservationRow] = []
13
    var observationRowsError: String?
Bogdan Timofte authored 2 weeks ago
14
    var archiveDiffs: [TypeDiff]?
15
    var archiveDiffError: String?
Bogdan Timofte authored a month ago
16

            
Bogdan Timofte authored a week ago
17
    var observationContexts: [DataTypeSnapshotContext] {
18
        observationRows
19
            .sorted { $0.observedAt > $1.observedAt }
20
            .map {
21
                DataTypeSnapshotContext(
22
                    observationID: $0.observationID,
23
                    observedAt: $0.observedAt
24
                )
25
            }
26
    }
27

            
Bogdan Timofte authored a week ago
28
    func diffs() -> [TypeDiff] {
29
        guard let archiveDiffs else { return [] }
30
        return apply(filter: filter, to: archiveDiffs)
Bogdan Timofte authored a month ago
31
    }
32

            
Bogdan Timofte authored a week ago
33
    func baseline(for snapshot: DataTypeSnapshotContext, in snapshots: [DataTypeSnapshotContext]) -> DataTypeSnapshotContext? {
Bogdan Timofte authored 2 weeks ago
34
        resolveBaseline(for: snapshot, in: snapshots)
35
    }
36

            
37
    func clearArchiveDiffs() {
38
        archiveDiffs = nil
39
        archiveDiffError = nil
40
    }
41

            
Bogdan Timofte authored a week ago
42
    @MainActor
43
    func loadArchiveRows(limit: Int = 200) async {
44
        do {
45
            let cache = try CoreDataArchiveCacheStore()
46
            observationRows = try cache.observationRows(limit: limit)
47
            observationRowsError = nil
48
        } catch {
49
            observationRows = []
50
            observationRowsError = error.localizedDescription
51
        }
52
    }
53

            
Bogdan Timofte authored 2 weeks ago
54
    @MainActor
Bogdan Timofte authored a week ago
55
    func loadArchiveDiffs(current: DataTypeSnapshotContext?, snapshots: [DataTypeSnapshotContext]) async {
Bogdan Timofte authored 2 weeks ago
56
        guard let current,
Bogdan Timofte authored a week ago
57
              let baseline = resolveBaseline(for: current, in: snapshots) else {
Bogdan Timofte authored 2 weeks ago
58
            clearArchiveDiffs()
59
            return
60
        }
61

            
62
        do {
63
            let cache = try CoreDataArchiveCacheStore()
Bogdan Timofte authored a week ago
64
            let currentSummaries = try cache.typeSummaries(observationID: current.observationID)
65
            let baselineSummaries = try cache.typeSummaries(observationID: baseline.observationID)
Bogdan Timofte authored 2 weeks ago
66
            let currentByType = Dictionary(uniqueKeysWithValues: currentSummaries.map { ($0.sampleTypeIdentifier, $0) })
67
            let baselineByType = Dictionary(uniqueKeysWithValues: baselineSummaries.map { ($0.sampleTypeIdentifier, $0) })
68
            let allTypeIdentifiers = Set(currentByType.keys).union(baselineByType.keys)
69

            
70
            var archiveRows: [TypeDiff] = []
71
            archiveRows.reserveCapacity(allTypeIdentifiers.count)
72

            
73
            for typeIdentifier in allTypeIdentifiers {
74
                let summary = currentByType[typeIdentifier]
75
                let baselineSummary = baselineByType[typeIdentifier]
76
                let diff = try await SQLiteHealthArchiveStore.shared.diffSummary(HealthArchiveDiffRequest(
Bogdan Timofte authored a week ago
77
                    fromObservationID: baseline.observationID,
78
                    toObservationID: current.observationID,
Bogdan Timofte authored 2 weeks ago
79
                    sampleTypeIdentifier: typeIdentifier
80
                ))
81
                archiveRows.append(TypeDiff(
82
                    id: typeIdentifier,
83
                    typeIdentifier: typeIdentifier,
84
                    displayName: summary?.displayName ?? baselineSummary?.displayName ?? typeIdentifier,
85
                    currentCount: summary?.visibleRecordCount ?? 0,
86
                    previousCount: baselineSummary?.visibleRecordCount ?? 0,
87
                    previousTracked: baselineSummary != nil,
88
                    appearedCount: diff.appearedCount,
89
                    disappearedCount: diff.disappearedCount,
90
                    representationChangedCount: diff.representationChangedCount
91
                ))
92
            }
93

            
94
            archiveDiffs = archiveRows.sorted {
95
                $0.displayName.localizedCompare($1.displayName) == .orderedAscending
96
            }
97
            archiveDiffError = nil
98
        } catch {
99
            archiveDiffs = nil
100
            archiveDiffError = error.localizedDescription
101
        }
102
    }
103

            
Bogdan Timofte authored a week ago
104
    private func resolveBaseline(
105
        for snapshot: DataTypeSnapshotContext,
106
        in snapshots: [DataTypeSnapshotContext]
107
    ) -> DataTypeSnapshotContext? {
108
        let sorted = snapshots.sorted { $0.observedAt > $1.observedAt }
Bogdan Timofte authored a month ago
109
        switch comparisonMode {
110
        case .previous:
Bogdan Timofte authored a week ago
111
            return sorted.first { $0.observedAt < snapshot.observedAt }
Bogdan Timofte authored a month ago
112
        case .selected:
113
            return nil  // DataTypesView uses .previous by default; selection lives in SnapshotsTab
114
        case .relativeTime(let interval):
Bogdan Timofte authored a week ago
115
            let target = snapshot.observedAt.addingTimeInterval(-interval)
116
            return snapshots
117
                .filter { $0.observedAt <= target }
118
                .max { $0.observedAt < $1.observedAt }
119
        }
120
    }
121

            
122
    private func apply(filter: DiffFilter, to diffs: [TypeDiff]) -> [TypeDiff] {
123
        switch filter {
124
        case .all:       return diffs
125
        case .changed:   return diffs.filter { $0.previousTracked && $0.hasChanges }
126
        case .increased: return diffs.filter { $0.previousTracked && $0.delta > 0 }
127
        case .decreased: return diffs.filter { $0.previousTracked && $0.delta < 0 }
Bogdan Timofte authored a month ago
128
        }
129
    }
130
}