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

Update to Spezi 1.2.0 and Improve Background Sample Delivery #19

Merged
merged 8 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,8 @@ deployment_target: # Availability checks or attributes shouldn’t be using olde
excluded: # paths to ignore during linting. Takes precedence over `included`.
- .build
- .swiftpm
- .derivedData
- Tests/UITests/.derivedData

closure_body_length: # Closure bodies should not span too many lines.
- 35 # warning - default: 20
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let package = Package(
.library(name: "SpeziHealthKit", targets: ["SpeziHealthKit"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.0.0")
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.0")
],
targets: [
.target(
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ class ExampleAppDelegate: SpeziAppDelegate {
)
CollectSample(
HKQuantityType(.stepCount),
deliverySetting: .background(.afterAuthorizationAndApplicationWillLaunch)
deliverySetting: .background(.automatic)
)
CollectSample(
HKQuantityType(.pushCount),
deliverySetting: .anchorQuery(.manual)
)
CollectSample(
HKQuantityType(.activeEnergyBurned),
deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch)
deliverySetting: .anchorQuery(.automatic)
)
CollectSample(
HKQuantityType(.restingHeartRate),
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziHealthKit/CollectSample/CollectSample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import Spezi
/// CollectSample(
/// HKQuantityType(.stepCount),
/// predicate: predicateOneMonth,
/// deliverySetting: .background(.afterAuthorizationAndApplicationWillLaunch)
/// deliverySetting: .background(.automatic)
/// )
/// ```
public struct CollectSample: HealthKitDataSourceDescription {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,60 +80,78 @@
}

Task {
await triggerDataSourceCollection()
await triggerManualDataSourceCollection()
}
}

func willFinishLaunchingWithOptions() {
func startAutomaticDataCollection() {
guard askedForAuthorization(for: sampleType) else {
return
}

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

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

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

Check warning on line 120 in Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift#L120

Added line #L120 was not covered by tests
}

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

Check warning on line 126 in Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift#L124-L126

Added lines #L124 - L126 were not covered by tests
}

do {
try await self.anchoredSingleObjectQuery()
Logger.healthKit.debug("Successfully processed background update for \(self.sampleType)")
} catch {
Logger.healthKit.error("Could not query samples in a background update for \(self.sampleType): \(error)")

Check warning on line 133 in Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift#L133

Added line #L133 was not covered by tests
}

// Provide feedback to HealthKit that the data has been processed: https://developer.apple.com/documentation/healthkit/hkobserverquerycompletionhandler
completionHandler()
}
}
}
} catch {
Logger.healthKit.error("\(error.localizedDescription)")
Logger.healthKit.error("Could not Process HealthKit data collection: \(error.localizedDescription)")

Check warning on line 142 in Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift#L142

Added line #L142 was not covered by tests
}
}


private func anchoredSingleObjectQuery() {
Task {
let resultsAnchor = try await healthStore.anchoredSingleObjectQuery(
for: self.sampleType,
using: self.anchor,
withPredicate: predicate,
standard: self.standard
)
self.anchor = resultsAnchor
}
private func anchoredSingleObjectQuery() async throws {
let resultsAnchor = try await healthStore.anchoredSingleObjectQuery(
for: self.sampleType,
using: self.anchor,
withPredicate: predicate,
standard: self.standard
)
self.anchor = resultsAnchor
}

private func anchoredContinuousObjectQuery() async throws {
Expand All @@ -143,19 +161,21 @@

let updateQueue = anchorDescriptor.results(for: healthStore)

for try await results in updateQueue {
if Task.isCancelled {
return
}

for deletedObject in results.deletedObjects {
await standard.remove(sample: deletedObject)
}

for addedSample in results.addedSamples {
await standard.add(sample: addedSample)
Task {
for try await results in updateQueue {
if Task.isCancelled {
return

Check warning on line 167 in Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift#L167

Added line #L167 was not covered by tests
}

for deletedObject in results.deletedObjects {
await standard.remove(sample: deletedObject)

Check warning on line 171 in Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift#L171

Added line #L171 was not covered by tests
}

for addedSample in results.addedSamples {
await standard.add(sample: addedSample)
}
self.anchor = results.newAnchor
}
self.anchor = results.newAnchor
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import HealthKit
import OSLog
import Spezi


Expand All @@ -15,48 +16,36 @@
private static let activeObservationsLock = NSLock()


func startObservation(
func startBackgroundDelivery(
for sampleTypes: Set<HKSampleType>,
withPredicate predicate: NSPredicate? = nil
) -> AsyncThrowingStream<(Set<HKSampleType>, HKObserverQueryCompletionHandler), Error> {
AsyncThrowingStream { continuation in
Task {
do {
try await enableBackgroundDelivery(for: sampleTypes)
} catch {
continuation.finish(throwing: error)
}

var queryDescriptors: [HKQueryDescriptor] = []
for sampleType in sampleTypes {
queryDescriptors.append(
HKQueryDescriptor(sampleType: sampleType, predicate: predicate)
)
}

let observerQuery = HKObserverQuery(queryDescriptors: queryDescriptors) { _, samples, completionHandler, error in
guard error == nil,
let samples else {
continuation.finish(throwing: error)
completionHandler()
return
}

continuation.yield((samples, completionHandler))
}

self.execute(observerQuery)

continuation.onTermination = { @Sendable _ in
self.stop(observerQuery)
self.disableBackgroundDelivery(for: sampleTypes)
}
withPredicate predicate: NSPredicate? = nil,
observerQuery: @escaping (Result<(sampleTypes: Set<HKSampleType>, completionHandler: HKObserverQueryCompletionHandler), Error>) -> Void
) async throws {
var queryDescriptors: [HKQueryDescriptor] = []
for sampleType in sampleTypes {
queryDescriptors.append(
HKQueryDescriptor(sampleType: sampleType, predicate: predicate)
)
}

let observerQuery = HKObserverQuery(queryDescriptors: queryDescriptors) { query, samples, completionHandler, error in
guard error == nil,
let samples else {
Logger.healthKit.error("Failed HealthKit background delivery for observer query \(query) with error: \(error)")
observerQuery(.failure(error ?? NSError(domain: "Spezi HealthKit", code: -1)))
completionHandler()
return

Check warning on line 37 in Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift#L34-L37

Added lines #L34 - L37 were not covered by tests
}

observerQuery(.success((samples, completionHandler)))
}

self.execute(observerQuery)
try await enableBackgroundDelivery(for: sampleTypes)
}


func enableBackgroundDelivery(
private func enableBackgroundDelivery(
for objectTypes: Set<HKObjectType>,
frequency: HKUpdateFrequency = .immediate
) async throws {
Expand All @@ -72,13 +61,14 @@
}
}
} catch {
Logger.healthKit.error("Could not enable HealthKit Backgound access for \(objectTypes): \(error.localizedDescription)")

Check warning on line 64 in Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift#L64

Added line #L64 was not covered by tests
// Revert all changes as enable background delivery for the object types failed.
disableBackgroundDelivery(for: enabledObjectTypes)
}
}


func disableBackgroundDelivery(
private func disableBackgroundDelivery(
for objectTypes: Set<HKObjectType>
) {
for objectType in objectTypes {
Expand All @@ -87,7 +77,7 @@
let newActiveObservation = activeObservation - 1
if newActiveObservation <= 0 {
HKHealthStore.activeObservations[objectType] = nil
Task {
Task { @MainActor in

Check warning on line 80 in Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift#L80

Added line #L80 was not covered by tests
try await self.disableBackgroundDelivery(for: objectType)
}
} else {
Expand Down
25 changes: 11 additions & 14 deletions Sources/SpeziHealthKit/HealthKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@
/// )
/// CollectSample(
/// HKQuantityType(.stepCount),
/// deliverySetting: .background(.afterAuthorizationAndApplicationWillLaunch)
/// deliverySetting: .background(.automatic)
/// )
/// CollectSample(
/// HKQuantityType(.pushCount),
/// deliverySetting: .anchorQuery(.manual)
/// )
/// CollectSample(
/// HKQuantityType(.activeEnergyBurned),
/// deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch)
/// deliverySetting: .anchorQuery(.automatic)
/// )
/// CollectSample(
/// HKQuantityType(.restingHeartRate),
Expand All @@ -67,7 +67,7 @@
/// }
/// ```
@Observable
public final class HealthKit: Module, LifecycleHandler, EnvironmentAccessible, DefaultInitializable {
public final class HealthKit: Module, EnvironmentAccessible, DefaultInitializable {
@ObservationIgnored @StandardActor private var standard: any HealthKitConstraint
private let healthStore: HKHealthStore
private var healthKitDataSourceDescriptions: [HealthKitDataSourceDescription] = []
Expand Down Expand Up @@ -132,6 +132,12 @@
}


public func configure() {
for healthKitComponent in healthKitComponents {
healthKitComponent.startAutomaticDataCollection()
}
}

/// Displays the user interface to ask for authorization for all HealthKit data defined by the ``HealthKitDataSourceDescription``s.
///
/// Call this function when you want to start HealthKit data collection.
Expand All @@ -154,7 +160,7 @@
healthKitDataSourceDescriptions.append(healthKitDataSourceDescription)
let dataSources = healthKitDataSourceDescription.dataSources(healthStore: healthStore, standard: standard)
for dataSource in dataSources {
dataSource.willFinishLaunchingWithOptions()
dataSource.startAutomaticDataCollection()

Check warning on line 163 in Sources/SpeziHealthKit/HealthKit.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziHealthKit/HealthKit.swift#L163

Added line #L163 was not covered by tests
}
}

Expand All @@ -164,21 +170,12 @@
}
}


@_documentation(visibility: internal)
public func willFinishLaunchingWithOptions(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) {
for healthKitComponent in healthKitComponents {
healthKitComponent.willFinishLaunchingWithOptions()
}
}


/// Triggers any ``HealthKitDeliverySetting/manual(safeAnchor:)`` collections and starts the collection for all ``HealthKitDeliveryStartSetting/manual`` HealthKit data collections.
public func triggerDataSourceCollection() async {
await withTaskGroup(of: Void.self) { group in
for healthKitComponent in healthKitComponents {
group.addTask {
await healthKitComponent.triggerDataSourceCollection()
await healthKitComponent.triggerManualDataSourceCollection()
}
}
await group.waitForAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ public protocol HealthKitDataSource {
/// Called after the used was asked for authorization.
func askedForAuthorization()
/// Called to trigger the manual data collection.
func triggerDataSourceCollection() async
/// Called when the application finished launching with options.
func willFinishLaunchingWithOptions()
func triggerManualDataSourceCollection() async
/// Called to start the automatic data collection.
func startAutomaticDataCollection()
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ public enum HealthKitDeliverySetting: Equatable {
case manual(safeAnchor: Bool = true)
/// The HealthKit data is collected based on the `HealthKitDeliveryStartSetting` and constantly listens to updates while the application is running.
/// If `safeAnchor` is enabled the `HKQueryAnchor` is persisted across multiple application launches using the user defaults.
case anchorQuery(HealthKitDeliveryStartSetting = .afterAuthorizationAndApplicationWillLaunch, saveAnchor: Bool = true)
case anchorQuery(HealthKitDeliveryStartSetting = .automatic, saveAnchor: Bool = true)
/// The HealthKit data is collected based on the `HealthKitDeliveryStartSetting` and constantly listens to updates even if the application is not running.
/// If `safeAnchor` is enabled the `HKQueryAnchor` is persisted across multiple application launches using the user defaults.
case background(HealthKitDeliveryStartSetting = .afterAuthorizationAndApplicationWillLaunch, saveAnchor: Bool = true)
case background(HealthKitDeliveryStartSetting = .automatic, saveAnchor: Bool = true)


var saveAnchor: Bool {
Expand Down
Loading
Loading