1 contributor
import SwiftUI
import SwiftData
import HealthKit
import UIKit
struct DashboardView: View {
@Environment(\.modelContext) private var modelContext
@Environment(AppSettings.self) private var appSettings
@Query(sort: \HealthSnapshot.timestamp, order: .reverse) private var snapshots: [HealthSnapshot]
@State private var viewModel = DashboardViewModel()
init() {
let id = UIDevice.current.identifierForVendor?.uuidString ?? ""
_snapshots = Query(
filter: #Predicate<HealthSnapshot> { $0.deviceID == id },
sort: \HealthSnapshot.timestamp,
order: .reverse
)
}
private var latest: HealthSnapshot? { snapshots.first }
private var previous: HealthSnapshot? { snapshots.dropFirst().first }
var body: some View {
NavigationStack {
List {
statusSection
anomalySummarySection
actionsSection
if let msg = viewModel.authError ?? viewModel.snapshotError {
Section {
Label(msg, systemImage: "exclamationmark.circle")
.foregroundStyle(Color.criticalRed)
.font(.caption)
}
}
}
.navigationTitle("HealthProbe")
}
.sheet(isPresented: $viewModel.showProgressSheet) {
SnapshotProgressSheet(viewModel: viewModel)
}
}
// MARK: - Sections
private var statusSection: some View {
Section("Status") {
if let latest {
InfoRow(label: "Last Snapshot") {
Text(latest.timestamp, style: .relative)
.foregroundStyle(.secondary)
}
InfoRow(label: "Device") {
Text(latest.deviceName)
.foregroundStyle(.secondary)
}
if latest.snapshotQuality != SnapshotQuality.complete {
Label("Incomplete snapshot", systemImage: "exclamationmark.triangle")
.font(.caption)
.foregroundStyle(Color.warningAmber)
}
} else {
Label("No snapshots yet", systemImage: "camera.viewfinder")
.foregroundStyle(.secondary)
}
InfoRow(label: "Monitored Types") {
Text("\(appSettings.selectedTypeIDs.count)")
.foregroundStyle(.secondary)
}
if let latest, let previous {
let delta = viewModel.totalChanges(latest: latest, previous: previous)
InfoRow(label: "Changes vs Previous") {
Text(delta == 0 ? "None" : "\(delta) records")
.foregroundStyle(delta == 0 ? Color.healthyGreen : Color.warningAmber)
}
}
}
}
@ViewBuilder
private var anomalySummarySection: some View {
AnomalySummarySection()
}
private var actionsSection: some View {
Section("Actions") {
Button {
Task { await viewModel.requestAuthorization() }
} label: {
HStack {
Label("Request Health Access", systemImage: "heart.text.square")
Spacer()
if viewModel.isRequestingAuth { ProgressView() }
}
}
.disabled(viewModel.isRequestingAuth)
.accessibilityLabel("Request HealthKit read authorization")
Button {
Task {
await viewModel.createSnapshot(
context: modelContext,
selectedTypeIDs: appSettings.selectedTypeIDs
)
}
} label: {
HStack {
Label("Create Snapshot", systemImage: "camera.viewfinder")
Spacer()
if viewModel.isCreatingSnapshot { ProgressView() }
}
}
.disabled(viewModel.isCreatingSnapshot)
.accessibilityLabel("Create a new data snapshot")
}
}
}
// Avoids LabeledContent's TableRowContent ambiguity in List/Section contexts.
private struct InfoRow<Content: View>: View {
let label: String
@ViewBuilder let content: () -> Content
var body: some View {
HStack {
Text(label)
Spacer()
content()
}
}
}
private struct AnomalySummarySection: View {
@Query(filter: #Predicate<AnomalyRecord> { !$0.isResolved })
private var unresolved: [AnomalyRecord]
private var criticalCount: Int { unresolved.filter { $0.severityRaw == Severity.critical.rawValue }.count }
private var warningCount: Int { unresolved.filter { $0.severityRaw == Severity.warning.rawValue }.count }
var body: some View {
if !unresolved.isEmpty {
Section("Anomalies") {
if criticalCount > 0 {
Label("\(criticalCount) critical \(criticalCount == 1 ? "anomaly" : "anomalies")",
systemImage: "exclamationmark.circle.fill")
.foregroundStyle(Color.criticalRed)
}
if warningCount > 0 {
Label("\(warningCount) \(warningCount == 1 ? "warning" : "warnings")",
systemImage: "exclamationmark.triangle.fill")
.foregroundStyle(Color.warningAmber)
}
}
}
}
}
#Preview {
DashboardView()
.modelContainer(for: [HealthSnapshot.self, TypeCount.self, YearlyCount.self], inMemory: true)
.environment(AppSettings())
}