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

Query Property Wrappers, HealthChart, and Other Refactoring #27

Merged
merged 96 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
9aaaa3a
[WIP] query property wrappers, concepts of a HealthChart
lukaskollmer Jan 12, 2025
d84d450
little cleanup
lukaskollmer Jan 12, 2025
af2254f
fix typo
lukaskollmer Jan 14, 2025
2fcf1b2
rework the HealthKit module itself a little bit
lukaskollmer Jan 14, 2025
335f426
small fixes, improve documentation
lukaskollmer Jan 14, 2025
9d6be14
centralise HealthKit auth handling, delay starting background observe…
lukaskollmer Jan 15, 2025
bea1710
make the config component configure function async, rework auth handl…
lukaskollmer Jan 15, 2025
4bb52de
RIP HealthKitTests (they were just checking the UserDefaults auth stu…
lukaskollmer Jan 15, 2025
cacfbe6
rename `__HKSampleTypeProviding` to `_HKSampleWithSampleType`
lukaskollmer Jan 16, 2025
fac771e
add HealthKit. isAuthorized, rename some parameters
lukaskollmer Jan 18, 2025
79e2e3d
merge SpeziHealthKitUI into SpeziHealthKit
lukaskollmer Jan 18, 2025
f8d16b4
enable "ExistentialAny" upcoming feature flag
lukaskollmer Jan 18, 2025
b642d79
rename HealthKitDataAccessRequirements → HealthKit.DataAccessRequirem…
lukaskollmer Jan 19, 2025
b47b1db
remove some things
lukaskollmer Jan 19, 2025
2cfdffe
remov
lukaskollmer Jan 19, 2025
80d3bbb
rename HealthKitSampleType → SampleType
lukaskollmer Jan 19, 2025
b246334
inline all the sample type getters
lukaskollmer Jan 19, 2025
edeed71
mark most of SampleType as @inlinable, localize sample type names and…
lukaskollmer Jan 19, 2025
ee4d321
try restore proper dependencies
lukaskollmer Jan 19, 2025
0ca7d74
add reference snapshots
lukaskollmer Jan 19, 2025
2b712de
add macOS support
lukaskollmer Jan 20, 2025
5f83cff
rework the "HealthKitDataSource" API
lukaskollmer Jan 20, 2025
2c0a599
split up the HealthChart into multiple files
lukaskollmer Jan 20, 2025
9e4e6b7
remove `frequency` parameter from `enableBackgroundDelivery` function
lukaskollmer Jan 20, 2025
6459e46
rework documentation (pt01)
lukaskollmer Jan 21, 2025
726c4ee
x
lukaskollmer Jan 21, 2025
3acef8a
remove InteractiveHealthChart
lukaskollmer Jan 21, 2025
3469baa
more documentation rework
lukaskollmer Jan 21, 2025
ab0e4ca
fix docc copyright notices
lukaskollmer Jan 21, 2025
8fab017
remove `withTimeRange` function
lukaskollmer Jan 21, 2025
e8ea0d0
HealthChart: remove errors overlay
lukaskollmer Jan 21, 2025
1aa6201
HealthChart: remove interactivity, swiftlint
lukaskollmer Jan 21, 2025
90ba660
rework HealthKitQueryTimeRange
lukaskollmer Jan 21, 2025
cb9fcf1
fix UITest dependencies
lukaskollmer Jan 21, 2025
d6aef67
adjust tests
lukaskollmer Jan 21, 2025
7dddd47
[Tests] force en_US locale
lukaskollmer Jan 21, 2025
264c1c7
HealthKitCharacteristicQuery : include usage example in docs
lukaskollmer Jan 21, 2025
2675017
bring DocC changes to readme
lukaskollmer Jan 21, 2025
e18f89c
vanity commit
lukaskollmer Jan 21, 2025
89c3407
UITests: enable swift 6 language mode
lukaskollmer Jan 22, 2025
d1341a5
[tests] always force use of en_US locale and time zone
lukaskollmer Jan 22, 2025
d9dabb8
rename `.anchorQuery` to `.continuous`
lukaskollmer Jan 22, 2025
3fc64ea
hmmm
lukaskollmer Jan 22, 2025
fb4df52
x
lukaskollmer Jan 22, 2025
494b62f
hmmm
lukaskollmer Jan 22, 2025
f5ad400
fix HealthKitQuery
lukaskollmer Jan 22, 2025
0621ef2
try to add some more tests
lukaskollmer Jan 22, 2025
1e6f32d
oof
lukaskollmer Jan 22, 2025
09eb23e
uwuuu
lukaskollmer Jan 22, 2025
282b12c
try to fix the test
lukaskollmer Jan 23, 2025
587b384
swiftlint
lukaskollmer Jan 23, 2025
b320564
my bad
lukaskollmer Jan 23, 2025
2a561b5
try to fix the test not being able to query app.staticTexts
lukaskollmer Jan 23, 2025
82357bd
license?
lukaskollmer Jan 23, 2025
268ae4c
license.
lukaskollmer Jan 23, 2025
5a8525e
include statistic query chart in UI tests; simplify SampleType and He…
lukaskollmer Jan 23, 2025
c322775
RequestReadAccess: add Info.plist notice
lukaskollmer Jan 23, 2025
81bc82f
de-generify the HealthChart
lukaskollmer Jan 23, 2025
9e318de
add `HealthKitQueryResults.isCurrentlyPerformingInitialFetch`
lukaskollmer Jan 23, 2025
8e0da6b
SampleType.heartRate: adjust expectedValuesRange
lukaskollmer Jan 23, 2025
81ded84
x
lukaskollmer Jan 23, 2025
a90c6bc
fix operator definition
lukaskollmer Jan 23, 2025
17a4b1d
test updates
lukaskollmer Jan 23, 2025
8796421
update XCTHealthKit dependency
lukaskollmer Jan 23, 2025
352ead0
Update SpeziHealthKitTests.swift
lukaskollmer Jan 23, 2025
093c8a4
rework CollectSample API
lukaskollmer Jan 24, 2025
3ca0c92
hmmm
lukaskollmer Jan 25, 2025
eb26de2
move HealthChart back into a separate UI target
lukaskollmer Jan 25, 2025
4b2e989
try to update docs
lukaskollmer Jan 25, 2025
14b6426
swiftlint
lukaskollmer Jan 25, 2025
b1a2cf6
remove stale localizations
lukaskollmer Jan 25, 2025
23ebe21
add well-known sample types
lukaskollmer Jan 25, 2025
f56fc66
codecov.yml licence
lukaskollmer Jan 25, 2025
8bf0d9b
add back the expectedValueRanges; fix tests
lukaskollmer Jan 25, 2025
3901f98
add missing licenses
lukaskollmer Jan 25, 2025
6e8e0bd
fix ui tests?
lukaskollmer Jan 25, 2025
b88bd4a
try to fix the test running
lukaskollmer Jan 25, 2025
3081291
try to fix ui tests
lukaskollmer Jan 25, 2025
bf3bef1
???
lukaskollmer Jan 25, 2025
3dcde39
codecov.yml
lukaskollmer Jan 25, 2025
3b82abb
codecov.yml
lukaskollmer Jan 25, 2025
60409a8
build-and-test.yml
lukaskollmer Jan 25, 2025
9fcd070
codecov.yml
lukaskollmer Jan 25, 2025
f5573cc
improve typing (use SampleType in more cases), improve tests
lukaskollmer Jan 25, 2025
b9d85f0
swiftlint
lukaskollmer Jan 25, 2025
541266c
add tests for HealthKitCharacteristic, rework it a little bit
lukaskollmer Jan 26, 2025
d82bfd7
try to fix ui tests
lukaskollmer Jan 26, 2025
390ecb6
try to fix ui tests
lukaskollmer Jan 26, 2025
6e75e9d
characteristic query tests
lukaskollmer Jan 27, 2025
463d289
try to fix ui tests
lukaskollmer Jan 27, 2025
e32a576
fix ui tests
lukaskollmer Jan 27, 2025
5c817ac
.spi.yml
lukaskollmer Jan 27, 2025
e9f513a
remove the remaining (now-unused) UserDefaults code
lukaskollmer Jan 27, 2025
cea84f0
update CollectSample docs to reflect new situation
lukaskollmer Jan 27, 2025
6a5d4d1
switch XCTHealthKit dependency
lukaskollmer Jan 27, 2025
f3d59b1
XCTHealthKit update
lukaskollmer Jan 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,28 @@ let package = Package(
.library(name: "SpeziHealthKit", targets: ["SpeziHealthKit"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.8.0")
.package(path: "../Spezi"),
.package(path: "../SpeziFoundation"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", 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")
],
swiftSettings: [.enableUpcomingFeature("ExistentialAny")],
lukaskollmer marked this conversation as resolved.
Show resolved Hide resolved
plugins: [] + swiftLintPlugin()
),
.testTarget(
name: "SpeziHealthKitTests",
dependencies: [
.product(name: "XCTSpezi", package: "Spezi"),
.target(name: "SpeziHealthKit")
.target(name: "SpeziHealthKit"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
],
swiftSettings: [.enableUpcomingFeature("ExistentialAny")],
plugins: [] + swiftLintPlugin()
)
]
Expand Down
55 changes: 37 additions & 18 deletions Sources/SpeziHealthKit/CollectSample/CollectSample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import HealthKit
import Spezi


/// Collects a specified `HKSampleType` in the ``HealthKit`` module.
/// Collects a specified ``HealthKitSampleType`` via the ``HealthKit`` module.
///
/// This structure define what and how the ``HealthKit`` samples are collected. By default, all samples of the provided `HKSampleType` will be collected. The collection starts on calling ``HealthKit/triggerDataSourceCollection()`` if you configure the `deliverySetting` as ``HealthKitDeliverySetting/manual(safeAnchor:)`` or automatic once the application is launched when you configure anything else than manual, i.e. ``HealthKitDeliverySetting/anchorQuery(_:saveAnchor:)`` or ``HealthKitDeliverySetting/background(_:saveAnchor:)``.
/// This structure define what and how the ``HealthKit`` samples are collected. By default, all samples of the provided ``HealthKitSampleType`` will be collected.
/// The collection starts on calling ``HealthKit/triggerDataSourceCollection()`` if you configure the `deliverySetting` as ``HealthKitDeliverySetting/manual(saveAnchor:)`` or automatic once the application is launched when you configure anything else than manual, i.e. ``HealthKitDeliverySetting/anchorQuery(_:saveAnchor:)`` or ``HealthKitDeliverySetting/background(_:saveAnchor:)``.
lukaskollmer marked this conversation as resolved.
Show resolved Hide resolved
///
/// Your can filter the HealthKit samples to collect by specifying the `predicate`. For example, you can define an `NSPredicate` to only collect the data collected at a time within the given start and end date. Below is an example to create a `NSPredicate` restricting the data collected in the previous month.
/// Your can filter the HealthKit samples to collect via an `NSPredicate`.
/// For example, you can define a predicate to only collect the data collected at a time within the given start and end date.
/// Below is an example to create a `NSPredicate` restricting the data collected in the previous month.
/// ```swift
/// private var predicateOneMonth: NSPredicate {
/// // Define the start and end time for the predicate. In this example,
Expand All @@ -38,39 +41,55 @@ import Spezi
///
/// ```swift
/// CollectSample(
/// HKQuantityType(.stepCount),
/// .stepCount,
/// predicate: predicateOneMonth,
/// deliverySetting: .background(.automatic)
/// )
/// ```
public struct CollectSample: HealthKitDataSourceDescription {
private let collectSamples: CollectSamples
public struct CollectSample: HealthKitConfigurationComponent {
private let sampleType: HKSampleType
private let predicate: NSPredicate?
private let deliverySetting: HealthKitDeliverySetting


public var sampleTypes: Set<HKSampleType> {
collectSamples.sampleTypes
public var dataAccessRequirements: HealthKitDataAccessRequirements {
lukaskollmer marked this conversation as resolved.
Show resolved Hide resolved
.init(read: [sampleType])
}


/// - Parameters:
/// - sampleType: The `HKSampleType` that should be collected
/// - sampleType: The ``HealthKitSampleType`` that should be collected
/// - predicate: A custom predicate that should be passed to the HealthKit query.
/// The default predicate collects all samples that have been collected from the first time that the user
/// provided the application authorization to collect the samples.
/// - deliverySetting: The ``HealthKitDeliverySetting`` that should be used to collect the sample type. `.manual` is the default argument used.
public init<S: HKSampleType>(
_ sampleType: S,
public init(
_ sampleType: HealthKitSampleType<some Any>,
predicate: NSPredicate? = nil,
delivery: HealthKitDeliverySetting = .manual() // TODO Question @Paul : why does it default to manual?
lukaskollmer marked this conversation as resolved.
Show resolved Hide resolved
) {
self.sampleType = sampleType.hkSampleType
self.predicate = predicate
self.deliverySetting = delivery
}

@available(*, deprecated, renamed: "init(_:predicate:delivery:)")
public init(
_ sampleType: HealthKitSampleType<some Any>,
predicate: NSPredicate? = nil,
deliverySetting: HealthKitDeliverySetting = .manual()
) {
self.collectSamples = CollectSamples([sampleType], predicate: predicate, deliverySetting: deliverySetting)
self.init(sampleType, predicate: predicate, delivery: deliverySetting)
}


public func dataSources(
healthStore: HKHealthStore,
standard: any HealthKitConstraint
) -> [any HealthKitDataSource] {
collectSamples.dataSources(healthStore: healthStore, standard: standard)
public func configure(for healthKit: HealthKit, on standard: any HealthKitConstraint) async {
let dataSource = HealthKitSampleDataSource(
healthKit: healthKit,
standard: standard,
sampleType: sampleType,
predicate: predicate,
deliverySetting: deliverySetting
)
await healthKit.addBackgroundHealthDataSource(dataSource)
}
}
53 changes: 0 additions & 53 deletions Sources/SpeziHealthKit/CollectSample/CollectSamples.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,22 @@ import SwiftUI

/// Requirement for every HealthKit Data Source.
public protocol HealthKitDataSource {
/// The data source's sample type
var sampleType: HKSampleType { get }

/// Whether the data source is currently active.
@MainActor
var isActive: Bool { get }

/// Called after the used was asked for authorization.
@MainActor
func askedForAuthorization() async
/// Called to trigger the manual data collection.
@MainActor
func triggerManualDataSourceCollection() async

/// Called to start the automatic data collection.
@MainActor
func startAutomaticDataCollection() async
}


extension HealthKitDataSource {
func askedForAuthorization(for sampleType: HKSampleType) -> Bool {
HealthKit.didAskForAuthorization(for: sampleType)
}
}


extension UserDefaults {
var alreadyRequestedSampleTypes: Set<String> {
get {
Set(stringArray(forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) ?? [])
}
set {
set(Array(newValue), forKey: UserDefaults.Keys.healthKitRequestedSampleTypes)
}
}

/// Called to trigger the manual data collection.
@MainActor
func triggerManualDataSourceCollection() async
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,33 @@ import SwiftUI


final class HealthKitSampleDataSource: HealthKitDataSource {
let healthStore: HKHealthStore
let standard: any HealthKitConstraint
// This needs to be unowned since the HealthKit module will establish a strong reference to the data source.
private unowned let healthKit: HealthKit
private let standard: any HealthKitConstraint

let sampleType: HKSampleType
let predicate: NSPredicate?
let deliverySetting: HealthKitDeliverySetting
@MainActor var active = false

private let predicate: NSPredicate?
private let deliverySetting: HealthKitDeliverySetting
@MainActor private(set) var isActive = false
@MainActor private lazy var anchorUserDefaultsKey = UserDefaults.Keys.healthKitAnchorPrefix.appending(sampleType.identifier)
@MainActor private lazy var anchor: HKQueryAnchor? = loadAnchor() {
didSet {
saveAnchor()
}
}

private var healthStore: HKHealthStore { healthKit.healthStore }


required init(
healthStore: HKHealthStore,
healthKit: HealthKit,
standard: any HealthKitConstraint,
sampleType: HKSampleType,
predicate: NSPredicate? = nil, // swiftlint:disable:this function_default_parameter_at_end
predicate: NSPredicate? = nil, // swiftlint:disable:this function_default_parameter_at_end
deliverySetting: HealthKitDeliverySetting
) {
self.healthStore = healthStore
self.healthKit = healthKit
self.standard = standard
self.sampleType = sampleType
self.deliverySetting = deliverySetting
Expand All @@ -61,62 +65,55 @@ final class HealthKitSampleDataSource: HealthKitDataSource {
components.setValue(0, for: .second)
components.setValue(0, for: .nanosecond)
let defaultQueryDate = components.date ?? .now

UserDefaults.standard.set(defaultQueryDate, forKey: defaultPredicateDateUserDefaultsKey)

return defaultQueryDate
}
return date
}


func askedForAuthorization() async {
guard askedForAuthorization(for: sampleType) && !deliverySetting.isManual && !active else {
guard await healthKit.askedForAuthorization(toRead: sampleType) && !deliverySetting.isManual && !isActive else {
return
}

await triggerManualDataSourceCollection()
}



func startAutomaticDataCollection() async {
guard askedForAuthorization(for: sampleType) else {
guard await healthKit.askedForAuthorization(toRead: sampleType) else {
return
}

switch deliverySetting {
case let .anchorQuery(startSetting, _) where startSetting == .automatic,
let .background(startSetting, _) where startSetting == .automatic:
case .anchorQuery(.automatic, _), .background(.automatic, _):
await triggerManualDataSourceCollection()
default:
break
}
}

func triggerManualDataSourceCollection() async {
guard !active else {
guard !isActive else {
return
}

do {
switch deliverySetting {
case .manual:
try await anchoredSingleObjectQuery()
case .anchorQuery:
active = true
isActive = true
try await anchoredContinuousObjectQuery()
case .background:
active = true
isActive = true
try await healthStore.startBackgroundDelivery(for: [sampleType]) { result in
guard case let .success((sampleTypes, completionHandler)) = result else {
return
}

guard sampleTypes.contains(self.sampleType) else {
Logger.healthKit.warning("Received Observation query types (\(sampleTypes)) are not corresponding to the CollectSample type \(self.sampleType)")
completionHandler()
return
}

do {
try await self.anchoredSingleObjectQuery()
Logger.healthKit.debug("Successfully processed background update for \(self.sampleType)")
Expand Down Expand Up @@ -145,14 +142,12 @@ final class HealthKitSampleDataSource: HealthKitDataSource {
self.anchor = resultsAnchor
}


@MainActor
private func anchoredContinuousObjectQuery() async throws {
try await healthStore.requestAuthorization(toShare: [], read: [sampleType])

print("ANCHOR", anchor)
lukaskollmer marked this conversation as resolved.
Show resolved Hide resolved
let anchorDescriptor = healthStore.anchorDescriptor(sampleType: sampleType, predicate: predicate, anchor: anchor)

let updateQueue = anchorDescriptor.results(for: healthStore)

Task {
for try await results in updateQueue {
for deletedObject in results.deletedObjects {
Expand All @@ -162,31 +157,32 @@ final class HealthKitSampleDataSource: HealthKitDataSource {
for addedSample in results.addedSamples {
await standard.add(sample: addedSample)
}
print("NEW ANCHOR", results.newAnchor)
lukaskollmer marked this conversation as resolved.
Show resolved Hide resolved
self.anchor = results.newAnchor
}
}
}


@MainActor
private func saveAnchor() {
if deliverySetting.saveAnchor {
guard let anchor,
let data = try? NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true) else {
return
}

UserDefaults.standard.set(data, forKey: anchorUserDefaultsKey)
}
}


@MainActor
private func loadAnchor() -> HKQueryAnchor? {
guard deliverySetting.saveAnchor,
let userDefaultsData = UserDefaults.standard.data(forKey: anchorUserDefaultsKey),
let loadedAnchor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: userDefaultsData) else {
return nil
}

return loadedAnchor
}
}
Loading
Loading