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) + } } }