@@ -27,8 +27,8 @@ There are no real deployments, only test installations. Existing prototype datab |
||
| 27 | 27 |
| HealthKit capture | Capture now opens one archive observation per user-visible snapshot and attaches HealthKit pages, deleted-object evidence, and type verification to that observation id before finishing it | Continue moving UI/cache reads to archive-backed observation ids | |
| 28 | 28 |
| SQLite archive | Archive v2 schema, snapshot-level observation grouping, differential write path, v2 verification/delete bookkeeping, daily aggregate rebuilds, integrity report, v2 record reads, SQL diff/count/aggregate/provenance/consolidation-evidence APIs, large synthetic diff pagination coverage, formal timing/memory metrics, and XCTest coverage are in place; the legacy `archive_samples` mirror has been removed | Continue moving capture/Dashboard actions to archive/cache DTOs | |
| 29 | 29 |
| Core Data cache | Initial programmatic Core Data model, full-cache rebuild service, read DTOs for observation/type/diff/health rows, and Dashboard archive-cache status wiring are in place | Move remaining export/report paths to cache DTOs and add targeted partial invalidation | |
| 30 |
-| SwiftData cache | Exists; test builds now reset legacy prototype UI/archive/cache stores once for archive v2 so old SwiftData-only snapshots are not treated as backed-up observations. Metric timeout calibration, local device profile settings, operation logging, ContentView preview, Settings data maintenance, legacy detail/PDF views, unused legacy repair/observer services, Dashboard view access, and legacy anomaly/count-drop review have moved outside SwiftData or been removed. Remaining SwiftData imports are inventoried in [`SwiftData-Retirement-Inventory.md`](SwiftData-Retirement-Inventory.md) | Treat as disposable prototype data; replace capture/review actions before removing `ModelContainer` | |
|
| 31 |
-| UI | Prototype exists; Dashboard status reads archive/cache observation rows and shows cache health, and `DashboardView` no longer imports SwiftData or reads `ModelContext`; capture/review actions still route through a Dashboard view-model legacy bridge. Snapshots and Data Types tab roots no longer import SwiftData, load Core Data cached observation rows, and open archive/cache-backed detail rows; `SnapshotArchiveDetailView` and `DataTypeArchiveDetailView` read Core Data type/diff summaries and page record drill-down through SQLite; unused legacy SwiftData snapshot/type detail and PDF views have been deleted; record-change evolution and temporal distribution screens now receive DTO rows/cache input instead of querying SwiftData directly; export preview reads the archive export API before showing/exporting JSON; simplified detail mode replaces heavy charts with summary rows on small/accessibility layouts or when enabled in Settings; visible change labels now use neutral new/missing/change-review language | Move remaining Dashboard view-model capture/review actions away from SwiftData | |
|
| 30 |
+| SwiftData cache | Exists; test builds now reset legacy prototype UI/archive/cache stores once for archive v2 so old SwiftData-only snapshots are not treated as backed-up observations. Metric timeout calibration, local device profile settings, operation logging, ContentView preview, Settings data maintenance, legacy detail/PDF views, unused legacy repair/observer services, Dashboard view/view-model access, and legacy anomaly/count-drop review have moved outside SwiftData or been removed. Remaining SwiftData imports are inventoried in [`SwiftData-Retirement-Inventory.md`](SwiftData-Retirement-Inventory.md) | Treat as disposable prototype data; stop returning/storing `HealthSnapshot` bridge handles before removing `ModelContainer` | |
|
| 31 |
+| UI | Prototype exists; Dashboard status reads archive/cache observation rows and shows cache health, and Dashboard view/view-model code no longer imports SwiftData or reads `ModelContext`; capture/review actions still route through a legacy bridge isolated in `HealthKitService`. Snapshots and Data Types tab roots no longer import SwiftData, load Core Data cached observation rows, and open archive/cache-backed detail rows; `SnapshotArchiveDetailView` and `DataTypeArchiveDetailView` read Core Data type/diff summaries and page record drill-down through SQLite; unused legacy SwiftData snapshot/type detail and PDF views have been deleted; record-change evolution and temporal distribution screens now receive DTO rows/cache input instead of querying SwiftData directly; export preview reads the archive export API before showing/exporting JSON; simplified detail mode replaces heavy charts with summary rows on small/accessibility layouts or when enabled in Settings; visible change labels now use neutral new/missing/change-review language | Stop returning/storing prototype `HealthSnapshot` handles from capture/review | |
|
| 32 | 32 |
| Diff/change explanation | SQL diff summaries, paged diff records, aggregate comparisons, and consolidation-evidence labels exist; legacy anomaly/count-drop review has been removed from active flows; the old direct `HealthSnapshot.typeCounts` diff helper has been retired | Keep active diff/count views on archive/cache DTOs | |
| 33 | 33 |
| Export | SQLite export preview, paged JSON writing, SHA256 manifest hashing, and `export_manifests` rows are in place for selected records and observation diffs | Fill remaining recovery-compatible envelope metadata, CSV export, relationship preservation, and reproducibility checks | |
| 34 | 34 |
| Legacy device support | Simplified detail UI mode is implemented for small/accessibility layouts and as a Settings toggle | Remove SwiftData dependency and validate lower deployment targets | |
@@ -38,7 +38,7 @@ There are no real deployments, only test installations. Existing prototype datab |
||
| 38 | 38 |
|
| 39 | 39 |
Detailed checkable milestones live in [`Refactoring-Plan.md`](Refactoring-Plan.md). |
| 40 | 40 |
|
| 41 |
-1. Move remaining Dashboard capture/review actions away from SwiftData. |
|
| 41 |
+1. Stop returning/storing prototype `HealthSnapshot` handles from capture/review. |
|
| 42 | 42 |
2. Add targeted cache invalidation for affected observation/type ranges. |
| 43 | 43 |
3. Finish remaining UI language cleanup from anomaly/status to observation/diff/export where legacy model names still leak into active flows. |
| 44 | 44 |
4. Complete recovery-compatible export metadata, CSV output, and reproducibility checks. |
@@ -48,7 +48,7 @@ Detailed checkable milestones live in [`Refactoring-Plan.md`](Refactoring-Plan.m |
||
| 48 | 48 |
|
| 49 | 49 |
- SwiftData currently blocks iOS 15-era device support. |
| 50 | 50 |
- Some screens still imply snapshot-count monitoring rather than Time Machine inspection. |
| 51 |
-- Current UI/cache layers still depend on 5 SwiftData-backed files for launch container, capture review actions, capture bridge writes, and remaining model definitions. |
|
| 51 |
+- Current UI/cache layers still depend on 4 SwiftData-backed files for launch container, capture bridge writes, and remaining model definitions. |
|
| 52 | 52 |
- Snapshots timeline/detail rows, Data Types list/detail rows, and record drill-down are archive/cache-backed for new archive v2 observations when cache rows exist. |
| 53 | 53 |
- Legacy SwiftData-only snapshots are reset for archive v2 test installs rather than migrated. |
| 54 | 54 |
- Capture strategy and some legacy SwiftData transition paths may still decode or cache too much data for low-end devices. |
@@ -224,9 +224,9 @@ Acceptance: |
||
| 224 | 224 |
Checklist: |
| 225 | 225 |
- [ ] Replace direct SwiftData `@Query` dependencies for target screens. |
| 226 | 226 |
- [x] Dashboard status reads Core Data cached observation rows and cache health, |
| 227 |
- and `DashboardView` no longer imports SwiftData or reads `ModelContext`. |
|
| 228 |
- Capture/review actions still route through the Dashboard view model's legacy |
|
| 229 |
- bridge. |
|
| 227 |
+ and Dashboard view/view-model code no longer imports SwiftData or reads |
|
| 228 |
+ `ModelContext`. Capture/review actions still route through the legacy bridge |
|
| 229 |
+ isolated in `HealthKitService`. |
|
| 230 | 230 |
- [x] Observation timeline rows read Core Data cache when available and no |
| 231 | 231 |
longer query `SnapshotDelta` list summaries. |
| 232 | 232 |
- [x] Observation root and archive detail use cached summary/type rows plus |
@@ -268,12 +268,13 @@ Checklist: |
||
| 268 | 268 |
moved to local Codable stores and removed from `ModelContainer`; Settings |
| 269 | 269 |
data maintenance now uses the rebuildable Core Data cache; legacy |
| 270 | 270 |
anomaly/count-drop review has been deleted; Snapshots/Data Types tab roots no |
| 271 |
- longer import SwiftData; `DashboardView` no longer imports SwiftData or reads |
|
| 272 |
- `ModelContext`; unused legacy snapshot/type detail and PDF views have been |
|
| 273 |
- deleted; unused legacy lifecycle/observer/repair services have been deleted; |
|
| 274 |
- unused legacy delta service/models have been deleted; `HealthRecord`, |
|
| 275 |
- `YearlyCount`, and `TypeDistributionBin` are no longer SwiftData models; |
|
| 276 |
- Dashboard view-model capture/review actions and capture bridge writes remain. |
|
| 271 |
+ longer import SwiftData; Dashboard view/view-model code no longer imports |
|
| 272 |
+ SwiftData or reads `ModelContext`; unused legacy snapshot/type detail and PDF |
|
| 273 |
+ views have been deleted; unused legacy lifecycle/observer/repair services have |
|
| 274 |
+ been deleted; unused legacy delta service/models have been deleted; |
|
| 275 |
+ `HealthRecord`, `YearlyCount`, and `TypeDistributionBin` are no longer |
|
| 276 |
+ SwiftData models; capture/review still returns/stores prototype |
|
| 277 |
+ `HealthSnapshot` handles through the legacy bridge. |
|
| 277 | 278 |
- [ ] Remove/disable `ModelContainer` as required for target builds. |
| 278 | 279 |
- [x] Add prototype-store ignore/delete/reset path for test installs. |
| 279 | 280 |
- [ ] Verify no old-store compatibility layer remains in active flows. |
@@ -9,10 +9,10 @@ local settings stored outside SwiftData where needed. |
||
| 9 | 9 |
|
| 10 | 10 |
## Current Count |
| 11 | 11 |
|
| 12 |
-After moving the Snapshots, Data Types, and Dashboard view roots to |
|
| 13 |
-archive/cache observations or view-model bridges, 5 app files still have |
|
| 14 |
-SwiftData imports because capture, Dashboard review actions, and remaining model |
|
| 15 |
-definitions still use prototype snapshot handles. |
|
| 12 |
+After moving the Snapshots, Data Types, Dashboard view, and Dashboard view model |
|
| 13 |
+roots away from direct SwiftData imports, 4 app files still have SwiftData |
|
| 14 |
+imports because launch, capture bridge writes, and remaining model definitions |
|
| 15 |
+still use prototype snapshot handles. |
|
| 16 | 16 |
|
| 17 | 17 |
## Launch Container |
| 18 | 18 |
|
@@ -49,13 +49,14 @@ Retirement path: |
||
| 49 | 49 |
|
| 50 | 50 |
## UI And View Models |
| 51 | 51 |
|
| 52 |
-These active surfaces still use `@Query`, `ModelContext`, or SwiftData model |
|
| 53 |
-types: |
|
| 54 |
- |
|
| 55 |
-- `HealthProbe/ViewModels/DashboardViewModel.swift` |
|
| 52 |
+No active UI/view-model file currently imports SwiftData. Dashboard |
|
| 53 |
+capture/review actions still hold legacy `HealthSnapshot` handles while the |
|
| 54 |
+bridge exists, but `ModelContext` and `FetchDescriptor` access is isolated in |
|
| 55 |
+`HealthKitService`. |
|
| 56 | 56 |
|
| 57 | 57 |
Retirement path: |
| 58 |
-- move capture/review actions away from `ModelContext`; |
|
| 58 |
+- stop returning/storing prototype `HealthSnapshot` handles from capture/review |
|
| 59 |
+ flows; |
|
| 59 | 60 |
- keep status/report rows on archive/cache APIs. |
| 60 | 61 |
|
| 61 | 62 |
## Removed During This Pass |
@@ -83,12 +84,12 @@ The following SwiftData dependencies were removed from active flows: |
||
| 83 | 84 |
- `HealthProbe/Views/Settings/SettingsView.swift` no longer imports SwiftData. |
| 84 | 85 |
Its Data section now reports/rebuilds/deletes the rebuildable Core Data UI |
| 85 | 86 |
cache and leaves the SQLite archive untouched. |
| 86 |
-- `HealthProbe/Views/Dashboard/DashboardView.swift` no longer queries |
|
| 87 |
- `HealthSnapshot` for status rows; Dashboard status now uses archive/cache rows |
|
| 88 |
- only. SwiftData remains there for capture/review actions. |
|
| 89 | 87 |
- `HealthProbe/Views/Dashboard/DashboardView.swift` no longer imports SwiftData |
| 90 | 88 |
or reads `ModelContext`. Capture/review actions now route through the |
| 91 |
- Dashboard view model's legacy bridge while that bridge is retired. |
|
| 89 |
+ Dashboard view model while the legacy bridge is retired. |
|
| 90 |
+- `HealthProbe/ViewModels/DashboardViewModel.swift` no longer imports SwiftData |
|
| 91 |
+ or reads `ModelContext`/`FetchDescriptor`. Legacy snapshot-cache operations |
|
| 92 |
+ are isolated in `HealthKitService`. |
|
| 92 | 93 |
- `HealthProbe/Views/DataTypes/RecordChangeEvolutionChart.swift` now accepts a |
| 93 | 94 |
small `RecordChangeEvolutionSnapshot` DTO and loads archive/cache counts |
| 94 | 95 |
without importing SwiftData or querying `SnapshotDelta`. |
@@ -35,6 +35,100 @@ extension Array where Element == MonitoredType {
|
||
| 35 | 35 |
} |
| 36 | 36 |
} |
| 37 | 37 |
|
| 38 |
+enum LegacySwiftDataBridgeError: LocalizedError {
|
|
| 39 |
+ case notConfigured |
|
| 40 |
+ |
|
| 41 |
+ var errorDescription: String? {
|
|
| 42 |
+ switch self {
|
|
| 43 |
+ case .notConfigured: |
|
| 44 |
+ return "Legacy SwiftData bridge is not configured." |
|
| 45 |
+ } |
|
| 46 |
+ } |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+enum LegacySwiftDataBridge {
|
|
| 50 |
+ private static var container: ModelContainer? |
|
| 51 |
+ private static var context: ModelContext? |
|
| 52 |
+ |
|
| 53 |
+ static func configure(container: ModelContainer) {
|
|
| 54 |
+ self.container = container |
|
| 55 |
+ context = ModelContext(container) |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ static func createSnapshot( |
|
| 59 |
+ using healthKit: HealthKitService, |
|
| 60 |
+ selectedTypeIDs: Set<String>, |
|
| 61 |
+ adaptiveTimeoutsEnabled: Bool, |
|
| 62 |
+ triggerReason: String, |
|
| 63 |
+ retryOfSnapshotID: UUID?, |
|
| 64 |
+ timeoutMultiplier: Double, |
|
| 65 |
+ reviewAmbiguousCompleteDisappearedTypes: Bool, |
|
| 66 |
+ progress: SnapshotFetchProgress? |
|
| 67 |
+ ) async throws -> HealthSnapshot {
|
|
| 68 |
+ try await healthKit.createSnapshot( |
|
| 69 |
+ in: modelContext(), |
|
| 70 |
+ selectedTypeIDs: selectedTypeIDs, |
|
| 71 |
+ adaptiveTimeoutsEnabled: adaptiveTimeoutsEnabled, |
|
| 72 |
+ triggerReason: triggerReason, |
|
| 73 |
+ retryOfSnapshotID: retryOfSnapshotID, |
|
| 74 |
+ timeoutMultiplier: timeoutMultiplier, |
|
| 75 |
+ reviewAmbiguousCompleteDisappearedTypes: reviewAmbiguousCompleteDisappearedTypes, |
|
| 76 |
+ progress: progress |
|
| 77 |
+ ) |
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 80 |
+ static func savePartialSnapshot( |
|
| 81 |
+ _ snapshot: HealthSnapshot, |
|
| 82 |
+ using healthKit: HealthKitService |
|
| 83 |
+ ) async throws -> HealthSnapshot {
|
|
| 84 |
+ try await healthKit.savePartialSnapshot(snapshot, in: modelContext()) |
|
| 85 |
+ } |
|
| 86 |
+ |
|
| 87 |
+ static func saveReviewedCompleteSnapshot( |
|
| 88 |
+ _ snapshot: HealthSnapshot, |
|
| 89 |
+ using healthKit: HealthKitService |
|
| 90 |
+ ) async throws -> HealthSnapshot {
|
|
| 91 |
+ try await healthKit.saveReviewedCompleteSnapshot(snapshot, in: modelContext()) |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ static func snapshotExists(id: UUID) throws -> Bool {
|
|
| 95 |
+ var descriptor = FetchDescriptor<HealthSnapshot>( |
|
| 96 |
+ predicate: #Predicate<HealthSnapshot> { $0.id == id }
|
|
| 97 |
+ ) |
|
| 98 |
+ descriptor.fetchLimit = 1 |
|
| 99 |
+ return try !modelContext().fetch(descriptor).isEmpty |
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ static func previousSnapshot(for snapshot: HealthSnapshot) -> HealthSnapshot? {
|
|
| 103 |
+ guard let previousID = snapshot.previousSnapshotID else { return nil }
|
|
| 104 |
+ let descriptor = FetchDescriptor<HealthSnapshot>( |
|
| 105 |
+ predicate: #Predicate<HealthSnapshot> { $0.id == previousID }
|
|
| 106 |
+ ) |
|
| 107 |
+ return try? modelContext().fetch(descriptor).first |
|
| 108 |
+ } |
|
| 109 |
+ |
|
| 110 |
+ static func deleteSnapshot(id: UUID) throws {
|
|
| 111 |
+ let context = try modelContext() |
|
| 112 |
+ var descriptor = FetchDescriptor<HealthSnapshot>( |
|
| 113 |
+ predicate: #Predicate<HealthSnapshot> { $0.id == id }
|
|
| 114 |
+ ) |
|
| 115 |
+ descriptor.fetchLimit = 1 |
|
| 116 |
+ if let snapshot = try context.fetch(descriptor).first {
|
|
| 117 |
+ context.delete(snapshot) |
|
| 118 |
+ } |
|
| 119 |
+ } |
|
| 120 |
+ |
|
| 121 |
+ private static func modelContext() throws -> ModelContext {
|
|
| 122 |
+ if let context { return context }
|
|
| 123 |
+ guard let container else {
|
|
| 124 |
+ throw LegacySwiftDataBridgeError.notConfigured |
|
| 125 |
+ } |
|
| 126 |
+ let context = ModelContext(container) |
|
| 127 |
+ self.context = context |
|
| 128 |
+ return context |
|
| 129 |
+ } |
|
| 130 |
+} |
|
| 131 |
+ |
|
| 38 | 132 |
final class HealthKitService {
|
| 39 | 133 |
static let shared = HealthKitService() |
| 40 | 134 |
let store = HKHealthStore() |
@@ -1,36 +1,4 @@ |
||
| 1 | 1 |
import Foundation |
| 2 |
-import SwiftData |
|
| 3 |
- |
|
| 4 |
-enum LegacySwiftDataBridgeError: LocalizedError {
|
|
| 5 |
- case notConfigured |
|
| 6 |
- |
|
| 7 |
- var errorDescription: String? {
|
|
| 8 |
- switch self {
|
|
| 9 |
- case .notConfigured: |
|
| 10 |
- return "Legacy SwiftData bridge is not configured." |
|
| 11 |
- } |
|
| 12 |
- } |
|
| 13 |
-} |
|
| 14 |
- |
|
| 15 |
-enum LegacySwiftDataBridge {
|
|
| 16 |
- private static var container: ModelContainer? |
|
| 17 |
- private static var context: ModelContext? |
|
| 18 |
- |
|
| 19 |
- static func configure(container: ModelContainer) {
|
|
| 20 |
- self.container = container |
|
| 21 |
- context = ModelContext(container) |
|
| 22 |
- } |
|
| 23 |
- |
|
| 24 |
- static func modelContext() throws -> ModelContext {
|
|
| 25 |
- if let context { return context }
|
|
| 26 |
- guard let container else {
|
|
| 27 |
- throw LegacySwiftDataBridgeError.notConfigured |
|
| 28 |
- } |
|
| 29 |
- let context = ModelContext(container) |
|
| 30 |
- self.context = context |
|
| 31 |
- return context |
|
| 32 |
- } |
|
| 33 |
-} |
|
| 34 | 2 |
|
| 35 | 3 |
@Observable |
| 36 | 4 |
final class DashboardViewModel {
|
@@ -86,16 +54,6 @@ final class DashboardViewModel {
|
||
| 86 | 54 |
return |
| 87 | 55 |
} |
| 88 | 56 |
|
| 89 |
- let context: ModelContext |
|
| 90 |
- do {
|
|
| 91 |
- context = try LegacySwiftDataBridge.modelContext() |
|
| 92 |
- } catch {
|
|
| 93 |
- snapshotError = "Failed to access legacy snapshot cache: \(error.localizedDescription)" |
|
| 94 |
- snapshotProgress = .idle |
|
| 95 |
- showProgressSheet = true |
|
| 96 |
- return |
|
| 97 |
- } |
|
| 98 |
- |
|
| 99 | 57 |
isCreatingSnapshot = true |
| 100 | 58 |
snapshotError = nil |
| 101 | 59 |
snapshotProgress = .fetching |
@@ -135,8 +93,8 @@ final class DashboardViewModel {
|
||
| 135 | 93 |
let learnedMetricTimeout = Double(selectedTypeIDs.count) * HealthKitService.maximumTimeoutSeconds + 30 |
| 136 | 94 |
let operationTimeout = max(120, historyImportTimeout, learnedMetricTimeout) |
| 137 | 95 |
let snapshot = try await withTimeout(seconds: operationTimeout) {
|
| 138 |
- try await self.healthKit.createSnapshot( |
|
| 139 |
- in: context, |
|
| 96 |
+ try await LegacySwiftDataBridge.createSnapshot( |
|
| 97 |
+ using: self.healthKit, |
|
| 140 | 98 |
selectedTypeIDs: selectedTypeIDs, |
| 141 | 99 |
adaptiveTimeoutsEnabled: adaptiveTimeoutsEnabled, |
| 142 | 100 |
triggerReason: triggerReason, |
@@ -177,7 +135,7 @@ final class DashboardViewModel {
|
||
| 177 | 135 |
snapshotProgressMessage = "Incomplete snapshot" |
| 178 | 136 |
permissionsAlreadyRequested = healthKit.hasRequestedPermissionsBefore |
| 179 | 137 |
|
| 180 |
- if shouldAutoSaveKnownUnauthorizedPartial(snapshot: snapshot, failedCount: failedCount, context: context) {
|
|
| 138 |
+ if shouldAutoSaveKnownUnauthorizedPartial(snapshot: snapshot, failedCount: failedCount) {
|
|
| 181 | 139 |
snapshotProgressMessage = "Known unavailable metrics" |
| 182 | 140 |
snapshotProgressDetail = "Snapshot was auto-saved as partial because unavailable metrics were already confirmed in the previous snapshot." |
| 183 | 141 |
await savePartialSnapshot(keepSheetOpenForReview: true) |
@@ -217,7 +175,7 @@ final class DashboardViewModel {
|
||
| 217 | 175 |
return |
| 218 | 176 |
} |
| 219 | 177 |
|
| 220 |
- let ambiguousMetrics = findAmbiguousDisappearedMetrics(snapshot: snapshot, context: context) |
|
| 178 |
+ let ambiguousMetrics = findAmbiguousDisappearedMetrics(snapshot: snapshot) |
|
| 221 | 179 |
if !ambiguousMetrics.isEmpty {
|
| 222 | 180 |
pendingAmbiguousSnapshot = snapshot |
| 223 | 181 |
ambiguousDisappearedMetrics = ambiguousMetrics |
@@ -229,11 +187,7 @@ final class DashboardViewModel {
|
||
| 229 | 187 |
} |
| 230 | 188 |
|
| 231 | 189 |
let snapshotID = snapshot.id |
| 232 |
- var descriptor = FetchDescriptor<HealthSnapshot>( |
|
| 233 |
- predicate: #Predicate<HealthSnapshot> { $0.id == snapshotID }
|
|
| 234 |
- ) |
|
| 235 |
- descriptor.fetchLimit = 1 |
|
| 236 |
- let exists = try !context.fetch(descriptor).isEmpty |
|
| 190 |
+ let exists = try LegacySwiftDataBridge.snapshotExists(id: snapshotID) |
|
| 237 | 191 |
|
| 238 | 192 |
if !exists {
|
| 239 | 193 |
throw SnapshotCreationError.snapshotNotSaved |
@@ -272,8 +226,7 @@ final class DashboardViewModel {
|
||
| 272 | 226 |
snapshot.snapshotQuality = healthKit.deriveSnapshotQuality(from: typeCounts) |
| 273 | 227 |
|
| 274 | 228 |
do {
|
| 275 |
- let context = try LegacySwiftDataBridge.modelContext() |
|
| 276 |
- let saved = try await healthKit.savePartialSnapshot(snapshot, in: context) |
|
| 229 |
+ let saved = try await LegacySwiftDataBridge.savePartialSnapshot(snapshot, using: healthKit) |
|
| 277 | 230 |
finishSavedReviewedSnapshot(saved) |
| 278 | 231 |
} catch {
|
| 279 | 232 |
snapshotError = "Failed to save reviewed snapshot: \(error.localizedDescription)" |
@@ -285,8 +238,7 @@ final class DashboardViewModel {
|
||
| 285 | 238 |
guard let snapshot = pendingAmbiguousSnapshot else { return }
|
| 286 | 239 |
|
| 287 | 240 |
do {
|
| 288 |
- let context = try LegacySwiftDataBridge.modelContext() |
|
| 289 |
- let saved = try await healthKit.saveReviewedCompleteSnapshot(snapshot, in: context) |
|
| 241 |
+ let saved = try await LegacySwiftDataBridge.saveReviewedCompleteSnapshot(snapshot, using: healthKit) |
|
| 290 | 242 |
finishSavedReviewedSnapshot(saved) |
| 291 | 243 |
} catch {
|
| 292 | 244 |
snapshotError = "Failed to save reviewed snapshot: \(error.localizedDescription)" |
@@ -337,8 +289,7 @@ final class DashboardViewModel {
|
||
| 337 | 289 |
} |
| 338 | 290 |
|
| 339 | 291 |
do {
|
| 340 |
- let context = try LegacySwiftDataBridge.modelContext() |
|
| 341 |
- let saved = try await healthKit.savePartialSnapshot(snapshot, in: context) |
|
| 292 |
+ let saved = try await LegacySwiftDataBridge.savePartialSnapshot(snapshot, using: healthKit) |
|
| 342 | 293 |
completedSnapshotID = saved.id |
| 343 | 294 |
completedSnapshotTimestamp = saved.timestamp |
| 344 | 295 |
completedSnapshotDeviceID = saved.deviceID |
@@ -376,14 +327,7 @@ final class DashboardViewModel {
|
||
| 376 | 327 |
ambiguousDisappearedMetrics = [] |
| 377 | 328 |
if let snapshotID = completedSnapshotID {
|
| 378 | 329 |
do {
|
| 379 |
- let context = try LegacySwiftDataBridge.modelContext() |
|
| 380 |
- var descriptor = FetchDescriptor<HealthSnapshot>( |
|
| 381 |
- predicate: #Predicate<HealthSnapshot> { $0.id == snapshotID }
|
|
| 382 |
- ) |
|
| 383 |
- descriptor.fetchLimit = 1 |
|
| 384 |
- if let snapshot = try context.fetch(descriptor).first {
|
|
| 385 |
- context.delete(snapshot) |
|
| 386 |
- } |
|
| 330 |
+ try LegacySwiftDataBridge.deleteSnapshot(id: snapshotID) |
|
| 387 | 331 |
} catch { }
|
| 388 | 332 |
} |
| 389 | 333 |
completedSnapshotID = nil |
@@ -420,13 +364,8 @@ final class DashboardViewModel {
|
||
| 420 | 364 |
} |
| 421 | 365 |
} |
| 422 | 366 |
|
| 423 |
- private func findAmbiguousDisappearedMetrics(snapshot: HealthSnapshot, context: ModelContext) -> [AmbiguousDisappearedMetric] {
|
|
| 424 |
- guard let previousID = snapshot.previousSnapshotID else { return [] }
|
|
| 425 |
- let descriptor = FetchDescriptor<HealthSnapshot>( |
|
| 426 |
- predicate: #Predicate<HealthSnapshot> { $0.id == previousID }
|
|
| 427 |
- ) |
|
| 428 |
- guard let previous = try? context.fetch(descriptor).first else { return [] }
|
|
| 429 |
- |
|
| 367 |
+ private func findAmbiguousDisappearedMetrics(snapshot: HealthSnapshot) -> [AmbiguousDisappearedMetric] {
|
|
| 368 |
+ guard let previous = LegacySwiftDataBridge.previousSnapshot(for: snapshot) else { return [] }
|
|
| 430 | 369 |
let previousByType = Dictionary( |
| 431 | 370 |
uniqueKeysWithValues: (previous.typeCounts ?? []).map { ($0.typeIdentifier, $0) }
|
| 432 | 371 |
) |
@@ -454,19 +393,16 @@ final class DashboardViewModel {
|
||
| 454 | 393 |
} |
| 455 | 394 |
} |
| 456 | 395 |
|
| 457 |
- private func shouldAutoSaveKnownUnauthorizedPartial(snapshot: HealthSnapshot, failedCount: Int, context: ModelContext) -> Bool {
|
|
| 396 |
+ private func shouldAutoSaveKnownUnauthorizedPartial(snapshot: HealthSnapshot, failedCount: Int) -> Bool {
|
|
| 458 | 397 |
guard failedCount == 0, |
| 459 |
- let previousID = snapshot.previousSnapshotID else {
|
|
| 398 |
+ snapshot.previousSnapshotID != nil else {
|
|
| 460 | 399 |
return false |
| 461 | 400 |
} |
| 462 | 401 |
|
| 463 | 402 |
let currentUnauthorized = (snapshot.typeCounts ?? []).filter { $0.quality == .unauthorized }
|
| 464 | 403 |
guard !currentUnauthorized.isEmpty else { return false }
|
| 465 | 404 |
|
| 466 |
- let descriptor = FetchDescriptor<HealthSnapshot>( |
|
| 467 |
- predicate: #Predicate<HealthSnapshot> { $0.id == previousID }
|
|
| 468 |
- ) |
|
| 469 |
- guard let previous = try? context.fetch(descriptor).first else {
|
|
| 405 |
+ guard let previous = LegacySwiftDataBridge.previousSnapshot(for: snapshot) else {
|
|
| 470 | 406 |
return false |
| 471 | 407 |
} |
| 472 | 408 |
|