Skip to content

Commit

Permalink
Query Property Wrappers, HealthChart, and Other Refactoring (#27)
Browse files Browse the repository at this point in the history
# query property wrappers, HealthChart, other refactoring

## ♻️ Current situation & Problem
The HealthKit module currently only provides access to HealthKit data
via long-running anchor queries that deliver information about
new/deleted objects to the app's Standard.
It does not provide any facilities for querying for past samples, or
accessing health data from SwiftUI. This PR attempts to address these
issues.

Furthermore, the HealthKit module is lacking an API allowing spezi users
to integrate custom HealthKit permission requests into the module's
permission handling (i.e., you currently can only request HealthKit
access for some specific sample type by actively defining a long-running
observer for that sample type).

Furthermore, this PR attempts to implement a `HealthChart` view, which
can display various types of queried HealthKit data as a chart.

resolves #8 
requires StanfordSpezi/SpeziFoundation#19
requires StanfordBDHG/XCTestExtensions#28


## ⚙️ Release Notes 
- Added `HealthKitQuery` property wrapper
- Added `HealthKitStatisticsQuery` property wrapper
- Added `HealthKitCharacteristicQuery` property wrapper
- Added `HealthChart` view
- Extended `HealthKit` configuration API to allow users to specify
sample types the system should request read and/or write access to
- Removed `CollectSamples`. The same functionality can be achieved using
a `for` loop creating individual `CollectSample` instances.


## 📚 Documentation
All added new and changed existing APIs are documented. The DocC
structure was reworked and some key aspects of SpeziHealthKit now have
dedicated article/extension pages.


## ✅ Testing
The new and changed APIs are tested using both "normal" unit tests and
UI tests. Existing tests were kept and adapted where possible


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
lukaskollmer authored Jan 28, 2025
1 parent 79ee967 commit 90575b7
Show file tree
Hide file tree
Showing 90 changed files with 6,565 additions and 1,234 deletions.
10 changes: 6 additions & 4 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,24 @@ jobs:
name: Build and Test Swift Package
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
artifactname: SpeziHealthKit.xcresult
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziHealthKit
scheme: SpeziHealthKit-Package
artifactname: SpeziHealthKit-Package.xcresult
resultBundle: SpeziHealthKit-Package.xcresult
buildandtestuitests:
name: Build and Test UI Tests
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
artifactname: TestApp.xcresult
runsonlabels: '["macOS", "self-hosted"]'
path: 'Tests/UITests'
scheme: TestApp
artifactname: TestApp.xcresult
resultBundle: TestApp.xcresult
uploadcoveragereport:
name: Upload Coverage Report
needs: [buildandtest, buildandtestuitests]
uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2
with:
coveragereports: SpeziHealthKit.xcresult TestApp.xcresult
coveragereports: SpeziHealthKit-Package.xcresult TestApp.xcresult
secrets:
token: ${{ secrets.CODECOV_TOKEN }}
6 changes: 6 additions & 0 deletions .reuse/dep5
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/

Files: Tests/SpeziHealthKitTests/__Snapshots__/*
Copyright: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
License: MIT

1 change: 1 addition & 0 deletions .spi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ builder:
- platform: ios
documentation_targets:
- SpeziHealthKit
- SpeziHealthKitUI
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ SpeziHealthKit contributors

* [Paul Schmiedmayer](https://github.com/PSchmiedmayer)
* [Andreas Bauer](https://github.com/Supereg)
* [Lukas Kollmer](https://github.com/lukaskollmer)
30 changes: 25 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,48 @@ let package = Package(
name: "SpeziHealthKit",
defaultLocalization: "en",
platforms: [
.iOS(.v17)
.iOS(.v17),
.macOS(.v14)
],
products: [
.library(name: "SpeziHealthKit", targets: ["SpeziHealthKit"])
.library(name: "SpeziHealthKit", targets: ["SpeziHealthKit"]),
.library(name: "SpeziHealthKitUI", targets: ["SpeziHealthKitUI"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.8.0")
.package(url: "https://github.com/StanfordSpezi/Spezi.git", from: "1.8.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation.git", from: "2.1.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziStorage.git", from: "1.2.2"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.17.7")
] + swiftLintPackage(),
targets: [
.target(
name: "SpeziHealthKit",
dependencies: [
.product(name: "Spezi", package: "Spezi")
.product(name: "Spezi", package: "Spezi"),
.product(name: "SpeziFoundation", package: "SpeziFoundation"),
.product(name: "SpeziLocalStorage", package: "SpeziStorage")
],
swiftSettings: [.enableUpcomingFeature("ExistentialAny")],
plugins: [] + swiftLintPlugin()
),
.target(
name: "SpeziHealthKitUI",
dependencies: [
.target(name: "SpeziHealthKit"),
.product(name: "SpeziFoundation", package: "SpeziFoundation")
],
swiftSettings: [.enableUpcomingFeature("ExistentialAny")],
plugins: [] + swiftLintPlugin()
),
.testTarget(
name: "SpeziHealthKitTests",
dependencies: [
.product(name: "XCTSpezi", package: "Spezi"),
.target(name: "SpeziHealthKit")
.target(name: "SpeziHealthKit"),
.target(name: "SpeziHealthKitUI"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
],
swiftSettings: [.enableUpcomingFeature("ExistentialAny")],
plugins: [] + swiftLintPlugin()
)
]
Expand Down
76 changes: 46 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ SPDX-License-Identifier: MIT
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziHealthKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/StanfordSpezi/SpeziHealthKit)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziHealthKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/StanfordSpezi/SpeziHealthKit)

Simplifies access to HealthKit samples ranging from single, anchored, and background queries.
Access Health data in your Spezi app.

## Overview

The Spezi HealthKit module simplifies access to HealthKit samples ranging from single, anchored, and background queries.
The Spezi HealthKit module enables apps to integrate with Apple's HealthKit system, fetch data, set up long-lived background data collection, and visualize Health-related data.

### Setup

Expand All @@ -29,12 +29,12 @@ You need to add the Spezi HealthKit Swift package to
[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package).

> Important: If your application is not yet configured to use Spezi, follow the
[Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) and set up the core Spezi infrastructure.
[Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) and set up the core Spezi infrastructure.

### Example

Before you configure the ``HealthKit`` module, make sure your `Standard` in your Spezi Application conforms to the ``HealthKitConstraint`` protocol to receive HealthKit data.
The [`add(sample:)`](https://swiftpackageindex.com/stanfordspezi/spezihealthkit/documentation/spezihealthkit/healthkitconstraint/add(sample:)) function is triggered once for every newly collected HealthKit sample, and the [`remove(sample:)`](https://swiftpackageindex.com/stanfordspezi/spezihealthkit/documentation/spezihealthkit/healthkitconstraint/remove(sample:)) function
### Example: health data collection

Before you configure the ``HealthKit-class`` module, make sure your `Standard` in your Spezi Application conforms to the ``HealthKitConstraint`` protocol to receive HealthKit data. The ``HealthKitConstraint/add(sample:)`` function is triggered once for every newly collected HealthKit sample, and the ``HealthKitConstraint/remove(sample:)`` function is triggered once for every deleted HealthKit sample.
```swift
actor ExampleStandard: Standard, HealthKitConstraint {
// Add the newly collected HKSample to your application.
Expand All @@ -50,42 +50,58 @@ actor ExampleStandard: Standard, HealthKitConstraint {
```


Then, you can configure the ``HealthKit`` module in the configuration section of your `SpeziAppDelegate`.
Then, you can configure the ``HealthKit-class`` module in the configuration section of your `SpeziAppDelegate`.
Provide ``HealthKitDataSourceDescription`` to define the data collection.
You can, e.g., use ``CollectSample`` to collect a wide variety of `HKSampleTypes`:
```swift
class ExampleAppDelegate: SpeziAppDelegate {
override var configuration: Configuration {
Configuration(standard: ExampleStandard()) {
if HKHealthStore.isHealthDataAvailable() {
HealthKit {
CollectSample(
HKQuantityType.electrocardiogramType(),
deliverySetting: .background(.manual)
)
CollectSample(
HKQuantityType(.stepCount),
deliverySetting: .background(.automatic)
)
CollectSample(
HKQuantityType(.pushCount),
deliverySetting: .anchorQuery(.manual)
)
CollectSample(
HKQuantityType(.activeEnergyBurned),
deliverySetting: .anchorQuery(.automatic)
)
CollectSample(
HKQuantityType(.restingHeartRate),
deliverySetting: .manual()
)
}
HealthKit {
CollectSample(.activeEnergyBurned)
CollectSample(.stepCount, start: .manual)
CollectSample(.pushCount, start: .manual)
CollectSample(.heartRate, continueInBackground: true)
CollectSample(.electrocardiogram, start: .manual)
RequestReadAccess(quantity: [.bloodOxygen])
}
}
}
}
```


### Example: querying Health data in SwiftUI

You can use [`SpeziHealthKitUI`](https://swiftpackageindex.com/stanfordspezi/spezihealthkit/documentation/spezihealthkitui)'s [`HealthKitQuery`](https://swiftpackageindex.com/stanfordspezi/spezihealthkit/documentation/spezihealthkitui/healthkitquery) and [`HealthKitStatisticsQuery`](https://swiftpackageindex.com/stanfordspezi/spezihealthkit/documentation/spezihealthkitui/healthkitstatisticsquery) property wrappers to access the Health database in a View:
```swift
struct ExampleView: View {
@HealthKitQuery(.heartRate, timeRange: .today)
private var heartRateSamples

var body: some View {
ForEach(heartRateSamples) { sample in
// ...
}
}
}
```

Additionally, you can use [`SpeziHealthKitUI`](https://swiftpackageindex.com/stanfordspezi/spezihealthkit/documentation/spezihealthkitui)'s [`HealthChart`](https://swiftpackageindex.com/stanfordspezi/spezihealthkit/documentation/spezihealthkitui/healthchart) to visualise query results:
```swift
struct ExampleView: View {
@HealthKitQuery(.heartRate, timeRange: .today)
private var heartRateSamples

var body: some View {
HealthChart {
HealthChartEntry($heartRateSamples, drawingConfig: .init(mode: .line, color: .red))
}
}
}
```


For more information, please refer to the [API documentation](https://swiftpackageindex.com/StanfordSpezi/SpeziHealthKit/documentation).

## The Spezi Template Application
Expand Down
76 changes: 0 additions & 76 deletions Sources/SpeziHealthKit/CollectSample/CollectSample.swift

This file was deleted.

53 changes: 0 additions & 53 deletions Sources/SpeziHealthKit/CollectSample/CollectSamples.swift

This file was deleted.

Loading

0 comments on commit 90575b7

Please sign in to comment.