From 4f5ff3814d412c807dfac218443e091d95bcee9f Mon Sep 17 00:00:00 2001
From: nriedman <108841122+nriedman@users.noreply.github.com>
Date: Tue, 5 Nov 2024 17:05:01 -0800
Subject: [PATCH] Finish `HealthChart` as public-facing api

---
 .../ChartViews/HealthChart+ViewModel.swift    | 72 ------------------
 .../MetricChart/ChartViews/HealthChart.swift  | 37 +++++-----
 .../ChartViews/InternalHealthChart.swift      | 73 +++++++++++++++++++
 .../MetricChart/Models/ChartRange.swift       | 10 +--
 .../Models/HealthChartInteractions.swift      |  6 +-
 .../MetricChart/Models/HealthChartStyle.swift |  7 +-
 .../UITests/TestApp/HealthKitTestsView.swift  | 25 +++++++
 7 files changed, 132 insertions(+), 98 deletions(-)
 delete mode 100644 Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart+ViewModel.swift
 create mode 100644 Sources/SpeziHealthCharts/MetricChart/ChartViews/InternalHealthChart.swift

diff --git a/Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart+ViewModel.swift b/Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart+ViewModel.swift
deleted file mode 100644
index 53a7a54..0000000
--- a/Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart+ViewModel.swift
+++ /dev/null
@@ -1,72 +0,0 @@
-//
-// This source file is part of the Stanford Spezi open-source project
-//
-// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
-//
-// SPDX-License-Identifier: MIT
-//
-
-import HealthKit
-import SwiftUI
-import SpeziViews
-
-
-enum AsyncState<T: Sendable>: Sendable {
-    case idle
-    case processing
-    case success(T)
-    case failure(LocalizedError)
-}
-
-
-extension HealthChart {
-    @Observable
-    @MainActor
-    class ViewModel {
-        private let type: HKQuantityType
-        private let provider: any DataProvider
-        
-        private var _range: ChartRange
-        private var _rangeBinding: Binding<ChartRange>?
-        
-        var range: ChartRange {
-            get {
-                return _rangeBinding?.wrappedValue ?? _range
-            }
-            set {
-                self._range = newValue
-                self.refreshMeasurements()
-            }
-        }
-        
-        private(set) var measurements: [HKQuantitySample] = []
-        
-        
-        /// Queries the stored `DataProvider` to store all the data points that lie in the given `ChartRange` in the `.measurements` property.
-        ///
-        /// Internally, the `DataProvider` should only query the data store once (for all the measurements of that type), then cache the results. This
-        /// call will then return the measurements in the cached array that lie in the new date range.
-        private func refreshMeasurements() {
-            
-        }
-        
-        
-        init(
-            type: HKQuantityType,
-            range: ChartRange,
-            provider: any DataProvider
-        ) {
-            self.type = type
-            self.provider = provider
-            self._range = range
-        }
-        
-        init(
-            type: HKQuantityType,
-            range: Binding<ChartRange>,
-            provider: any DataProvider
-        ) {
-            
-        }
-    }
-}
diff --git a/Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart.swift b/Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart.swift
index d324945..253615a 100644
--- a/Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart.swift
+++ b/Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart.swift
@@ -6,31 +6,34 @@
 // SPDX-License-Identifier: MIT
 //
 
-
 import SwiftUI
 import HealthKit
 
-// TODO: Next steps:
-// Verify data flow / implement data input infrastructure.
-// Mock text in `HealthChart` just shows the current values of all inputs.
-// See how they change with picker, modifiers, etc.
 
 public struct HealthChart: View {
-    @State private var range: ChartRange
-    @State private var rangeBinding: Binding<ChartRange>?
-    @State private var measurements: [HKQuantitySample] = []
+    @State private var privateRange: ChartRange
+    private var privateRangeBinding: Binding<ChartRange>?
+    
+    var range: Binding<ChartRange> {
+        Binding(
+            get: {
+                privateRangeBinding?.wrappedValue ?? privateRange
+            }, set: { newValue in
+                if let privateRangeBinding {
+                    privateRangeBinding.wrappedValue = newValue
+                } else {
+                    privateRange = newValue
+                }
+            }
+        )
+    }
     
     private let quantityType: HKQuantityType
     private let dataProvider: any DataProvider
     
     
     public var body: some View {
-        Text("here is the metric chart.")
-            .onChange(of: range) { _, newRange in
-                Task { @MainActor in
-                    measurements = try await dataProvider.fetchData(for: quantityType, in: newRange)
-                }
-            }
+        InternalHealthChart(quantityType, range: range, provider: dataProvider)
     }
     
     
@@ -40,7 +43,7 @@ public struct HealthChart: View {
         provider: any DataProvider = HealthKitDataProvider()
     ) {
         self.quantityType = type
-        self.range = initialRange
+        self.privateRange = initialRange
         self.dataProvider = provider
     }
     
@@ -49,8 +52,8 @@ public struct HealthChart: View {
         range: Binding<ChartRange>,
         provider: any DataProvider = HealthKitDataProvider()
     ) {
-        self.range = range.wrappedValue
-        self.rangeBinding = range
+        self.privateRange = range.wrappedValue
+        self.privateRangeBinding = range
         self.quantityType = type
         self.dataProvider = provider
     }
diff --git a/Sources/SpeziHealthCharts/MetricChart/ChartViews/InternalHealthChart.swift b/Sources/SpeziHealthCharts/MetricChart/ChartViews/InternalHealthChart.swift
new file mode 100644
index 0000000..d171622
--- /dev/null
+++ b/Sources/SpeziHealthCharts/MetricChart/ChartViews/InternalHealthChart.swift
@@ -0,0 +1,73 @@
+//
+// This source file is part of the Stanford Spezi open-source project
+//
+// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
+//
+// SPDX-License-Identifier: MIT
+//
+
+import HealthKit
+import SwiftUI
+
+
+struct InternalHealthChart: View {
+    @Binding private var range: ChartRange
+    @State private var measurements: [Int] = [1]
+    
+    
+    @Environment(\.disabledChartInteractions) private var disabledInteractions
+    @Environment(\.healthChartStyle) private var chartStyle
+    
+    
+    private let quantityType: HKQuantityType
+    private let dataProvider: any DataProvider
+    
+    
+    var body: some View {
+        List {
+            HStack {
+                Text("Quantity Type:")
+                    .bold()
+                Spacer(minLength: 5)
+                Text(quantityType.identifier)
+            }
+            HStack {
+                Text("Chart Range (Binding):")
+                    .bold()
+                Spacer(minLength: 5)
+                Text(range.interval.description)
+            }
+            HStack {
+                Text("Chart Style (Modifier):")
+                    .bold()
+                Spacer(minLength: 5)
+                Text("\(chartStyle.frameSize)")
+            }
+            HStack {
+                Text("Disabled Interactions (Modifier):")
+                    .bold()
+                Spacer(minLength: 5)
+                Text(String(disabledInteractions.rawValue, radix: 2))
+            }
+            Section("Measurements") {
+                ForEach(measurements, id: \.self) { measurement in
+                    Text("\(measurement)")
+                }
+            }
+        }
+            .onChange(of: range) { _, _ in
+                measurements.append(measurements.reduce(0, +))
+            }
+    }
+    
+    
+    init(
+        _ type: HKQuantityType,
+        range: Binding<ChartRange>,
+        provider: any DataProvider = HealthKitDataProvider()
+    ) {
+        self._range = range
+        self.quantityType = type
+        self.dataProvider = provider
+    }
+}
diff --git a/Sources/SpeziHealthCharts/MetricChart/Models/ChartRange.swift b/Sources/SpeziHealthCharts/MetricChart/Models/ChartRange.swift
index b9649a2..3667ee8 100644
--- a/Sources/SpeziHealthCharts/MetricChart/Models/ChartRange.swift
+++ b/Sources/SpeziHealthCharts/MetricChart/Models/ChartRange.swift
@@ -10,13 +10,13 @@ import Foundation
 
 
 /// A `ChartRange` is the date domain of the x-axis of a `HealthChart`.
-public struct ChartRange: Sendable, Equatable {
-    var start: Date
-    var end: Date
-    var granularity: Calendar.Component
+public struct ChartRange: Sendable, Equatable, Hashable {
+    public var start: Date
+    public var end: Date
+    public var granularity: Calendar.Component
     
     
-    var interval: DateInterval {
+    public var interval: DateInterval {
         DateInterval(start: start, end: end)
     }
     
diff --git a/Sources/SpeziHealthCharts/MetricChart/Models/HealthChartInteractions.swift b/Sources/SpeziHealthCharts/MetricChart/Models/HealthChartInteractions.swift
index 048e130..462d1b7 100644
--- a/Sources/SpeziHealthCharts/MetricChart/Models/HealthChartInteractions.swift
+++ b/Sources/SpeziHealthCharts/MetricChart/Models/HealthChartInteractions.swift
@@ -16,8 +16,8 @@ public struct HealthChartInteractions: OptionSet, Sendable {
     }
     
     
-    static let swipe: HealthChartInteractions = HealthChartInteractions(rawValue: 1 << 0)
-    static let tap: HealthChartInteractions = HealthChartInteractions(rawValue: 1 << 1)
+    public static let swipe: HealthChartInteractions = HealthChartInteractions(rawValue: 1 << 0)
+    public static let tap: HealthChartInteractions = HealthChartInteractions(rawValue: 1 << 1)
     
-    static let all: HealthChartInteractions = [.tap, .swipe]
+    public static let all: HealthChartInteractions = [.tap, .swipe]
 }
diff --git a/Sources/SpeziHealthCharts/MetricChart/Models/HealthChartStyle.swift b/Sources/SpeziHealthCharts/MetricChart/Models/HealthChartStyle.swift
index cf80d47..248f1e7 100644
--- a/Sources/SpeziHealthCharts/MetricChart/Models/HealthChartStyle.swift
+++ b/Sources/SpeziHealthCharts/MetricChart/Models/HealthChartStyle.swift
@@ -10,7 +10,12 @@ import Foundation
 
 
 public struct HealthChartStyle: Sendable {
-    public let frameSize: CGFloat = 200.0
+    let frameSize: CGFloat
+    
+    
+    public init(idealHeight: CGFloat = 200.0) {
+        frameSize = idealHeight
+    }
     
     
     public static let `default` = HealthChartStyle()
diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift
index ee54a2f..abb2cc0 100644
--- a/Tests/UITests/TestApp/HealthKitTestsView.swift
+++ b/Tests/UITests/TestApp/HealthKitTestsView.swift
@@ -16,6 +16,11 @@ struct HealthKitTestsView: View {
     @Environment(HealthKit.self) var healthKitModule
     @Environment(HealthKitStore.self) var healthKitStore
 
+    @State private var showHealthChartBinding = false
+    @State private var showHealthChart = false
+    
+    @State private var chartRange: ChartRange = .month
+    
     
     var body: some View {
         List {
@@ -40,7 +45,27 @@ struct HealthKitTestsView: View {
                     }
                 }
             }
+            Button("Show HealthChart with binding") { showHealthChartBinding.toggle() }
+            Button("Show HealthChart without binding") { showHealthChart.toggle() }
         }
+            .sheet(isPresented: $showHealthChartBinding) {
+                VStack {
+                    Picker("Chart Range", selection: $chartRange) {
+                        Text("Daily").tag(ChartRange.day)
+                        Text("Weekly").tag(ChartRange.week)
+                        Text("Monthly").tag(ChartRange.month)
+                        Text("Six Months").tag(ChartRange.sixMonths)
+                        Text("Yearly").tag(ChartRange.year)
+                    }
+                    Text("Parent's Range: \(chartRange.interval.description)")
+                    HealthChart(HKQuantityType(.bodyMass), range: $chartRange)
+                        .style(HealthChartStyle(idealHeight: 150))
+                }
+            }
+            .sheet(isPresented: $showHealthChart) {
+                HealthChart(HKQuantityType(.bodyMass))
+                    .disable(interactions: .swipe)
+            }
     }
 }