Skip to content

Commit

Permalink
New Spezi Standard Implementation (#9)
Browse files Browse the repository at this point in the history
Co-authored-by: Paul Schmiedmayer <[email protected]>
  • Loading branch information
niallkehoe and PSchmiedmayer authored Aug 9, 2023
1 parent 0a26f41 commit 36a9d3b
Show file tree
Hide file tree
Showing 19 changed files with 354 additions and 237 deletions.
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", .upToNextMinor(from: "0.5.0"))
.package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.0"))
],
targets: [
.target(
Expand Down
22 changes: 0 additions & 22 deletions Sources/SpeziHealthKit/Adapter/HKSampleRemovalContext.swift

This file was deleted.

7 changes: 3 additions & 4 deletions Sources/SpeziHealthKit/CollectSample/CollectSample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@ public struct CollectSample: HealthKitDataSourceDescription {
}


public func dataSources<S: Standard>(
public func dataSources(
healthStore: HKHealthStore,
standard: S,
adapter: HealthKit<S>.HKSampleAdapter
standard: any HealthKitConstraint
) -> [any HealthKitDataSource] {
collectSamples.dataSources(healthStore: healthStore, standard: standard, adapter: adapter)
collectSamples.dataSources(healthStore: healthStore, standard: standard)
}
}
10 changes: 4 additions & 6 deletions Sources/SpeziHealthKit/CollectSample/CollectSamples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,17 @@ public struct CollectSamples: HealthKitDataSourceDescription {
}


public func dataSources<S: Standard>(
public func dataSources(
healthStore: HKHealthStore,
standard: S,
adapter: HealthKit<S>.HKSampleAdapter
standard: any HealthKitConstraint
) -> [any HealthKitDataSource] {
sampleTypes.map { sampleType in
HealthKitSampleDataSource<S>(
HealthKitSampleDataSource(
healthStore: healthStore,
standard: standard,
sampleType: sampleType,
predicate: predicate,
deliverySetting: deliverySetting,
adapter: adapter
deliverySetting: deliverySetting
)
}
}
Expand Down
118 changes: 53 additions & 65 deletions Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,18 @@
//

import HealthKit
import OSLog
import Spezi
import SwiftUI


final class HealthKitSampleDataSource<ComponentStandard: Standard>: HealthKitDataSource {
final class HealthKitSampleDataSource: HealthKitDataSource {
let healthStore: HKHealthStore
let standard: ComponentStandard
let standard: any HealthKitConstraint

let sampleType: HKSampleType
let predicate: NSPredicate?
let deliverySetting: HealthKitDeliverySetting
let adapter: HealthKit<ComponentStandard>.HKSampleAdapter

var active = false

private lazy var anchorUserDefaultsKey = UserDefaults.Keys.healthKitAnchorPrefix.appending(sampleType.identifier)
Expand All @@ -29,31 +28,32 @@ final class HealthKitSampleDataSource<ComponentStandard: Standard>: HealthKitDat
}
}


required init( // swiftlint:disable:this function_default_parameter_at_end
// We disable the SwiftLint as we order the parameters in a logical order and
// therefore don't put the predicate at the end here.
// swiftlint:disable function_default_parameter_at_end
required init(
healthStore: HKHealthStore,
standard: ComponentStandard,
standard: any HealthKitConstraint,
sampleType: HKSampleType,
predicate: NSPredicate? = nil, // We order the parameters in a logical order and therefore don't put the predicate at the end here.
deliverySetting: HealthKitDeliverySetting,
adapter: HealthKit<ComponentStandard>.HKSampleAdapter
predicate: NSPredicate? = nil,
deliverySetting: HealthKitDeliverySetting
) {
self.healthStore = healthStore
self.standard = standard
self.sampleType = sampleType
self.deliverySetting = deliverySetting
self.adapter = adapter

if predicate == nil {
self.predicate = HKQuery.predicateForSamples(
withStart: HealthKitSampleDataSource<ComponentStandard>.loadDefaultQueryDate(for: sampleType),
withStart: HealthKitSampleDataSource.loadDefaultQueryDate(for: sampleType),
end: nil,
options: .strictEndDate
)
} else {
self.predicate = predicate
}
}
// swiftlint:enable function_default_parameter_at_end


private static func loadDefaultQueryDate(for sampleType: HKSampleType) -> Date {
Expand Down Expand Up @@ -105,69 +105,57 @@ final class HealthKitSampleDataSource<ComponentStandard: Standard>: HealthKitDat
return
}

switch deliverySetting {
case .manual:
await standard.registerDataSource(adapter.transform(anchoredSingleObjectQuery()))
case .anchorQuery:
active = true
await standard.registerDataSource(adapter.transform(anchoredContinousObjectQuery()))
case .background:
active = true
let healthKitSamples = healthStore.startObservation(for: [sampleType], withPredicate: predicate)
.flatMap { _ in
do {
switch deliverySetting {
case .manual:
anchoredSingleObjectQuery()
case .anchorQuery:
active = true
try await anchoredContinousObjectQuery()
case .background:
active = true
for try await _ in healthStore.startObservation(for: [sampleType], withPredicate: predicate) {
self.anchoredSingleObjectQuery()
}
await standard.registerDataSource(adapter.transform(healthKitSamples))
}
} catch {
Logger.healthKit.error("\(error.localizedDescription)")
}
}


private func anchoredSingleObjectQuery() -> AsyncThrowingStream<DataChange<HKSample, HKSampleRemovalContext>, Error> {
AsyncThrowingStream { continuation in
Task {
let results = try await healthStore.anchoredSingleObjectQuery(
for: self.sampleType,
using: self.anchor,
withPredicate: predicate
)
self.anchor = results.anchor
for result in results.elements {
continuation.yield(result)
}
continuation.finish()
}
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 anchoredContinousObjectQuery() async -> any TypedAsyncSequence<DataChange<HKSample, HKSampleRemovalContext>> {
AsyncThrowingStream { continuation in
Task {
try await healthStore.requestAuthorization(toShare: [], read: [sampleType])

let anchorDescriptor = healthStore.anchorDescriptor(sampleType: sampleType, predicate: predicate, anchor: anchor)

let updateQueue = anchorDescriptor.results(for: healthStore)

do {
for try await results in updateQueue {
if Task.isCancelled {
continuation.finish()
return
}

for deletedObject in results.deletedObjects {
continuation.yield(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)))
}

for addedSample in results.addedSamples {
continuation.yield(.addition(addedSample))
}
self.anchor = results.newAnchor
}
} catch {
continuation.finish(throwing: error)
}
private func anchoredContinousObjectQuery() async throws {
try await healthStore.requestAuthorization(toShare: [], read: [sampleType])

let anchorDescriptor = healthStore.anchorDescriptor(sampleType: sampleType, predicate: predicate, anchor: anchor)

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)
}
self.anchor = results.newAnchor
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,32 @@ extension HKSample: Identifiable {
}
}


extension HKHealthStore {
// We disable the SwiftLint as we order the parameters in a logical order and
// therefore don't put the predicate at the end here.
// swiftlint:disable function_default_parameter_at_end
func anchoredSingleObjectQuery(
for sampleType: HKSampleType,
using anchor: HKQueryAnchor? = nil,
withPredicate predicate: NSPredicate? = nil
) async throws -> (elements: [DataChange<HKSample, HKSampleRemovalContext>], anchor: HKQueryAnchor) {
withPredicate predicate: NSPredicate? = nil,
standard: any HealthKitConstraint
) async throws -> (HKQueryAnchor) {
try await self.requestAuthorization(toShare: [], read: [sampleType])

let anchorDescriptor = anchorDescriptor(sampleType: sampleType, predicate: predicate, anchor: anchor)

let result = try await anchorDescriptor.result(for: self)

var elements: [DataChange<HKSample, HKSampleRemovalContext>] = []
elements.reserveCapacity(result.deletedObjects.count + result.addedSamples.count)


for deletedObject in result.deletedObjects {
elements.append(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)))
await standard.remove(sample: deletedObject)
}

for addedSample in result.addedSamples {
elements.append(.addition(addedSample))
await standard.add(sample: addedSample)
}
return (elements, result.newAnchor)

return (result.newAnchor)
}
// swiftlint:enable function_default_parameter_at_end


func anchorDescriptor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,19 @@ extension HKHealthStore {
return try await sampleQueryDescriptor.result(for: self)
}


// We disable the SwiftLint as we order the parameters in a logical order and
// therefore don't put the predicate at the end here.
// swiftlint:disable function_default_parameter_at_end
func sampleQueryStream(
for sampleType: HKSampleType,
withPredicate predicate: NSPredicate? = nil
) -> AsyncThrowingStream<DataChange<HKSample, HKSample.ID>, Error> {
AsyncThrowingStream { continuation in
Task {
for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) {
continuation.yield(.addition(sample))
}
continuation.finish()
withPredicate predicate: NSPredicate? = nil,
standard: any HealthKitConstraint
) {
_Concurrency.Task {
for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) {
await standard.add(sample: sample)
}
}
}
// swiftlint:enable function_default_parameter_at_end
}
Loading

0 comments on commit 36a9d3b

Please sign in to comment.