From abcc4478ce46bdc23ec99cf53d804318a52c1298 Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Mon, 24 Jul 2023 11:52:21 -0700 Subject: [PATCH 01/16] newStandard without dataChange fixes --- Package.swift | 2 +- .../CollectSample/CollectSample.swift | 8 +- .../CollectSample/CollectSamples.swift | 12 +- .../HealthKitSampleDataSource.swift | 114 ++++++++++++------ .../HKHealthStore+AnchoredObjectQuery.swift | 58 ++++++--- .../HKHealthStore+SampleQuery.swift | 41 +++++-- Sources/SpeziHealthKit/HealthKit.swift | 22 ++-- .../SpeziHealthKit/HealthKitConstraint.swift | 16 +++ .../HealthKitDataSourceDescription.swift | 4 +- .../Shared/MockAdapterActor.swift | 46 +++---- .../SpeziHealthKitTests.swift | 8 +- .../UITests/TestApp/HealthKitTestsView.swift | 17 +-- Tests/UITests/TestApp/TestApp.swift | 1 + Tests/UITests/TestApp/TestAppDelegate.swift | 27 ++++- .../TestApp/TestAppHealthKitAdapter.swift | 42 +++---- .../UITests/UITests.xcodeproj/project.pbxproj | 26 ++-- 16 files changed, 296 insertions(+), 148 deletions(-) create mode 100644 Sources/SpeziHealthKit/HealthKitConstraint.swift diff --git a/Package.swift b/Package.swift index fd18de8..fef817d 100644 --- a/Package.swift +++ b/Package.swift @@ -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( diff --git a/Sources/SpeziHealthKit/CollectSample/CollectSample.swift b/Sources/SpeziHealthKit/CollectSample/CollectSample.swift index 8dccf96..6043d1c 100644 --- a/Sources/SpeziHealthKit/CollectSample/CollectSample.swift +++ b/Sources/SpeziHealthKit/CollectSample/CollectSample.swift @@ -35,11 +35,11 @@ public struct CollectSample: HealthKitDataSourceDescription { } - public func dataSources( + public func dataSources( healthStore: HKHealthStore, - standard: S, - adapter: HealthKit.HKSampleAdapter + standard: any HealthKitConstraint //, +// adapter: HealthKit.HKSampleAdapter ) -> [any HealthKitDataSource] { - collectSamples.dataSources(healthStore: healthStore, standard: standard, adapter: adapter) + collectSamples.dataSources(healthStore: healthStore, standard: standard) //, standard: standard, adapter: adapter) } } diff --git a/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift b/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift index 1f679b6..b98c300 100644 --- a/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift +++ b/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift @@ -34,19 +34,19 @@ public struct CollectSamples: HealthKitDataSourceDescription { } - public func dataSources( + public func dataSources( healthStore: HKHealthStore, - standard: S, - adapter: HealthKit.HKSampleAdapter + standard: any HealthKitConstraint //, +// adapter: HealthKit.HKSampleAdapter ) -> [any HealthKitDataSource] { sampleTypes.map { sampleType in - HealthKitSampleDataSource( + HealthKitSampleDataSource( healthStore: healthStore, standard: standard, sampleType: sampleType, predicate: predicate, - deliverySetting: deliverySetting, - adapter: adapter + deliverySetting: deliverySetting //, +// adapter: adapter ) } } diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index fbe7120..d9e2dc1 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -11,14 +11,14 @@ import Spezi import SwiftUI -final class HealthKitSampleDataSource: HealthKitDataSource { +final class HealthKitSampleDataSource: HealthKitDataSource { let healthStore: HKHealthStore - let standard: ComponentStandard + let standard: any HealthKitConstraint //ComponentStandard let sampleType: HKSampleType let predicate: NSPredicate? let deliverySetting: HealthKitDeliverySetting - let adapter: HealthKit.HKSampleAdapter +// let adapter: HealthKit.HKSampleAdapter var active = false @@ -32,21 +32,21 @@ final class HealthKitSampleDataSource: HealthKitDat required init( // swiftlint:disable:this function_default_parameter_at_end 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.HKSampleAdapter + deliverySetting: HealthKitDeliverySetting //, +// adapter: HealthKit.HKSampleAdapter ) { self.healthStore = healthStore self.standard = standard self.sampleType = sampleType self.deliverySetting = deliverySetting - self.adapter = adapter +// self.adapter = adapter if predicate == nil { self.predicate = HKQuery.predicateForSamples( - withStart: HealthKitSampleDataSource.loadDefaultQueryDate(for: sampleType), + withStart: HealthKitSampleDataSource.loadDefaultQueryDate(for: sampleType), end: nil, options: .strictEndDate ) @@ -105,43 +105,79 @@ final class HealthKitSampleDataSource: 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 - self.anchoredSingleObjectQuery() - } - await standard.registerDataSource(adapter.transform(healthKitSamples)) - } + // TODO: reimplement +// 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 +// self.anchoredSingleObjectQuery() +// } +// await standard.registerDataSource(adapter.transform(healthKitSamples)) +// } } - private func anchoredSingleObjectQuery() -> AsyncThrowingStream, Error> { - AsyncThrowingStream { continuation in + private func anchoredSingleObjectQuery() { //}-> AsyncThrowingStream, Error> { +// AsyncThrowingStream { continuation in Task { - let results = try await healthStore.anchoredSingleObjectQuery( + let resultsAnchor = try await healthStore.anchoredSingleObjectQuery( for: self.sampleType, using: self.anchor, - withPredicate: predicate + withPredicate: predicate, + standard: self.standard ) - self.anchor = results.anchor - for result in results.elements { - continuation.yield(result) - } - continuation.finish() + self.anchor = resultsAnchor // results.anchor +// for result in results.elements { +// continuation.yield(result) +// } +// continuation.finish() } - } +// } } - private func anchoredContinousObjectQuery() async -> any TypedAsyncSequence> { - AsyncThrowingStream { continuation in - Task { +// private func anchoredContinousObjectQuery() async -> any TypedAsyncSequence> { +// 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) +// } +// } +// } +// } + + // TODO: Solve this DataChange + private func anchoredContinousObjectQuery() async { +// -> any TypedAsyncSequence> { + AsyncThrowingStream { continuation in + _Concurrency.Task { try await healthStore.requestAuthorization(toShare: [], read: [sampleType]) let anchorDescriptor = healthStore.anchorDescriptor(sampleType: sampleType, predicate: predicate, anchor: anchor) @@ -156,16 +192,20 @@ final class HealthKitSampleDataSource: HealthKitDat } for deletedObject in results.deletedObjects { - continuation.yield(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType))) + // continuation.yield(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType))) + await standard.remove(removalContext: HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)) } for addedSample in results.addedSamples { - continuation.yield(.addition(addedSample)) + // continuation.yield(.addition(addedSample)) + await standard.add(addedSample) } self.anchor = results.newAnchor } } catch { + // continuation.finish(throwing: error) continuation.finish(throwing: error) + // TODO: what to put here } } } diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift index 908ec55..c22c3a6 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift @@ -16,31 +16,61 @@ extension HKSample: Identifiable { } } - extension HKHealthStore { +// func anchoredSingleObjectQuery( +// for sampleType: HKSampleType, +// using anchor: HKQueryAnchor? = nil, +// withPredicate predicate: NSPredicate? = nil +// ) async throws -> (elements: [DataChange], anchor: 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] = [] +// elements.reserveCapacity(result.deletedObjects.count + result.addedSamples.count) +// +// for deletedObject in result.deletedObjects { +// elements.append(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType))) +// } +// +// for addedSample in result.addedSamples { +// elements.append(.addition(addedSample)) +// } +// +// return (elements, result.newAnchor) +// } + func anchoredSingleObjectQuery( for sampleType: HKSampleType, using anchor: HKQueryAnchor? = nil, - withPredicate predicate: NSPredicate? = nil - ) async throws -> (elements: [DataChange], anchor: HKQueryAnchor) { + withPredicate predicate: NSPredicate? = nil, + standard: any HealthKitConstraint + ) async throws -> (HKQueryAnchor) { + //-> (elements: [HKSample], anchor: 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] = [] - elements.reserveCapacity(result.deletedObjects.count + result.addedSamples.count) - + +// var elements: [HKSample] = [] +// elements.reserveCapacity(result.deletedObjects.count + result.addedSamples.count) + for deletedObject in result.deletedObjects { - elements.append(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType))) +// elements.append(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType))) +// elements.append(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType))) + await standard.remove(removalContext: HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)) } - + for addedSample in result.addedSamples { - elements.append(.addition(addedSample)) +// elements.append(.addition(addedSample)) + await standard.add(addedSample) } - - return (elements, result.newAnchor) + +// return (elements, result.newAnchor) + return (result.newAnchor) } diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift index 1063d4b..34c2ce9 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift @@ -11,6 +11,7 @@ import Spezi extension HKHealthStore { + func sampleQuery( for sampleType: HKSampleType, withPredicate predicate: NSPredicate? = nil @@ -31,16 +32,40 @@ extension HKHealthStore { } +// func sampleQueryStream( +// for sampleType: HKSampleType, +// withPredicate predicate: NSPredicate? = nil +// ) -> AsyncThrowingStream, Error> { +// AsyncThrowingStream { continuation in +// Task { +// for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) { +// continuation.yield(.addition(sample)) +// } +// continuation.finish() +// } +// } +// } + + func sampleQueryStream( for sampleType: HKSampleType, - withPredicate predicate: NSPredicate? = nil - ) -> AsyncThrowingStream, 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 + ) { //}-> AsyncThrowingStream, Error> { +// AsyncThrowingStream { continuation in +// Task { +// for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) { +// continuation.yield(.addition(sample)) +// } +// continuation.finish() +// } +// } + + _Concurrency.Task { +// await standard.store(response) + + for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) { + await standard.add(sample) } } } diff --git a/Sources/SpeziHealthKit/HealthKit.swift b/Sources/SpeziHealthKit/HealthKit.swift index b3720a2..d40b374 100644 --- a/Sources/SpeziHealthKit/HealthKit.swift +++ b/Sources/SpeziHealthKit/HealthKit.swift @@ -49,19 +49,20 @@ import SwiftUI /// } /// } /// ``` -public final class HealthKit: Module { +public final class HealthKit: Module { /// The ``HealthKit/HKSampleAdapter`` type defines the mapping of `HKSample`s to the component's standard's base type. - public typealias HKSampleAdapter = any Adapter +// public typealias HKSampleAdapter = any Adapter - @StandardActor var standard: ComponentStandard + @StandardActor var standard: any HealthKitConstraint //ComponentStandard let healthStore: HKHealthStore let healthKitDataSourceDescriptions: [HealthKitDataSourceDescription] - let adapter: HKSampleAdapter +// let adapter: HKSampleAdapter lazy var healthKitComponents: [any HealthKitDataSource] = { healthKitDataSourceDescriptions - .flatMap { $0.dataSources(healthStore: healthStore, standard: standard, adapter: adapter) } +// .flatMap { $0.dataSources(healthStore: healthStore, standard: standard, adapter: adapter) } + .flatMap { $0.dataSources(healthStore: healthStore, standard: standard) } }() private var healthKitSampleTypes: Set { @@ -86,9 +87,12 @@ public final class HealthKit: Module { /// - Parameters: /// - healthKitDataSourceDescriptions: The ``HealthKitDataSourceDescription``s define what data is collected by the ``HealthKit`` module. You can, e.g., use ``CollectSample`` to collect a wide variaty of `HKSampleTypes`. /// - adapter: The ``HealthKit/HKSampleAdapter`` type defines the mapping of `HKSample`s to the component's standard's base type. +// public init( +// @HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> ([HealthKitDataSourceDescription]), +// @AdapterBuilder adapter: () -> (HKSampleAdapter) +// ) { public init( - @HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> ([HealthKitDataSourceDescription]), - @AdapterBuilder adapter: () -> (HKSampleAdapter) + @HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> ([HealthKitDataSourceDescription]) ) { precondition( HKHealthStore.isHealthDataAvailable(), @@ -103,10 +107,10 @@ public final class HealthKit: Module { ) let healthStore = HKHealthStore() - let adapter = adapter() +// let adapter = adapter() let healthKitDataSourceDescriptions = healthKitDataSourceDescriptions() - self.adapter = adapter +// self.adapter = adapter self.healthKitDataSourceDescriptions = healthKitDataSourceDescriptions self.healthStore = healthStore } diff --git a/Sources/SpeziHealthKit/HealthKitConstraint.swift b/Sources/SpeziHealthKit/HealthKitConstraint.swift new file mode 100644 index 0000000..31b943e --- /dev/null +++ b/Sources/SpeziHealthKit/HealthKitConstraint.swift @@ -0,0 +1,16 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Spezi +//import ModelsR4 +import HealthKit + +public protocol HealthKitConstraint: Standard { + func add(_ response: HKSample) async + func remove(removalContext: HKSampleRemovalContext) +} diff --git a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift index e6f0b4b..5353945 100644 --- a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift +++ b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift @@ -21,5 +21,7 @@ public protocol HealthKitDataSourceDescription { /// - healthStore: The `HKHealthStore` instance that the queries should be performed on. /// - standard: The `Standard` instance that is used in the software system. /// - adapter: An adapter that can adapt HealthKit data to the corresponding data standard. - func dataSources(healthStore: HKHealthStore, standard: S, adapter: HealthKit.HKSampleAdapter) -> [HealthKitDataSource] + // func dataSources(healthStore: HKHealthStore, standard: S, adapter: HealthKit.HKSampleAdapter) -> [HealthKitDataSource] + + func dataSources(healthStore: HKHealthStore, standard: any HealthKitConstraint) -> [HealthKitDataSource] } diff --git a/Tests/SpeziHealthKitTests/Shared/MockAdapterActor.swift b/Tests/SpeziHealthKitTests/Shared/MockAdapterActor.swift index 671e7bf..ddc3a32 100644 --- a/Tests/SpeziHealthKitTests/Shared/MockAdapterActor.swift +++ b/Tests/SpeziHealthKitTests/Shared/MockAdapterActor.swift @@ -6,26 +6,26 @@ // SPDX-License-Identifier: MIT // -import HealthKit -import Spezi -import SpeziHealthKit -import XCTSpezi - -actor MockAdapterActor: Adapter { - typealias InputElement = HKSample - typealias InputRemovalContext = HKSampleRemovalContext - typealias OutputElement = TestAppStandard.BaseType - typealias OutputRemovalContext = TestAppStandard.RemovalContext - - - func transform( - _ asyncSequence: some TypedAsyncSequence> - ) async -> any TypedAsyncSequence> { - asyncSequence.map { element in - element.map( - element: { OutputElement(id: String(describing: $0.id)) }, - removalContext: { OutputRemovalContext(id: $0.id.uuidString) } - ) - } - } -} +//import HealthKit +//import Spezi +//import SpeziHealthKit +//import XCTSpezi +// +//actor MockAdapterActor: Adapter { +// typealias InputElement = HKSample +// typealias InputRemovalContext = HKSampleRemovalContext +// typealias OutputElement = TestAppStandard.BaseType +// typealias OutputRemovalContext = TestAppStandard.RemovalContext +// +// +// func transform( +// _ asyncSequence: some TypedAsyncSequence> +// ) async -> any TypedAsyncSequence> { +// asyncSequence.map { element in +// element.map( +// element: { OutputElement(id: String(describing: $0.id)) }, +// removalContext: { OutputRemovalContext(id: $0.id.uuidString) } +// ) +// } +// } +//} diff --git a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift index e27c4a5..a228d09 100644 --- a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift +++ b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift @@ -17,14 +17,16 @@ final class SpeziHealthKitTests: XCTestCase { HKQuantityType(.distanceWalkingRunning) ] - let healthKitComponent: HealthKit = HealthKit { + let healthKitComponent: HealthKit = HealthKit { +// HealthKit = HealthKit { CollectSamples( collectedSamples, deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch) ) - } adapter: { - MockAdapterActor() } +//adapter: { +// MockAdapterActor() +// } override func tearDown() { // Clean up UserDefaults diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift index c4e9c0b..5d6014f 100644 --- a/Tests/UITests/TestApp/HealthKitTestsView.swift +++ b/Tests/UITests/TestApp/HealthKitTestsView.swift @@ -13,8 +13,11 @@ import XCTSpezi struct HealthKitTestsView: View { - @EnvironmentObject var healthKitComponent: HealthKit - @EnvironmentObject var standard: TestAppStandard + @EnvironmentObject var healthKitComponent: HealthKit // +// @EnvironmentObject var standard: TestAppStandard +// @StandardActor var standard: any HealthKitConstraint + + @State var dataChanges: [String] = [] @State var cancellable: AnyCancellable? @@ -35,11 +38,11 @@ struct HealthKitTestsView: View { } .task { self.dataChanges = await standard.dataChanges.map { $0.id } - cancellable = standard.objectWillChange.sink { - Task { @MainActor in - self.dataChanges = await standard.dataChanges.map { $0.id } - } - } +// cancellable = .standard.objectWillChange.sink { +// Task { @MainActor in +// self.dataChanges = await standard.dataChanges.map { $0.id } +// } +// } } .onDisappear { cancellable?.cancel() diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index 8fdf998..c542450 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -9,6 +9,7 @@ import Spezi import SwiftUI +// TODO: Change back bundle ID to edu.stanford.HPDS.healthkit.testapp when done @main struct UITestsApp: App { diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index db7651c..400e85a 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -11,11 +11,29 @@ import Spezi import SpeziHealthKit import XCTSpezi +/// an example Standard used for the configuration +actor ExampleStandard: Standard { + // ... +} + +extension ExampleStandard: HealthKitConstraint { + func add(_ response: HKSample) async { + print("add") + } + + func remove(removalContext: SpeziHealthKit.HKSampleRemovalContext) { + print("remove") + } + + +} class TestAppDelegate: SpeziAppDelegate { override var configuration: Configuration { - Configuration(standard: TestAppStandard()) { - HealthKit { +// Configuration(standard: TestAppStandard()) { + Configuration(standard: ExampleStandard()) { + + HealthKit { CollectSample( HKQuantityType.electrocardiogramType(), deliverySetting: .background(.manual) @@ -36,9 +54,10 @@ class TestAppDelegate: SpeziAppDelegate { HKQuantityType(.restingHeartRate), deliverySetting: .manual() ) - } adapter: { - TestAppHealthKitAdapter() } +// adapter: { +// TestAppHealthKitAdapter() +// } } } } diff --git a/Tests/UITests/TestApp/TestAppHealthKitAdapter.swift b/Tests/UITests/TestApp/TestAppHealthKitAdapter.swift index 3dd0747..50d7aeb 100644 --- a/Tests/UITests/TestApp/TestAppHealthKitAdapter.swift +++ b/Tests/UITests/TestApp/TestAppHealthKitAdapter.swift @@ -6,24 +6,24 @@ // SPDX-License-Identifier: MIT // -@preconcurrency import HealthKit -import Spezi -import SpeziHealthKit -import XCTSpezi - - -actor TestAppHealthKitAdapter: SingleValueAdapter { - typealias InputElement = HKSample - typealias InputRemovalContext = HKSampleRemovalContext - typealias OutputElement = TestAppStandard.BaseType - typealias OutputRemovalContext = TestAppStandard.RemovalContext - - - func transform(element: InputElement) throws -> OutputElement { - TestAppStandard.BaseType(id: element.sampleType.identifier) - } - - func transform(removalContext: InputRemovalContext) throws -> OutputRemovalContext { - OutputRemovalContext(id: removalContext.id.uuidString) - } -} +//@preconcurrency import HealthKit +//import Spezi +//import SpeziHealthKit +//import XCTSpezi +// +// +//actor TestAppHealthKitAdapter: SingleValueAdapter { +// typealias InputElement = HKSample +// typealias InputRemovalContext = HKSampleRemovalContext +// typealias OutputElement = TestAppStandard.BaseType +// typealias OutputRemovalContext = TestAppStandard.RemovalContext +// +// +// func transform(element: InputElement) throws -> OutputElement { +// TestAppStandard.BaseType(id: element.sampleType.identifier) +// } +// +// func transform(removalContext: InputRemovalContext) throws -> OutputRemovalContext { +// OutputRemovalContext(id: removalContext.id.uuidString) +// } +//} diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 1cd884c..1fbb299 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -375,10 +375,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 637867499T; + DEVELOPMENT_TEAM = 64FJ2MWNP4; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -393,8 +394,9 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testapp; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.HPDS.healthkit.testapp; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; @@ -408,10 +410,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 637867499T; + DEVELOPMENT_TEAM = 64FJ2MWNP4; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -426,8 +429,9 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testapp; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.HPDS.healthkit.testapp; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; @@ -441,7 +445,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 637867499T; + DEVELOPMENT_TEAM = 64FJ2MWNP4; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testappuitests; @@ -460,7 +464,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 637867499T; + DEVELOPMENT_TEAM = 64FJ2MWNP4; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testappuitests; @@ -539,10 +543,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 637867499T; + DEVELOPMENT_TEAM = 64FJ2MWNP4; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -557,8 +562,9 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testapp; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.HPDS.healthkit.testapp; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; @@ -572,7 +578,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 637867499T; + DEVELOPMENT_TEAM = 64FJ2MWNP4; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testappuitests; @@ -626,7 +632,7 @@ repositoryURL = "https://github.com/StanfordSpezi/Spezi.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.5.0; + minimumVersion = 0.7.0; }; }; 2F85827B29E778110021D637 /* XCRemoteSwiftPackageReference "XCTestExtensions" */ = { From ce8422390cea035b7a1c8f00aa18623977ea18a0 Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Mon, 24 Jul 2023 13:28:49 -0700 Subject: [PATCH 02/16] commenting changes --- .../CollectSample/CollectSample.swift | 5 +- .../CollectSample/CollectSamples.swift | 6 +- .../HealthKitSampleDataSource.swift | 104 +++++------------- Sources/SpeziHealthKit/HealthKit.swift | 17 +-- .../SpeziHealthKit/HealthKitConstraint.swift | 10 +- .../HealthKitDataSourceDescription.swift | 1 - .../UITests/TestApp/HealthKitTestsView.swift | 10 +- Tests/UITests/TestApp/TestApp.swift | 1 + Tests/UITests/TestApp/TestAppDelegate.swift | 5 - .../TestApp/TestAppHealthKitAdapter.swift | 29 ----- .../UITests/UITests.xcodeproj/project.pbxproj | 4 - 11 files changed, 47 insertions(+), 145 deletions(-) delete mode 100644 Tests/UITests/TestApp/TestAppHealthKitAdapter.swift diff --git a/Sources/SpeziHealthKit/CollectSample/CollectSample.swift b/Sources/SpeziHealthKit/CollectSample/CollectSample.swift index 6043d1c..5da8845 100644 --- a/Sources/SpeziHealthKit/CollectSample/CollectSample.swift +++ b/Sources/SpeziHealthKit/CollectSample/CollectSample.swift @@ -37,9 +37,8 @@ public struct CollectSample: HealthKitDataSourceDescription { public func dataSources( healthStore: HKHealthStore, - standard: any HealthKitConstraint //, -// adapter: HealthKit.HKSampleAdapter + standard: any HealthKitConstraint ) -> [any HealthKitDataSource] { - collectSamples.dataSources(healthStore: healthStore, standard: standard) //, standard: standard, adapter: adapter) + collectSamples.dataSources(healthStore: healthStore, standard: standard) } } diff --git a/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift b/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift index b98c300..efc642a 100644 --- a/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift +++ b/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift @@ -36,8 +36,7 @@ public struct CollectSamples: HealthKitDataSourceDescription { public func dataSources( healthStore: HKHealthStore, - standard: any HealthKitConstraint //, -// adapter: HealthKit.HKSampleAdapter + standard: any HealthKitConstraint ) -> [any HealthKitDataSource] { sampleTypes.map { sampleType in HealthKitSampleDataSource( @@ -45,8 +44,7 @@ public struct CollectSamples: HealthKitDataSourceDescription { standard: standard, sampleType: sampleType, predicate: predicate, - deliverySetting: deliverySetting //, -// adapter: adapter + deliverySetting: deliverySetting ) } } diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index d9e2dc1..7c423e0 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -13,13 +13,11 @@ import SwiftUI final class HealthKitSampleDataSource: HealthKitDataSource { let healthStore: HKHealthStore - let standard: any HealthKitConstraint //ComponentStandard + let standard: any HealthKitConstraint let sampleType: HKSampleType let predicate: NSPredicate? let deliverySetting: HealthKitDeliverySetting -// let adapter: HealthKit.HKSampleAdapter - var active = false private lazy var anchorUserDefaultsKey = UserDefaults.Keys.healthKitAnchorPrefix.appending(sampleType.identifier) @@ -30,19 +28,17 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } - required init( // swiftlint:disable:this function_default_parameter_at_end + required init( healthStore: HKHealthStore, 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.HKSampleAdapter + deliverySetting: HealthKitDeliverySetting ) { self.healthStore = healthStore self.standard = standard self.sampleType = sampleType self.deliverySetting = deliverySetting -// self.adapter = adapter if predicate == nil { self.predicate = HKQuery.predicateForSamples( @@ -100,82 +96,44 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } } + //TODO: PAUL, what to do here func triggerDataSourceCollection() async { guard !active else { return } // TODO: reimplement -// 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 -// self.anchoredSingleObjectQuery() -// } -// await standard.registerDataSource(adapter.transform(healthKitSamples)) -// } + 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 + self.anchoredSingleObjectQuery() + } + await standard.registerDataSource(adapter.transform(healthKitSamples)) + } } - private func anchoredSingleObjectQuery() { //}-> AsyncThrowingStream, Error> { -// AsyncThrowingStream { continuation in - Task { - let resultsAnchor = try await healthStore.anchoredSingleObjectQuery( - for: self.sampleType, - using: self.anchor, - withPredicate: predicate, - standard: self.standard - ) - self.anchor = resultsAnchor // 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> { -// 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) -// } -// } -// } -// } - - // TODO: Solve this DataChange + // TODO: PAUL, is AsyncThrowingStream needed Here? private func anchoredContinousObjectQuery() async { -// -> any TypedAsyncSequence> { AsyncThrowingStream { continuation in _Concurrency.Task { try await healthStore.requestAuthorization(toShare: [], read: [sampleType]) @@ -192,20 +150,16 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } for deletedObject in results.deletedObjects { - // continuation.yield(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType))) await standard.remove(removalContext: HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)) } for addedSample in results.addedSamples { - // continuation.yield(.addition(addedSample)) await standard.add(addedSample) } self.anchor = results.newAnchor } } catch { - // continuation.finish(throwing: error) continuation.finish(throwing: error) - // TODO: what to put here } } } diff --git a/Sources/SpeziHealthKit/HealthKit.swift b/Sources/SpeziHealthKit/HealthKit.swift index d40b374..b9911a4 100644 --- a/Sources/SpeziHealthKit/HealthKit.swift +++ b/Sources/SpeziHealthKit/HealthKit.swift @@ -41,8 +41,6 @@ import SwiftUI /// HKQuantityType(.restingHeartRate), /// deliverySetting: .manual() /// ) -/// } adapter: { -/// TestAppHealthKitAdapter() /// } /// } /// } @@ -50,18 +48,11 @@ import SwiftUI /// } /// ``` public final class HealthKit: Module { - /// The ``HealthKit/HKSampleAdapter`` type defines the mapping of `HKSample`s to the component's standard's base type. -// public typealias HKSampleAdapter = any Adapter - - - @StandardActor var standard: any HealthKitConstraint //ComponentStandard - + @StandardActor var standard: any HealthKitConstraint let healthStore: HKHealthStore let healthKitDataSourceDescriptions: [HealthKitDataSourceDescription] -// let adapter: HKSampleAdapter lazy var healthKitComponents: [any HealthKitDataSource] = { healthKitDataSourceDescriptions -// .flatMap { $0.dataSources(healthStore: healthStore, standard: standard, adapter: adapter) } .flatMap { $0.dataSources(healthStore: healthStore, standard: standard) } }() @@ -87,10 +78,6 @@ public final class HealthKit: Module { /// - Parameters: /// - healthKitDataSourceDescriptions: The ``HealthKitDataSourceDescription``s define what data is collected by the ``HealthKit`` module. You can, e.g., use ``CollectSample`` to collect a wide variaty of `HKSampleTypes`. /// - adapter: The ``HealthKit/HKSampleAdapter`` type defines the mapping of `HKSample`s to the component's standard's base type. -// public init( -// @HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> ([HealthKitDataSourceDescription]), -// @AdapterBuilder adapter: () -> (HKSampleAdapter) -// ) { public init( @HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> ([HealthKitDataSourceDescription]) ) { @@ -107,10 +94,8 @@ public final class HealthKit: Module { ) let healthStore = HKHealthStore() -// let adapter = adapter() let healthKitDataSourceDescriptions = healthKitDataSourceDescriptions() -// self.adapter = adapter self.healthKitDataSourceDescriptions = healthKitDataSourceDescriptions self.healthStore = healthStore } diff --git a/Sources/SpeziHealthKit/HealthKitConstraint.swift b/Sources/SpeziHealthKit/HealthKitConstraint.swift index 31b943e..79ade96 100644 --- a/Sources/SpeziHealthKit/HealthKitConstraint.swift +++ b/Sources/SpeziHealthKit/HealthKitConstraint.swift @@ -6,11 +6,17 @@ // SPDX-License-Identifier: MIT // -import Spezi -//import ModelsR4 import HealthKit +import Spezi +/// A Standard which all Spezi HealthKit modules must follow public protocol HealthKitConstraint: Standard { + + /// Adds a new `HKSample` to the ``HealthKit`` module + /// - Parameter response: The `HKSample` that should be added. func add(_ response: HKSample) async + + /// Removes a `HKSampleRemovalContext` from the ``HealthKit`` module + /// - Parameter response: The `HKSampleRemovalContext` that contains information on the item that should be removed. func remove(removalContext: HKSampleRemovalContext) } diff --git a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift index 5353945..7645ec3 100644 --- a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift +++ b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift @@ -22,6 +22,5 @@ public protocol HealthKitDataSourceDescription { /// - standard: The `Standard` instance that is used in the software system. /// - adapter: An adapter that can adapt HealthKit data to the corresponding data standard. // func dataSources(healthStore: HKHealthStore, standard: S, adapter: HealthKit.HKSampleAdapter) -> [HealthKitDataSource] - func dataSources(healthStore: HKHealthStore, standard: any HealthKitConstraint) -> [HealthKitDataSource] } diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift index 5d6014f..4ab7e59 100644 --- a/Tests/UITests/TestApp/HealthKitTestsView.swift +++ b/Tests/UITests/TestApp/HealthKitTestsView.swift @@ -13,11 +13,7 @@ import XCTSpezi struct HealthKitTestsView: View { - @EnvironmentObject var healthKitComponent: HealthKit // -// @EnvironmentObject var standard: TestAppStandard -// @StandardActor var standard: any HealthKitConstraint - - + @EnvironmentObject var healthKitComponent: HealthKit @State var dataChanges: [String] = [] @State var cancellable: AnyCancellable? @@ -37,7 +33,9 @@ struct HealthKitTestsView: View { } } .task { - self.dataChanges = await standard.dataChanges.map { $0.id } + // TODO: Paul, what to do here + // ATTENTION: How to replace these lines of code for the new standard +// self.dataChanges = await standard.dataChanges.map { $0.id } // cancellable = .standard.objectWillChange.sink { // Task { @MainActor in // self.dataChanges = await standard.dataChanges.map { $0.id } diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index c542450..f1db78f 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -9,6 +9,7 @@ import Spezi import SwiftUI +// ATTENTION // TODO: Change back bundle ID to edu.stanford.HPDS.healthkit.testapp when done @main diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index 400e85a..43fbb5a 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -30,9 +30,7 @@ extension ExampleStandard: HealthKitConstraint { class TestAppDelegate: SpeziAppDelegate { override var configuration: Configuration { -// Configuration(standard: TestAppStandard()) { Configuration(standard: ExampleStandard()) { - HealthKit { CollectSample( HKQuantityType.electrocardiogramType(), @@ -55,9 +53,6 @@ class TestAppDelegate: SpeziAppDelegate { deliverySetting: .manual() ) } -// adapter: { -// TestAppHealthKitAdapter() -// } } } } diff --git a/Tests/UITests/TestApp/TestAppHealthKitAdapter.swift b/Tests/UITests/TestApp/TestAppHealthKitAdapter.swift deleted file mode 100644 index 50d7aeb..0000000 --- a/Tests/UITests/TestApp/TestAppHealthKitAdapter.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -//@preconcurrency import HealthKit -//import Spezi -//import SpeziHealthKit -//import XCTSpezi -// -// -//actor TestAppHealthKitAdapter: SingleValueAdapter { -// typealias InputElement = HKSample -// typealias InputRemovalContext = HKSampleRemovalContext -// typealias OutputElement = TestAppStandard.BaseType -// typealias OutputRemovalContext = TestAppStandard.RemovalContext -// -// -// func transform(element: InputElement) throws -> OutputElement { -// TestAppStandard.BaseType(id: element.sampleType.identifier) -// } -// -// func transform(removalContext: InputRemovalContext) throws -> OutputRemovalContext { -// OutputRemovalContext(id: removalContext.id.uuidString) -// } -//} diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 1fbb299..47959f0 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ 2F85827329E776AC0021D637 /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F85827229E776AC0021D637 /* TestAppDelegate.swift */; }; 2F85827629E776D10021D637 /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F85827529E776D10021D637 /* Spezi */; }; 2F85827829E776D10021D637 /* XCTSpezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F85827729E776D10021D637 /* XCTSpezi */; }; - 2F85827A29E777980021D637 /* TestAppHealthKitAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F85827929E777980021D637 /* TestAppHealthKitAdapter.swift */; }; 2F85827F29E7782C0021D637 /* XCTestExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 2F85827E29E7782C0021D637 /* XCTestExtensions */; }; 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; }; 97B029102A5710C800946EF8 /* XCTHealthKit in Frameworks */ = {isa = PBXBuildFile; productRef = 97B0290F2A5710C800946EF8 /* XCTHealthKit */; }; @@ -38,7 +37,6 @@ 2F85826D29E776690021D637 /* SpeziHealthKitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeziHealthKitTests.swift; sourceTree = ""; }; 2F85827029E776780021D637 /* HealthKitTestsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HealthKitTestsView.swift; sourceTree = ""; }; 2F85827229E776AC0021D637 /* TestAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppDelegate.swift; sourceTree = ""; }; - 2F85827929E777980021D637 /* TestAppHealthKitAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestAppHealthKitAdapter.swift; sourceTree = ""; }; 2F85828329E77C4A0021D637 /* TestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TestApp.entitlements; sourceTree = ""; }; 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = ""; }; 2FB0758A299DDB9000C0B37F /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = ""; }; @@ -93,7 +91,6 @@ children = ( 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, 2F85827229E776AC0021D637 /* TestAppDelegate.swift */, - 2F85827929E777980021D637 /* TestAppHealthKitAdapter.swift */, 2F85827029E776780021D637 /* HealthKitTestsView.swift */, 2F85828329E77C4A0021D637 /* TestApp.entitlements */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, @@ -229,7 +226,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2F85827A29E777980021D637 /* TestAppHealthKitAdapter.swift in Sources */, 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, 2F85827129E776780021D637 /* HealthKitTestsView.swift in Sources */, 2F85827329E776AC0021D637 /* TestAppDelegate.swift in Sources */, From 4aa7a1b891f4c473637d0530437e9bf9546c5a0f Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Mon, 24 Jul 2023 13:32:57 -0700 Subject: [PATCH 03/16] File duplication issue --- .../CollectSample/HealthKitSampleDataSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index 7c423e0..e300b09 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -101,7 +101,7 @@ final class HealthKitSampleDataSource: HealthKitDataSource { guard !active else { return } - + // TODO: reimplement switch deliverySetting { case .manual: From fc00aace8838257714e5fcf8e2b27101ab8b1a62 Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Thu, 27 Jul 2023 16:17:03 -0700 Subject: [PATCH 04/16] New changes, UI not working yet --- .../HealthKitSampleDataSource.swift | 76 ++++++++----------- .../HKHealthStore+AnchoredObjectQuery.swift | 34 --------- .../HKHealthStore+SampleQuery.swift | 29 +------ .../UITests/TestApp/HealthKitTestsView.swift | 12 +++ Tests/UITests/TestApp/TestAppDelegate.swift | 21 ++++- 5 files changed, 63 insertions(+), 109 deletions(-) diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index e300b09..5002529 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -96,26 +96,26 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } } - //TODO: PAUL, what to do here func triggerDataSourceCollection() async { guard !active else { return } - - // TODO: reimplement - 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 { + print(error) } } @@ -132,36 +132,26 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } } - // TODO: PAUL, is AsyncThrowingStream needed Here? - private func anchoredContinousObjectQuery() async { - AsyncThrowingStream { continuation in - _Concurrency.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 { - await standard.remove(removalContext: HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)) - } - - for addedSample in results.addedSamples { - await standard.add(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(removalContext: HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)) + } + + for addedSample in results.addedSamples { + await standard.add(addedSample) } + self.anchor = results.newAnchor } } diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift index c22c3a6..ab295c9 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift @@ -17,59 +17,25 @@ extension HKSample: Identifiable { } extension HKHealthStore { -// func anchoredSingleObjectQuery( -// for sampleType: HKSampleType, -// using anchor: HKQueryAnchor? = nil, -// withPredicate predicate: NSPredicate? = nil -// ) async throws -> (elements: [DataChange], anchor: 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] = [] -// elements.reserveCapacity(result.deletedObjects.count + result.addedSamples.count) -// -// for deletedObject in result.deletedObjects { -// elements.append(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType))) -// } -// -// for addedSample in result.addedSamples { -// elements.append(.addition(addedSample)) -// } -// -// return (elements, result.newAnchor) -// } - func anchoredSingleObjectQuery( for sampleType: HKSampleType, using anchor: HKQueryAnchor? = nil, withPredicate predicate: NSPredicate? = nil, standard: any HealthKitConstraint ) async throws -> (HKQueryAnchor) { - //-> (elements: [HKSample], anchor: 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: [HKSample] = [] -// elements.reserveCapacity(result.deletedObjects.count + result.addedSamples.count) - for deletedObject in result.deletedObjects { -// elements.append(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType))) -// elements.append(.removal(HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType))) await standard.remove(removalContext: HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)) } for addedSample in result.addedSamples { -// elements.append(.addition(addedSample)) await standard.add(addedSample) } -// return (elements, result.newAnchor) return (result.newAnchor) } diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift index 34c2ce9..f485554 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift @@ -31,39 +31,12 @@ extension HKHealthStore { return try await sampleQueryDescriptor.result(for: self) } - -// func sampleQueryStream( -// for sampleType: HKSampleType, -// withPredicate predicate: NSPredicate? = nil -// ) -> AsyncThrowingStream, Error> { -// AsyncThrowingStream { continuation in -// Task { -// for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) { -// continuation.yield(.addition(sample)) -// } -// continuation.finish() -// } -// } -// } - - func sampleQueryStream( for sampleType: HKSampleType, withPredicate predicate: NSPredicate? = nil, standard: any HealthKitConstraint - ) { //}-> AsyncThrowingStream, Error> { -// AsyncThrowingStream { continuation in -// Task { -// for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) { -// continuation.yield(.addition(sample)) -// } -// continuation.finish() -// } -// } - + ) { _Concurrency.Task { -// await standard.store(response) - for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) { await standard.add(sample) } diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift index 4ab7e59..830dac8 100644 --- a/Tests/UITests/TestApp/HealthKitTestsView.swift +++ b/Tests/UITests/TestApp/HealthKitTestsView.swift @@ -14,6 +14,9 @@ import XCTSpezi struct HealthKitTestsView: View { @EnvironmentObject var healthKitComponent: HealthKit + @EnvironmentObject var standard: ExampleStandard + + // use environemnt to pass in standard @State var dataChanges: [String] = [] @State var cancellable: AnyCancellable? @@ -36,11 +39,20 @@ struct HealthKitTestsView: View { // TODO: Paul, what to do here // ATTENTION: How to replace these lines of code for the new standard // self.dataChanges = await standard.dataChanges.map { $0.id } +// // replace datachanges with addedElements array // cancellable = .standard.objectWillChange.sink { // Task { @MainActor in // self.dataChanges = await standard.dataChanges.map { $0.id } // } // } + + self.dataChanges = await standard.addedResponses.map { $0.id } + cancellable = standard.objectWillChange.sink { + Task { @MainActor in + self.dataChanges = await standard.addedResponses.map { $0.id } + } + } + } .onDisappear { cancellable?.cancel() diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index 43fbb5a..47fc20e 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -11,20 +11,33 @@ import Spezi import SpeziHealthKit import XCTSpezi +struct HKItem { + var data: HKSample + var id: String +} + /// an example Standard used for the configuration -actor ExampleStandard: Standard { - // ... +actor ExampleStandard: Standard, ObservableObjectProvider, ObservableObject { +// var observableObjectProviders: [any ObservableObjectProvider] + + var addedResponses = [HKItem]() } extension ExampleStandard: HealthKitConstraint { + func add(_ response: HKSample) async { - print("add") +// addedResponses.append(response) + addedResponses.append(.init(data: response, id: "\(UUID())")) } func remove(removalContext: SpeziHealthKit.HKSampleRemovalContext) { - print("remove") + if let index = addedResponses.firstIndex(where: { $0.data.sampleType == removalContext.sampleType && $0.id == "\(removalContext.id)" }) { + addedResponses.remove(at: index) + } + } + // store by appening to added elements, and removed elements for data changes old code } From 9c1e33282cc2ba327c6c6246709d5e816a105afe Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Mon, 31 Jul 2023 16:39:02 -0700 Subject: [PATCH 05/16] linting --- .../HKHealthStore+SampleQuery.swift | 1 - .../SpeziHealthKit/HealthKitConstraint.swift | 4 +-- .../Shared/MockAdapterActor.swift | 31 ---------------- .../SpeziHealthKitTests.swift | 4 --- Tests/UITests/TestApp/ExampleStandard.swift | 36 +++++++++++++++++++ .../UITests/TestApp/HealthKitTestsView.swift | 12 ------- Tests/UITests/TestApp/TestAppDelegate.swift | 30 ---------------- .../UITests/UITests.xcodeproj/project.pbxproj | 25 +++++++++++++ 8 files changed, 63 insertions(+), 80 deletions(-) delete mode 100644 Tests/SpeziHealthKitTests/Shared/MockAdapterActor.swift create mode 100644 Tests/UITests/TestApp/ExampleStandard.swift diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift index f485554..2231d64 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift @@ -11,7 +11,6 @@ import Spezi extension HKHealthStore { - func sampleQuery( for sampleType: HKSampleType, withPredicate predicate: NSPredicate? = nil diff --git a/Sources/SpeziHealthKit/HealthKitConstraint.swift b/Sources/SpeziHealthKit/HealthKitConstraint.swift index 79ade96..7e640c3 100644 --- a/Sources/SpeziHealthKit/HealthKitConstraint.swift +++ b/Sources/SpeziHealthKit/HealthKitConstraint.swift @@ -9,9 +9,9 @@ import HealthKit import Spezi -/// A Standard which all Spezi HealthKit modules must follow + +/// A Constraint which all `Standard` instances must conform to when using the Spezi HealthKit module. public protocol HealthKitConstraint: Standard { - /// Adds a new `HKSample` to the ``HealthKit`` module /// - Parameter response: The `HKSample` that should be added. func add(_ response: HKSample) async diff --git a/Tests/SpeziHealthKitTests/Shared/MockAdapterActor.swift b/Tests/SpeziHealthKitTests/Shared/MockAdapterActor.swift deleted file mode 100644 index ddc3a32..0000000 --- a/Tests/SpeziHealthKitTests/Shared/MockAdapterActor.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -//import HealthKit -//import Spezi -//import SpeziHealthKit -//import XCTSpezi -// -//actor MockAdapterActor: Adapter { -// typealias InputElement = HKSample -// typealias InputRemovalContext = HKSampleRemovalContext -// typealias OutputElement = TestAppStandard.BaseType -// typealias OutputRemovalContext = TestAppStandard.RemovalContext -// -// -// func transform( -// _ asyncSequence: some TypedAsyncSequence> -// ) async -> any TypedAsyncSequence> { -// asyncSequence.map { element in -// element.map( -// element: { OutputElement(id: String(describing: $0.id)) }, -// removalContext: { OutputRemovalContext(id: $0.id.uuidString) } -// ) -// } -// } -//} diff --git a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift index a228d09..b746d1b 100644 --- a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift +++ b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift @@ -18,15 +18,11 @@ final class SpeziHealthKitTests: XCTestCase { ] let healthKitComponent: HealthKit = HealthKit { -// HealthKit = HealthKit { CollectSamples( collectedSamples, deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch) ) } -//adapter: { -// MockAdapterActor() -// } override func tearDown() { // Clean up UserDefaults diff --git a/Tests/UITests/TestApp/ExampleStandard.swift b/Tests/UITests/TestApp/ExampleStandard.swift new file mode 100644 index 0000000..b4477cf --- /dev/null +++ b/Tests/UITests/TestApp/ExampleStandard.swift @@ -0,0 +1,36 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import HealthKit +import Spezi +import SpeziHealthKit + + +/// an example datatype for storing HealthSamples. +struct HKItem { + var data: HKSample + var id: String +} + + +/// an example Standard used for the configuration. +actor ExampleStandard: Standard, ObservableObjectProvider, ObservableObject { + var addedResponses = [HKItem]() +} + +extension ExampleStandard: HealthKitConstraint { + func add(_ response: HKSample) async { + addedResponses.append(.init(data: response, id: "\(UUID())")) + } + + func remove(removalContext: SpeziHealthKit.HKSampleRemovalContext) { + if let index = addedResponses.firstIndex(where: { $0.data.sampleType == removalContext.sampleType && $0.id == "\(removalContext.id)" }) { + addedResponses.remove(at: index) + } + } +} diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift index 830dac8..9a7ba35 100644 --- a/Tests/UITests/TestApp/HealthKitTestsView.swift +++ b/Tests/UITests/TestApp/HealthKitTestsView.swift @@ -16,7 +16,6 @@ struct HealthKitTestsView: View { @EnvironmentObject var healthKitComponent: HealthKit @EnvironmentObject var standard: ExampleStandard - // use environemnt to pass in standard @State var dataChanges: [String] = [] @State var cancellable: AnyCancellable? @@ -36,23 +35,12 @@ struct HealthKitTestsView: View { } } .task { - // TODO: Paul, what to do here - // ATTENTION: How to replace these lines of code for the new standard -// self.dataChanges = await standard.dataChanges.map { $0.id } -// // replace datachanges with addedElements array -// cancellable = .standard.objectWillChange.sink { -// Task { @MainActor in -// self.dataChanges = await standard.dataChanges.map { $0.id } -// } -// } - self.dataChanges = await standard.addedResponses.map { $0.id } cancellable = standard.objectWillChange.sink { Task { @MainActor in self.dataChanges = await standard.addedResponses.map { $0.id } } } - } .onDisappear { cancellable?.cancel() diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index 47fc20e..458e11c 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -11,36 +11,6 @@ import Spezi import SpeziHealthKit import XCTSpezi -struct HKItem { - var data: HKSample - var id: String -} - -/// an example Standard used for the configuration -actor ExampleStandard: Standard, ObservableObjectProvider, ObservableObject { -// var observableObjectProviders: [any ObservableObjectProvider] - - var addedResponses = [HKItem]() -} - -extension ExampleStandard: HealthKitConstraint { - - func add(_ response: HKSample) async { -// addedResponses.append(response) - addedResponses.append(.init(data: response, id: "\(UUID())")) - } - - func remove(removalContext: SpeziHealthKit.HKSampleRemovalContext) { - if let index = addedResponses.firstIndex(where: { $0.data.sampleType == removalContext.sampleType && $0.id == "\(removalContext.id)" }) { - addedResponses.remove(at: index) - } - - } - - // store by appening to added elements, and removed elements for data changes old code - -} - class TestAppDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration(standard: ExampleStandard()) { diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 47959f0..f18a3ae 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 2F85827829E776D10021D637 /* XCTSpezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F85827729E776D10021D637 /* XCTSpezi */; }; 2F85827F29E7782C0021D637 /* XCTestExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 2F85827E29E7782C0021D637 /* XCTestExtensions */; }; 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; }; + 390F29612A785A98000A236E /* ExampleStandard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390F29602A785A98000A236E /* ExampleStandard.swift */; }; 97B029102A5710C800946EF8 /* XCTHealthKit in Frameworks */ = {isa = PBXBuildFile; productRef = 97B0290F2A5710C800946EF8 /* XCTHealthKit */; }; /* End PBXBuildFile section */ @@ -40,6 +41,7 @@ 2F85828329E77C4A0021D637 /* TestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TestApp.entitlements; sourceTree = ""; }; 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = ""; }; 2FB0758A299DDB9000C0B37F /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = ""; }; + 390F29602A785A98000A236E /* ExampleStandard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleStandard.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -91,6 +93,7 @@ children = ( 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, 2F85827229E776AC0021D637 /* TestAppDelegate.swift */, + 390F29602A785A98000A236E /* ExampleStandard.swift */, 2F85827029E776780021D637 /* HealthKitTestsView.swift */, 2F85828329E77C4A0021D637 /* TestApp.entitlements */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, @@ -123,6 +126,7 @@ 2F6D138E28F5F384007C25D6 /* Sources */, 2F6D138F28F5F384007C25D6 /* Frameworks */, 2F6D139028F5F384007C25D6 /* Resources */, + 390F295F2A78596E000A236E /* ShellScript */, ); buildRules = ( ); @@ -221,12 +225,33 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 390F295F2A78596E000A236E /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint > /dev/null; then\n cd ../../ && swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 2F6D138E28F5F384007C25D6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, + 390F29612A785A98000A236E /* ExampleStandard.swift in Sources */, 2F85827129E776780021D637 /* HealthKitTestsView.swift in Sources */, 2F85827329E776AC0021D637 /* TestAppDelegate.swift in Sources */, ); From f6d315fa9b5309f6c8f887c4ea68698dc974bf2f Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Tue, 1 Aug 2023 14:09:19 -0700 Subject: [PATCH 06/16] linting --- Tests/UITests/TestApp/TestApp.swift | 3 --- Tests/UITests/UITests.xcodeproj/project.pbxproj | 12 ++++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index f1db78f..f7a5b7b 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -9,9 +9,6 @@ import Spezi import SwiftUI -// ATTENTION -// TODO: Change back bundle ID to edu.stanford.HPDS.healthkit.testapp when done - @main struct UITestsApp: App { @UIApplicationDelegateAdaptor(TestAppDelegate.self) var appDelegate diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index f18a3ae..f5e5448 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -400,7 +400,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 64FJ2MWNP4; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -415,7 +415,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.HPDS.healthkit.testapp; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; @@ -435,7 +435,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 64FJ2MWNP4; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -450,7 +450,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.HPDS.healthkit.testapp; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; @@ -568,7 +568,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 64FJ2MWNP4; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -583,7 +583,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.HPDS.healthkit.testapp; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; From 722875e2ded7ac9397d61ceea0020a5fa4240616 Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Tue, 1 Aug 2023 14:24:52 -0700 Subject: [PATCH 07/16] Comments for HealthKit --- Sources/SpeziHealthKit/HealthKit.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sources/SpeziHealthKit/HealthKit.swift b/Sources/SpeziHealthKit/HealthKit.swift index b9911a4..2442f82 100644 --- a/Sources/SpeziHealthKit/HealthKit.swift +++ b/Sources/SpeziHealthKit/HealthKit.swift @@ -13,6 +13,22 @@ import SwiftUI /// The ``HealthKit`` module enables the collection of HealthKit data and transforms it to the component's standard's base type using an `Adapter` (``HealthKit/HKSampleAdapter``) /// +/// Configuration for the ``SpeziHealthKit`` module. +/// +/// Make sure that your standard in your Spezi Application conforms to the ``HealthKitConstraint`` +/// protocol to receive HealthKit data. +/// ```swift +/// actor ExampleStandard: Standard, HealthKitConstraint { +/// func add(_ response: HKSample) async { +/// ... +/// } +/// +/// func remove(removalContext: SpeziHealthKit.HKSampleRemovalContext) { +/// ... +/// } +/// } +/// ``` +/// /// Use the ``HealthKit/init(_:adapter:)`` initializer to define different ``HealthKitDataSourceDescription``s to define the data collection. /// You can, e.g., use ``CollectSample`` to collect a wide variaty of `HKSampleTypes`: /// ```swift From 82e2795d09f224243443c285fcbc46ee205a087a Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Tue, 1 Aug 2023 14:41:23 -0700 Subject: [PATCH 08/16] Linting and comments --- .../CollectSample/HealthKitSampleDataSource.swift | 7 +++++-- .../HKHealthStore+AnchoredObjectQuery.swift | 4 ++++ .../HealthKit Extensions/HKHealthStore+SampleQuery.swift | 4 ++++ Sources/SpeziHealthKit/HealthKit.swift | 5 ++--- .../HealthKitDataSourceDescription.swift | 4 +--- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index 5002529..b8bcd4a 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -27,12 +27,14 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } } - + // 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: 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. + predicate: NSPredicate? = nil, deliverySetting: HealthKitDeliverySetting ) { self.healthStore = healthStore @@ -50,6 +52,7 @@ final class HealthKitSampleDataSource: HealthKitDataSource { self.predicate = predicate } } + // swiftlint:enable function_default_parameter_at_end private static func loadDefaultQueryDate(for sampleType: HKSampleType) -> Date { diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift index ab295c9..ad88efa 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift @@ -17,6 +17,9 @@ 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, @@ -38,6 +41,7 @@ extension HKHealthStore { return (result.newAnchor) } + // swiftlint:enable function_default_parameter_at_end func anchorDescriptor( diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift index 2231d64..6a85ca8 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift @@ -30,6 +30,9 @@ 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, @@ -41,4 +44,5 @@ extension HKHealthStore { } } } + // swiftlint:enable function_default_parameter_at_end } diff --git a/Sources/SpeziHealthKit/HealthKit.swift b/Sources/SpeziHealthKit/HealthKit.swift index 2442f82..5a6833c 100644 --- a/Sources/SpeziHealthKit/HealthKit.swift +++ b/Sources/SpeziHealthKit/HealthKit.swift @@ -11,7 +11,7 @@ import Spezi import SwiftUI -/// The ``HealthKit`` module enables the collection of HealthKit data and transforms it to the component's standard's base type using an `Adapter` (``HealthKit/HKSampleAdapter``) +/// The ``HealthKit`` module enables the collection of HealthKit data. /// /// Configuration for the ``SpeziHealthKit`` module. /// @@ -29,7 +29,7 @@ import SwiftUI /// } /// ``` /// -/// Use the ``HealthKit/init(_:adapter:)`` initializer to define different ``HealthKitDataSourceDescription``s to define the data collection. +/// Use the ``HealthKit/init(_:)`` initializer to define different ``HealthKitDataSourceDescription``s to define the data collection. /// You can, e.g., use ``CollectSample`` to collect a wide variaty of `HKSampleTypes`: /// ```swift /// class ExampleAppDelegate: SpeziAppDelegate { @@ -93,7 +93,6 @@ public final class HealthKit: Module { /// Creates a new instance of the ``HealthKit`` module. /// - Parameters: /// - healthKitDataSourceDescriptions: The ``HealthKitDataSourceDescription``s define what data is collected by the ``HealthKit`` module. You can, e.g., use ``CollectSample`` to collect a wide variaty of `HKSampleTypes`. - /// - adapter: The ``HealthKit/HKSampleAdapter`` type defines the mapping of `HKSample`s to the component's standard's base type. public init( @HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> ([HealthKitDataSourceDescription]) ) { diff --git a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift index 7645ec3..ae3cb57 100644 --- a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift +++ b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift @@ -16,11 +16,9 @@ public protocol HealthKitDataSourceDescription { var sampleTypes: Set { get } - /// The ``HealthKitDataSourceDescription/dataSources(healthStore:standard:adapter:)`` method creates ``HealthKitDataSource`` swhen the HealthKit component is instantiated. + /// The ``HealthKitDataSourceDescription/dataSources(healthStore:standard:)`` method creates ``HealthKitDataSource`` swhen the HealthKit component is instantiated. /// - Parameters: /// - healthStore: The `HKHealthStore` instance that the queries should be performed on. /// - standard: The `Standard` instance that is used in the software system. - /// - adapter: An adapter that can adapt HealthKit data to the corresponding data standard. - // func dataSources(healthStore: HKHealthStore, standard: S, adapter: HealthKit.HKSampleAdapter) -> [HealthKitDataSource] func dataSources(healthStore: HKHealthStore, standard: any HealthKitConstraint) -> [HealthKitDataSource] } From 51aca10640010fd0a7a8674265536e0bd904ffee Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Tue, 1 Aug 2023 14:42:27 -0700 Subject: [PATCH 09/16] Lint- redundant type --- Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift index b746d1b..c0d047b 100644 --- a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift +++ b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift @@ -17,7 +17,7 @@ final class SpeziHealthKitTests: XCTestCase { HKQuantityType(.distanceWalkingRunning) ] - let healthKitComponent: HealthKit = HealthKit { + let healthKitComponent = HealthKit { CollectSamples( collectedSamples, deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch) From 9ec56e35ccb47b322402bdd123cdbb18c3900ff8 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 6 Aug 2023 12:34:50 -0700 Subject: [PATCH 10/16] Add explicit TestApp Scheme --- .../xcshareddata/xcschemes/TestApp.xcscheme | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme new file mode 100644 index 0000000..b8e2be0 --- /dev/null +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 07898558b40e9e86956b88396089af0dd06eb530 Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Tue, 8 Aug 2023 16:01:54 -0700 Subject: [PATCH 11/16] First round of revisions --- .../Adapter/HKSampleRemovalContext.swift | 22 ------------- .../HealthKitSampleDataSource.swift | 4 +-- .../HKHealthStore+AnchoredObjectQuery.swift | 5 +-- .../HKHealthStore+SampleQuery.swift | 2 +- Sources/SpeziHealthKit/HealthKit.swift | 4 +-- .../SpeziHealthKit/HealthKitConstraint.swift | 26 ++++++++++++--- Tests/UITests/TestApp/ExampleStandard.swift | 27 +++++++-------- .../UITests/TestApp/HealthKitTestsView.swift | 33 ++++++++++--------- Tests/UITests/TestApp/TestApp.swift | 1 + Tests/UITests/TestApp/TestAppDelegate.swift | 1 + .../UITests/UITests.xcodeproj/project.pbxproj | 6 ++-- 11 files changed, 64 insertions(+), 67 deletions(-) delete mode 100644 Sources/SpeziHealthKit/Adapter/HKSampleRemovalContext.swift diff --git a/Sources/SpeziHealthKit/Adapter/HKSampleRemovalContext.swift b/Sources/SpeziHealthKit/Adapter/HKSampleRemovalContext.swift deleted file mode 100644 index 26c37e7..0000000 --- a/Sources/SpeziHealthKit/Adapter/HKSampleRemovalContext.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -@preconcurrency import HealthKit -import Spezi - - -public struct HKSampleRemovalContext: Identifiable, Sendable { - public let id: HKSample.ID - public let sampleType: HKSampleType - - - public init(id: HKSample.ID, sampleType: HKSampleType) { - self.id = id - self.sampleType = sampleType - } -} diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index b8bcd4a..360ed42 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -148,11 +148,11 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } for deletedObject in results.deletedObjects { - await standard.remove(removalContext: HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)) + await standard.remove(sample: deletedObject) } for addedSample in results.addedSamples { - await standard.add(addedSample) + await standard.add(sample: addedSample) } self.anchor = results.newAnchor } diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift index ad88efa..a317e92 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift @@ -32,11 +32,12 @@ extension HKHealthStore { let result = try await anchorDescriptor.result(for: self) for deletedObject in result.deletedObjects { - await standard.remove(removalContext: HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)) +// await standard.remove(removalContext: HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)) + await standard.remove(sample: deletedObject) } for addedSample in result.addedSamples { - await standard.add(addedSample) + await standard.add(sample: addedSample) } return (result.newAnchor) diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift index 6a85ca8..6428c2d 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift @@ -40,7 +40,7 @@ extension HKHealthStore { ) { _Concurrency.Task { for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) { - await standard.add(sample) + await standard.add(sample: sample) } } } diff --git a/Sources/SpeziHealthKit/HealthKit.swift b/Sources/SpeziHealthKit/HealthKit.swift index 5a6833c..5a78c97 100644 --- a/Sources/SpeziHealthKit/HealthKit.swift +++ b/Sources/SpeziHealthKit/HealthKit.swift @@ -19,11 +19,11 @@ import SwiftUI /// protocol to receive HealthKit data. /// ```swift /// actor ExampleStandard: Standard, HealthKitConstraint { -/// func add(_ response: HKSample) async { +/// func add(sample: HKSample) async { /// ... /// } /// -/// func remove(removalContext: SpeziHealthKit.HKSampleRemovalContext) { +/// func remove(sample: HKDeletedObject) { /// ... /// } /// } diff --git a/Sources/SpeziHealthKit/HealthKitConstraint.swift b/Sources/SpeziHealthKit/HealthKitConstraint.swift index 7e640c3..5657b5d 100644 --- a/Sources/SpeziHealthKit/HealthKitConstraint.swift +++ b/Sources/SpeziHealthKit/HealthKitConstraint.swift @@ -11,12 +11,28 @@ import Spezi /// A Constraint which all `Standard` instances must conform to when using the Spezi HealthKit module. +/// +/// +/// Make sure that your standard in your Spezi Application conforms to the ``HealthKitConstraint`` +/// protocol to receive HealthKit data. +/// ```swift +/// actor ExampleStandard: Standard, HealthKitConstraint { +/// func add(sample: HKSample) async { +/// ... +/// } +/// +/// func remove(sample: HKDeletedObject) { +/// ... +/// } +/// } +/// ``` +/// public protocol HealthKitConstraint: Standard { /// Adds a new `HKSample` to the ``HealthKit`` module - /// - Parameter response: The `HKSample` that should be added. - func add(_ response: HKSample) async + /// - Parameter sample: The `HKSample` that should be added. + func add(sample: HKSample) async - /// Removes a `HKSampleRemovalContext` from the ``HealthKit`` module - /// - Parameter response: The `HKSampleRemovalContext` that contains information on the item that should be removed. - func remove(removalContext: HKSampleRemovalContext) + /// Removes a `HKDeletedObject` from the ``HealthKit`` module + /// - Parameter sample: The `HKDeletedObject` is a sample that should be removed. + func remove(sample: HKDeletedObject) async } diff --git a/Tests/UITests/TestApp/ExampleStandard.swift b/Tests/UITests/TestApp/ExampleStandard.swift index b4477cf..2616dd3 100644 --- a/Tests/UITests/TestApp/ExampleStandard.swift +++ b/Tests/UITests/TestApp/ExampleStandard.swift @@ -6,31 +6,28 @@ // SPDX-License-Identifier: MIT // -import HealthKit +@preconcurrency import HealthKit import Spezi import SpeziHealthKit -/// an example datatype for storing HealthSamples. -struct HKItem { - var data: HKSample - var id: String -} - - /// an example Standard used for the configuration. -actor ExampleStandard: Standard, ObservableObjectProvider, ObservableObject { - var addedResponses = [HKItem]() +actor ExampleStandard: Standard, ObservableObject, ObservableObjectProvider { + @Published @MainActor var addedResponses = [HKSample]() } extension ExampleStandard: HealthKitConstraint { - func add(_ response: HKSample) async { - addedResponses.append(.init(data: response, id: "\(UUID())")) + func add(sample: HKSample) async { + _Concurrency.Task { @MainActor in + addedResponses.append(sample) + } } - func remove(removalContext: SpeziHealthKit.HKSampleRemovalContext) { - if let index = addedResponses.firstIndex(where: { $0.data.sampleType == removalContext.sampleType && $0.id == "\(removalContext.id)" }) { - addedResponses.remove(at: index) + func remove(sample: HKDeletedObject) async { + _Concurrency.Task { @MainActor in + if let index = addedResponses.firstIndex(where: { $0.uuid == sample.uuid }) { + addedResponses.remove(at: index) + } } } } diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift index 9a7ba35..0229b8a 100644 --- a/Tests/UITests/TestApp/HealthKitTestsView.swift +++ b/Tests/UITests/TestApp/HealthKitTestsView.swift @@ -7,6 +7,7 @@ // import Combine +import HealthKit import SpeziHealthKit import SwiftUI import XCTSpezi @@ -16,8 +17,8 @@ struct HealthKitTestsView: View { @EnvironmentObject var healthKitComponent: HealthKit @EnvironmentObject var standard: ExampleStandard - @State var dataChanges: [String] = [] - @State var cancellable: AnyCancellable? +// @State var dataChanges: [HKSample] = [] +// @State var cancellable: AnyCancellable? var body: some View { @@ -30,21 +31,23 @@ struct HealthKitTestsView: View { triggerDataSourceCollection() } HStack { - List(dataChanges, id: \.self) { element in - Text(element) +// List(dataChanges, id: \.self) { element in + + List(standard.addedResponses, id: \.self) { element in + Text(element.sampleType.identifier) } } - .task { - self.dataChanges = await standard.addedResponses.map { $0.id } - cancellable = standard.objectWillChange.sink { - Task { @MainActor in - self.dataChanges = await standard.addedResponses.map { $0.id } - } - } - } - .onDisappear { - cancellable?.cancel() - } +// .task { +// self.dataChanges = await standard.addedResponses //.map { $0.id } +// cancellable = standard.objectWillChange.sink { +// Task { @MainActor in +// self.dataChanges = await standard.addedResponses //.map { $0.id } +// } +// } +// } +// .onDisappear { +// cancellable?.cancel() +// } } diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index f7a5b7b..8fdf998 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -9,6 +9,7 @@ import Spezi import SwiftUI + @main struct UITestsApp: App { @UIApplicationDelegateAdaptor(TestAppDelegate.self) var appDelegate diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index 458e11c..8b92b6f 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -11,6 +11,7 @@ import Spezi import SpeziHealthKit import XCTSpezi + class TestAppDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration(standard: ExampleStandard()) { diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index f5e5448..d95183e 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -400,7 +400,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 64FJ2MWNP4; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -435,7 +435,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 64FJ2MWNP4; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -568,7 +568,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 64FJ2MWNP4; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; From 4869f682ef5cdadfb19d85d426a84f39ba4926da Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Wed, 9 Aug 2023 11:15:24 -0700 Subject: [PATCH 12/16] deletion of old code --- .../HKHealthStore+AnchoredObjectQuery.swift | 1 - Tests/UITests/TestApp/HealthKitTestsView.swift | 16 ---------------- 2 files changed, 17 deletions(-) diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift index a317e92..172fdef 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift @@ -32,7 +32,6 @@ extension HKHealthStore { let result = try await anchorDescriptor.result(for: self) for deletedObject in result.deletedObjects { -// await standard.remove(removalContext: HKSampleRemovalContext(id: deletedObject.uuid, sampleType: sampleType)) await standard.remove(sample: deletedObject) } diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift index 0229b8a..e164dd6 100644 --- a/Tests/UITests/TestApp/HealthKitTestsView.swift +++ b/Tests/UITests/TestApp/HealthKitTestsView.swift @@ -17,9 +17,6 @@ struct HealthKitTestsView: View { @EnvironmentObject var healthKitComponent: HealthKit @EnvironmentObject var standard: ExampleStandard -// @State var dataChanges: [HKSample] = [] -// @State var cancellable: AnyCancellable? - var body: some View { Button("Ask for authorization") { @@ -31,23 +28,10 @@ struct HealthKitTestsView: View { triggerDataSourceCollection() } HStack { -// List(dataChanges, id: \.self) { element in - List(standard.addedResponses, id: \.self) { element in Text(element.sampleType.identifier) } } -// .task { -// self.dataChanges = await standard.addedResponses //.map { $0.id } -// cancellable = standard.objectWillChange.sink { -// Task { @MainActor in -// self.dataChanges = await standard.addedResponses //.map { $0.id } -// } -// } -// } -// .onDisappear { -// cancellable?.cancel() -// } } From fad96f1b125331e1610b5742c68ae1629b904a73 Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Wed, 9 Aug 2023 14:44:42 -0700 Subject: [PATCH 13/16] HealthKitConstraint comment Co-authored-by: Paul Schmiedmayer --- Sources/SpeziHealthKit/HealthKitConstraint.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SpeziHealthKit/HealthKitConstraint.swift b/Sources/SpeziHealthKit/HealthKitConstraint.swift index 5657b5d..f68657d 100644 --- a/Sources/SpeziHealthKit/HealthKitConstraint.swift +++ b/Sources/SpeziHealthKit/HealthKitConstraint.swift @@ -32,7 +32,7 @@ public protocol HealthKitConstraint: Standard { /// - Parameter sample: The `HKSample` that should be added. func add(sample: HKSample) async - /// Removes a `HKDeletedObject` from the ``HealthKit`` module + /// Notifies the ``Standard`` about the removal of a HealthKit sample as defined by the `HKDeletedObject`. /// - Parameter sample: The `HKDeletedObject` is a sample that should be removed. func remove(sample: HKDeletedObject) async } From 577eb3bcc3e65fa08a6a7051878147581d22d984 Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Wed, 9 Aug 2023 14:48:29 -0700 Subject: [PATCH 14/16] Comments in ExampleStandard.swift Co-authored-by: Paul Schmiedmayer --- Tests/UITests/TestApp/ExampleStandard.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/UITests/TestApp/ExampleStandard.swift b/Tests/UITests/TestApp/ExampleStandard.swift index 2616dd3..3f53aff 100644 --- a/Tests/UITests/TestApp/ExampleStandard.swift +++ b/Tests/UITests/TestApp/ExampleStandard.swift @@ -11,7 +11,7 @@ import Spezi import SpeziHealthKit -/// an example Standard used for the configuration. +/// An example Standard used for the configuration. actor ExampleStandard: Standard, ObservableObject, ObservableObjectProvider { @Published @MainActor var addedResponses = [HKSample]() } From d1cea527bdd273e684fee5601f0aa42d582d2d71 Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Wed, 9 Aug 2023 15:08:52 -0700 Subject: [PATCH 15/16] Logging, adding comments and removing dev team --- .../HealthKitSampleDataSource.swift | 3 ++- .../SpeziHealthKit/HealthKitConstraint.swift | 2 +- .../Logging/Logger+HealthKit.swift | 18 ++++++++++++++++++ .../UITests/UITests.xcodeproj/project.pbxproj | 12 ++++++------ 4 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index 360ed42..8bbe76f 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -7,6 +7,7 @@ // import HealthKit +import OSLog import Spezi import SwiftUI @@ -118,7 +119,7 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } } } catch { - print(error) + Logger.healthKit.error("\(error.localizedDescription)") } } diff --git a/Sources/SpeziHealthKit/HealthKitConstraint.swift b/Sources/SpeziHealthKit/HealthKitConstraint.swift index f68657d..69c5276 100644 --- a/Sources/SpeziHealthKit/HealthKitConstraint.swift +++ b/Sources/SpeziHealthKit/HealthKitConstraint.swift @@ -28,7 +28,7 @@ import Spezi /// ``` /// public protocol HealthKitConstraint: Standard { - /// Adds a new `HKSample` to the ``HealthKit`` module + /// Notifies the ``Standard`` about the addition of a HealthKit ``HKSample`` sample instance. /// - Parameter sample: The `HKSample` that should be added. func add(sample: HKSample) async diff --git a/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift b/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift new file mode 100644 index 0000000..fcb2658 --- /dev/null +++ b/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift @@ -0,0 +1,18 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +import OSLog + +extension Logger { + /// Using the bundle identifier to ensure a unique identifier. + private static var subsystem = Bundle.main.bundleIdentifier! + + /// Logs the view cycles like a view that appeared. + static let healthKit = Logger(subsystem: subsystem, category: "healthkit") +} diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index d95183e..a6c0bb5 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -400,7 +400,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 64FJ2MWNP4; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -435,7 +435,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 64FJ2MWNP4; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -466,7 +466,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 64FJ2MWNP4; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testappuitests; @@ -485,7 +485,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 64FJ2MWNP4; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testappuitests; @@ -568,7 +568,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 64FJ2MWNP4; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -599,7 +599,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 64FJ2MWNP4; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testappuitests; From 11cf6f586be78e577685e76e85c6e424c51acbc7 Mon Sep 17 00:00:00 2001 From: Niall Kehoe Date: Wed, 9 Aug 2023 15:11:31 -0700 Subject: [PATCH 16/16] linter force_unwrapping error --- Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift b/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift index fcb2658..468f613 100644 --- a/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift +++ b/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift @@ -10,8 +10,10 @@ import OSLog extension Logger { + // swiftlint:disable force_unwrapping /// Using the bundle identifier to ensure a unique identifier. private static var subsystem = Bundle.main.bundleIdentifier! + // swiftlint:enable force_unwrapping /// Logs the view cycles like a view that appeared. static let healthKit = Logger(subsystem: subsystem, category: "healthkit")