Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API/public #24

Open
wants to merge 11 commits into
base: HealthChart/main
Choose a base branch
from
13 changes: 12 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ let package = Package(
.iOS(.v17)
],
products: [
.library(name: "SpeziHealthKit", targets: ["SpeziHealthKit"])
.library(name: "SpeziHealthKit", targets: ["SpeziHealthKit", "SpeziHealthCharts"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.7.0")
Expand All @@ -43,6 +43,17 @@ let package = Package(
],
plugins: [] + swiftLintPlugin()
),
.target(
name: "SpeziHealthCharts",
dependencies: [
.product(name: "Spezi", package: "Spezi")
],
swiftSettings: [
swiftConcurrency,
.enableUpcomingFeature("InferSendableFromCaptures")
nriedman marked this conversation as resolved.
Show resolved Hide resolved
],
plugins: [] + swiftLintPlugin()
),
.testTarget(
name: "SpeziHealthKitTests",
dependencies: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// 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 SwiftUI
import SpeziViews

Check failure on line 10 in Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart+ViewModel.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package / Test using xcodebuild or run fastlane

no such module 'SpeziViews'

Check failure on line 10 in Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart+ViewModel.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests / Test using xcodebuild or run fastlane

no such module '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: MeasurementType
private let provider: any DataProvider

private var _range: DateRange
var range: DateRange {
get {
return _range
}
set {
self._range = newValue
self.refresh(using: self.provider, range: newValue)
}
}

private let measurementCache = MeasurementCache()

@ObservationIgnored
private var fetchTask: Task<Void, Never>?

private(set) var measurements: AsyncState<[DataPoint]> = .idle


init(
type: MeasurementType,
range: DateRange,
provider: any DataProvider
) {
self.type = type
self.provider = provider
self._range = range
}


/// Replaces `ViewModel.measurements` with new data points provided by `provider`.
///
/// Passes currently stored `ViewModel.dateRange` and `ViewModel.type` options to the `DataProvider.fetchData` method.
private func refresh(using provider: any DataProvider, range dateRange: DateRange) {
self.fetchTask?.cancel()

self.fetchTask = Task { @MainActor in
self.measurements = .processing

do {
// First, try to fetch measurements from the cache.
if let cachedData = try? await self.measurementCache.fetch(for: self.type, range: dateRange.interval) {
self.measurements = .success(cachedData)
return
}

// If nothing is found in the cache, query the data provider.
let newMeasurements = try await provider.fetchData(for: self.type, in: dateRange.interval)

// Only set the changes if the task wasn't cancelled.
guard !Task.isCancelled else {
return
}

self.measurements = .success(newMeasurements)
await self.measurementCache.store(newMeasurements, for: self.type, range: dateRange)

} catch {
self.measurements = .failure(
AnyLocalizedError(
error: error,
defaultErrorDescription: "Failed to fetch new measurements."
)
)
}
}
}
}
}
32 changes: 32 additions & 0 deletions Sources/SpeziHealthCharts/MetricChart/ChartViews/HealthChart.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// 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 SwiftUI


public struct HealthChart: View {
@State private var viewModel: ViewModel

@State var disabledInteractions: HealthChartInteractions = []
@State var chartStyle: HealthChartStyle = HealthChartStyle()


public var body: some View {
Text("here is the metric chart.")
}


public init(
_ type: MeasurementType,
in range: DateRange = .month(start: .now),
provider: any DataProvider = HealthKitDataProvider()
) {
self.viewModel = ViewModel(type: type, range: range, provider: provider)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// 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 SwiftUI


extension HealthChart {
public func disable(interactions: HealthChartInteractions) {

Check failure on line 13 in Sources/SpeziHealthCharts/MetricChart/ChartViews/Modifiers/HealthChart+DisableInteractions.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Missing Docs Violation: public declarations should be documented (missing_docs)
self.disabledInteractions = interactions
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// 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
//


extension HealthChart {
public func style(_ chartStyle: HealthChartStyle) {

Check failure on line 11 in Sources/SpeziHealthCharts/MetricChart/ChartViews/Modifiers/HealthChart+Style.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Missing Docs Violation: public declarations should be documented (missing_docs)
self.chartStyle = chartStyle
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// 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 Foundation


/// A class conforming to `DataProvider` fetches the data from a data store in the form of `DataPoint`s.
public protocol DataProvider: Sendable {
associatedtype QueryBuilder

func fetchData(for measurementType: MeasurementType, in interval: DateInterval) async throws -> [DataPoint]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// 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 Foundation
import HealthKit


// MARK: - HKQueryBuilder

struct EmptyQuery {
let id: String
}

public class HKQueryBuilder: QueryBuilder {
typealias QueryType = EmptyQuery


func build(from predicates: [QueryPredicate]) -> QueryType {
EmptyQuery(id: "Null")
}
}


// MARK: - HealthKitDataProvider


public final class HealthKitDataProvider: DataProvider {
public typealias QueryBuilder = HKQueryBuilder


public func fetchData(for measurementType: MeasurementType, in interval: DateInterval) async throws -> [DataPoint] {
[]
}


public init() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// 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 Foundation


/// Given an array of predicates, builds a query conforming of type `QueryType`.
protocol QueryBuilder {
associatedtype QueryType

func build(from predicates: [QueryPredicate]) -> QueryType
}
23 changes: 23 additions & 0 deletions Sources/SpeziHealthCharts/MetricChart/Models/DataPoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// 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 Foundation


/// A `DataPoint` contains all the necessary information to plot a single element in a Chart.
/// - Parameters:
/// - value: The value of the measured quantity, which determines the point's position on the y-axis.
/// - timestamp: The date corresponding to when the quantity was measured, which determines the point's position on the x-axis.
/// - type: The type of quantity that the measurement belongs to, which determines the point's membership in a `Series`.
public struct DataPoint: Identifiable, Hashable, Sendable {
public var id: Self { self }

public let value: Double
public let timestamp: Date
public let type: MeasurementType
}
23 changes: 23 additions & 0 deletions Sources/SpeziHealthCharts/MetricChart/Models/DateRange.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// 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 Foundation


public enum DateRange: Equatable {
case day(start: Date)
case week(start: Date)
case month(start: Date)
case sixMonths(start: Date)
case year(start: Date)


public var interval: DateInterval {
DateInterval(start: .now, duration: 60*60*24)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// 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
//


public struct HealthChartInteractions: OptionSet, Sendable {
nriedman marked this conversation as resolved.
Show resolved Hide resolved
public let rawValue: Int

Check failure on line 11 in Sources/SpeziHealthCharts/MetricChart/Models/HealthChartInteractions.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Type Contents Order Violation: An 'instance_property' should not be placed amongst the type content(s) 'type_property' (type_contents_order)


public init(rawValue: Int) {

Check failure on line 14 in Sources/SpeziHealthCharts/MetricChart/Models/HealthChartInteractions.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Type Contents Order Violation: An 'initializer' should not be placed amongst the type content(s) 'type_property' (type_contents_order)
self.rawValue = rawValue
}


static let swipe: HealthChartInteractions = HealthChartInteractions(rawValue: 1 << 0)

Check failure on line 19 in Sources/SpeziHealthCharts/MetricChart/Models/HealthChartInteractions.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Redundant Type Annotation Violation: Variables should not have redundant type annotation (redundant_type_annotation)
static let tap: HealthChartInteractions = HealthChartInteractions(rawValue: 1 << 1)

Check failure on line 20 in Sources/SpeziHealthCharts/MetricChart/Models/HealthChartInteractions.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Redundant Type Annotation Violation: Variables should not have redundant type annotation (redundant_type_annotation)

static let all: HealthChartInteractions = [.tap, .swipe]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// 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 Foundation


public struct HealthChartStyle {

Check failure on line 12 in Sources/SpeziHealthCharts/MetricChart/Models/HealthChartStyle.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Missing Docs Violation: public declarations should be documented (missing_docs)
public let frameSize: CGFloat = 200.0

Check failure on line 13 in Sources/SpeziHealthCharts/MetricChart/Models/HealthChartStyle.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Missing Docs Violation: public declarations should be documented (missing_docs)
}

Check failure on line 15 in Sources/SpeziHealthCharts/MetricChart/Models/HealthChartStyle.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Trailing Newline Violation: Files should have a single trailing newline (trailing_newline)
Loading
Loading