From 46dcfde1ee73bcdb72b933446eb4a45438ce086a Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 5 Mar 2024 23:43:54 -0800 Subject: [PATCH 1/8] Update to Spezi 1.2.0 and improve backgroud sample delivery --- Package.swift | 2 +- README.md | 4 +-- .../CollectSample/CollectSample.swift | 2 +- .../HealthKitSampleDataSource.swift | 35 +++++++++---------- Sources/SpeziHealthKit/HealthKit.swift | 25 ++++++------- .../HealthKitDataSource.swift | 6 ++-- .../HealthKitDeliverySetting.swift | 4 +-- .../HealthKitDeliveryStartSetting.swift | 13 ++++++- .../SpeziHealthKit.docc/SpeziHealthKit.md | 4 +-- .../SpeziHealthKitTests.swift | 2 +- Tests/UITests/TestApp/TestAppDelegate.swift | 4 +-- 11 files changed, 54 insertions(+), 47 deletions(-) diff --git a/Package.swift b/Package.swift index ff2f284..c50aab0 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", from: "1.0.0") + .package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.0") ], targets: [ .target( diff --git a/README.md b/README.md index 424708c..d84308c 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ class ExampleAppDelegate: SpeziAppDelegate { ) CollectSample( HKQuantityType(.stepCount), - deliverySetting: .background(.afterAuthorizationAndApplicationWillLaunch) + deliverySetting: .background(.automatic) ) CollectSample( HKQuantityType(.pushCount), @@ -73,7 +73,7 @@ class ExampleAppDelegate: SpeziAppDelegate { ) CollectSample( HKQuantityType(.activeEnergyBurned), - deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch) + deliverySetting: .anchorQuery(.automatic) ) CollectSample( HKQuantityType(.restingHeartRate), diff --git a/Sources/SpeziHealthKit/CollectSample/CollectSample.swift b/Sources/SpeziHealthKit/CollectSample/CollectSample.swift index f9321b1..c51c411 100644 --- a/Sources/SpeziHealthKit/CollectSample/CollectSample.swift +++ b/Sources/SpeziHealthKit/CollectSample/CollectSample.swift @@ -40,7 +40,7 @@ import Spezi /// CollectSample( /// HKQuantityType(.stepCount), /// predicate: predicateOneMonth, -/// deliverySetting: .background(.afterAuthorizationAndApplicationWillLaunch) +/// deliverySetting: .background(.automatic) /// ) /// ``` public struct CollectSample: HealthKitDataSourceDescription { diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index fb645e1..025803b 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -80,27 +80,27 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } Task { - await triggerDataSourceCollection() + await triggerManualDataSourceCollection() } } - func willFinishLaunchingWithOptions() { + func startAutomaticDataCollection() { guard askedForAuthorization(for: sampleType) else { return } switch deliverySetting { - case let .anchorQuery(startSetting, _) where startSetting == .afterAuthorizationAndApplicationWillLaunch, - let .background(startSetting, _) where startSetting == .afterAuthorizationAndApplicationWillLaunch: + case let .anchorQuery(startSetting, _) where startSetting == .automatic, + let .background(startSetting, _) where startSetting == .automatic: Task { - await triggerDataSourceCollection() + await triggerManualDataSourceCollection() } default: break } } - func triggerDataSourceCollection() async { + func triggerManualDataSourceCollection() async { guard !active else { return } @@ -108,14 +108,15 @@ final class HealthKitSampleDataSource: HealthKitDataSource { do { switch deliverySetting { case .manual: - anchoredSingleObjectQuery() + try await anchoredSingleObjectQuery() case .anchorQuery: active = true try await anchoredContinuousObjectQuery() case .background: active = true + try await self.anchoredSingleObjectQuery() for try await _ in healthStore.startObservation(for: [sampleType], withPredicate: predicate) { - self.anchoredSingleObjectQuery() + try await self.anchoredSingleObjectQuery() } } } catch { @@ -124,16 +125,14 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } - private func anchoredSingleObjectQuery() { - Task { - let resultsAnchor = try await healthStore.anchoredSingleObjectQuery( - for: self.sampleType, - using: self.anchor, - withPredicate: predicate, - standard: self.standard - ) - self.anchor = resultsAnchor - } + private func anchoredSingleObjectQuery() async throws { + let resultsAnchor = try await healthStore.anchoredSingleObjectQuery( + for: self.sampleType, + using: self.anchor, + withPredicate: predicate, + standard: self.standard + ) + self.anchor = resultsAnchor } private func anchoredContinuousObjectQuery() async throws { diff --git a/Sources/SpeziHealthKit/HealthKit.swift b/Sources/SpeziHealthKit/HealthKit.swift index 85ef935..83b680b 100644 --- a/Sources/SpeziHealthKit/HealthKit.swift +++ b/Sources/SpeziHealthKit/HealthKit.swift @@ -46,7 +46,7 @@ import SwiftUI /// ) /// CollectSample( /// HKQuantityType(.stepCount), -/// deliverySetting: .background(.afterAuthorizationAndApplicationWillLaunch) +/// deliverySetting: .background(.automatic) /// ) /// CollectSample( /// HKQuantityType(.pushCount), @@ -54,7 +54,7 @@ import SwiftUI /// ) /// CollectSample( /// HKQuantityType(.activeEnergyBurned), -/// deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch) +/// deliverySetting: .anchorQuery(.automatic) /// ) /// CollectSample( /// HKQuantityType(.restingHeartRate), @@ -67,7 +67,7 @@ import SwiftUI /// } /// ``` @Observable -public final class HealthKit: Module, LifecycleHandler, EnvironmentAccessible, DefaultInitializable { +public final class HealthKit: Module, EnvironmentAccessible, DefaultInitializable { @ObservationIgnored @StandardActor private var standard: any HealthKitConstraint private let healthStore: HKHealthStore private var healthKitDataSourceDescriptions: [HealthKitDataSourceDescription] = [] @@ -132,6 +132,12 @@ public final class HealthKit: Module, LifecycleHandler, EnvironmentAccessible, D } + public func configure() { + for healthKitComponent in healthKitComponents { + healthKitComponent.startAutomaticDataCollection() + } + } + /// Displays the user interface to ask for authorization for all HealthKit data defined by the ``HealthKitDataSourceDescription``s. /// /// Call this function when you want to start HealthKit data collection. @@ -154,7 +160,7 @@ public final class HealthKit: Module, LifecycleHandler, EnvironmentAccessible, D healthKitDataSourceDescriptions.append(healthKitDataSourceDescription) let dataSources = healthKitDataSourceDescription.dataSources(healthStore: healthStore, standard: standard) for dataSource in dataSources { - dataSource.willFinishLaunchingWithOptions() + dataSource.startAutomaticDataCollection() } } @@ -164,21 +170,12 @@ public final class HealthKit: Module, LifecycleHandler, EnvironmentAccessible, D } } - - @_documentation(visibility: internal) - public func willFinishLaunchingWithOptions(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) { - for healthKitComponent in healthKitComponents { - healthKitComponent.willFinishLaunchingWithOptions() - } - } - - /// Triggers any ``HealthKitDeliverySetting/manual(safeAnchor:)`` collections and starts the collection for all ``HealthKitDeliveryStartSetting/manual`` HealthKit data collections. public func triggerDataSourceCollection() async { await withTaskGroup(of: Void.self) { group in for healthKitComponent in healthKitComponents { group.addTask { - await healthKitComponent.triggerDataSourceCollection() + await healthKitComponent.triggerManualDataSourceCollection() } } await group.waitForAll() diff --git a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSource.swift b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSource.swift index 3ca5343..68fd51f 100644 --- a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSource.swift +++ b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSource.swift @@ -16,9 +16,9 @@ public protocol HealthKitDataSource { /// Called after the used was asked for authorization. func askedForAuthorization() /// Called to trigger the manual data collection. - func triggerDataSourceCollection() async - /// Called when the application finished launching with options. - func willFinishLaunchingWithOptions() + func triggerManualDataSourceCollection() async + /// Called to start the automatic data collection. + func startAutomaticDataCollection() } diff --git a/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift b/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift index b4b58fe..33e3982 100644 --- a/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift +++ b/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift @@ -13,10 +13,10 @@ public enum HealthKitDeliverySetting: Equatable { case manual(safeAnchor: Bool = true) /// The HealthKit data is collected based on the `HealthKitDeliveryStartSetting` and constantly listens to updates while the application is running. /// If `safeAnchor` is enabled the `HKQueryAnchor` is persisted across multiple application launches using the user defaults. - case anchorQuery(HealthKitDeliveryStartSetting = .afterAuthorizationAndApplicationWillLaunch, saveAnchor: Bool = true) + case anchorQuery(HealthKitDeliveryStartSetting = .automatic, saveAnchor: Bool = true) /// The HealthKit data is collected based on the `HealthKitDeliveryStartSetting` and constantly listens to updates even if the application is not running. /// If `safeAnchor` is enabled the `HKQueryAnchor` is persisted across multiple application launches using the user defaults. - case background(HealthKitDeliveryStartSetting = .afterAuthorizationAndApplicationWillLaunch, saveAnchor: Bool = true) + case background(HealthKitDeliveryStartSetting = .automatic, saveAnchor: Bool = true) var saveAnchor: Bool { diff --git a/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliveryStartSetting.swift b/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliveryStartSetting.swift index c8937f7..fd0966c 100644 --- a/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliveryStartSetting.swift +++ b/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliveryStartSetting.swift @@ -13,6 +13,17 @@ public enum HealthKitDeliveryStartSetting: Equatable { case manual /// The delivery is started automatically after the user provided authorization and the application has launched. /// You can request authorization using the ``HealthKit/askForAuthorization()`` function. - case afterAuthorizationAndApplicationWillLaunch // swiftlint:disable:this identifier_name + case automatic + + + @available( + *, + deprecated, + message: + """ + Please use `.automatic`. + """ + ) + public static let afterAuthorizationAndApplicationWillLaunch: HealthKitDeliveryStartSetting = .automatic // swiftlint:disable:this identifier_name // We use a name longer than 40 characters to indicate the full depth of this setting. } diff --git a/Sources/SpeziHealthKit/SpeziHealthKit.docc/SpeziHealthKit.md b/Sources/SpeziHealthKit/SpeziHealthKit.docc/SpeziHealthKit.md index f61dcec..191c15c 100644 --- a/Sources/SpeziHealthKit/SpeziHealthKit.docc/SpeziHealthKit.md +++ b/Sources/SpeziHealthKit/SpeziHealthKit.docc/SpeziHealthKit.md @@ -58,7 +58,7 @@ class ExampleAppDelegate: SpeziAppDelegate { ) CollectSample( HKQuantityType(.stepCount), - deliverySetting: .background(.afterAuthorizationAndApplicationWillLaunch) + deliverySetting: .background(.automatic) ) CollectSample( HKQuantityType(.pushCount), @@ -66,7 +66,7 @@ class ExampleAppDelegate: SpeziAppDelegate { ) CollectSample( HKQuantityType(.activeEnergyBurned), - deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch) + deliverySetting: .anchorQuery(.automatic) ) CollectSample( HKQuantityType(.restingHeartRate), diff --git a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift index 681f119..0d1fdf0 100644 --- a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift +++ b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift @@ -20,7 +20,7 @@ final class SpeziHealthKitTests: XCTestCase { let healthKitModule = HealthKit { CollectSamples( collectedSamples, - deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch) + deliverySetting: .anchorQuery(.automatic) ) } diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index c7bb1b9..a85ddf8 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -20,7 +20,7 @@ class TestAppDelegate: SpeziAppDelegate { ) CollectSample( HKQuantityType(.stepCount), - deliverySetting: .background(.afterAuthorizationAndApplicationWillLaunch) + deliverySetting: .background(.automatic) ) CollectSample( HKQuantityType(.pushCount), @@ -28,7 +28,7 @@ class TestAppDelegate: SpeziAppDelegate { ) CollectSample( HKQuantityType(.activeEnergyBurned), - deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch) + deliverySetting: .anchorQuery(.automatic) ) CollectSample( HKQuantityType(.restingHeartRate), From 4fee3b357adf6be207e8a1887935d2b995bda935 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Thu, 7 Mar 2024 22:08:46 -0800 Subject: [PATCH 2/8] Update Setup --- .../HealthKitSampleDataSource.swift | 21 ++++-- .../HKHealthStore+Observations.swift | 65 +++++++++---------- Tests/UITests/TestApp/ExampleStandard.swift | 22 +++++++ Tests/UITests/TestApp/Info.plist | 10 +++ Tests/UITests/TestApp/TestApp.entitlements | 2 + Tests/UITests/TestApp/TestApp.swift | 3 + Tests/UITests/TestApp/TestAppDelegate.swift | 4 ++ .../UITests/UITests.xcodeproj/project.pbxproj | 12 +++- .../xcshareddata/xcschemes/TestApp.xcscheme | 7 +- 9 files changed, 103 insertions(+), 43 deletions(-) create mode 100644 Tests/UITests/TestApp/Info.plist diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index 025803b..08319fe 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -114,13 +114,26 @@ final class HealthKitSampleDataSource: HealthKitDataSource { try await anchoredContinuousObjectQuery() case .background: active = true - try await self.anchoredSingleObjectQuery() - for try await _ in healthStore.startObservation(for: [sampleType], withPredicate: predicate) { - try await self.anchoredSingleObjectQuery() + for try await sampleUpdate in try await healthStore.startBackgroundDelivery(for: [sampleType]) { + guard sampleUpdate.sampleTypes.contains(sampleType) else { + Logger.healthKit.warning("Recieved Observation query types (\(sampleUpdate.sampleTypes)) are not corresponding to the CollectSample type \(self.sampleType)") + sampleUpdate.observerQueryCompletionHandler() + continue + } + + do { + try await anchoredSingleObjectQuery() + Logger.healthKit.debug("Successfully processed background update for \(self.sampleType)") + } catch { + Logger.healthKit.error("Could not query samples in a background update for \(self.sampleType): \(error)") + } + + // Provide feedback to HealthKit that the data has been processed: https://developer.apple.com/documentation/healthkit/hkobserverquerycompletionhandler + sampleUpdate.observerQueryCompletionHandler() } } } catch { - Logger.healthKit.error("\(error.localizedDescription)") + Logger.healthKit.error("Could not Process HealthKit data collection: \(error.localizedDescription)") } } diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+Observations.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+Observations.swift index efb2b0e..f981278 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+Observations.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+Observations.swift @@ -7,6 +7,7 @@ // import HealthKit +import OSLog import Spezi @@ -15,48 +16,43 @@ extension HKHealthStore { private static let activeObservationsLock = NSLock() - func startObservation( + func startBackgroundDelivery( for sampleTypes: Set, withPredicate predicate: NSPredicate? = nil - ) -> AsyncThrowingStream<(Set, HKObserverQueryCompletionHandler), Error> { - AsyncThrowingStream { continuation in - Task { - do { - try await enableBackgroundDelivery(for: sampleTypes) - } catch { + ) async throws -> AsyncThrowingStream<(sampleTypes: Set, observerQueryCompletionHandler: HKObserverQueryCompletionHandler), Error> { + try await enableBackgroundDelivery(for: sampleTypes) + + return AsyncThrowingStream { continuation in + var queryDescriptors: [HKQueryDescriptor] = [] + for sampleType in sampleTypes { + queryDescriptors.append( + HKQueryDescriptor(sampleType: sampleType, predicate: predicate) + ) + } + + let observerQuery = HKObserverQuery(queryDescriptors: queryDescriptors) { query, samples, completionHandler, error in + guard error == nil, + let samples else { + Logger.healthKit.error("Failed HealthKit background delivery for observer query \(query) with error: \(error)") continuation.finish(throwing: error) + completionHandler() + return } - var queryDescriptors: [HKQueryDescriptor] = [] - for sampleType in sampleTypes { - queryDescriptors.append( - HKQueryDescriptor(sampleType: sampleType, predicate: predicate) - ) - } - - let observerQuery = HKObserverQuery(queryDescriptors: queryDescriptors) { _, samples, completionHandler, error in - guard error == nil, - let samples else { - continuation.finish(throwing: error) - completionHandler() - return - } - - continuation.yield((samples, completionHandler)) - } - - self.execute(observerQuery) - - continuation.onTermination = { @Sendable _ in - self.stop(observerQuery) - self.disableBackgroundDelivery(for: sampleTypes) - } + continuation.yield((samples, completionHandler)) + } + + self.execute(observerQuery) + + continuation.onTermination = { @Sendable _ in + self.stop(observerQuery) + self.disableBackgroundDelivery(for: sampleTypes) } } } - func enableBackgroundDelivery( + private func enableBackgroundDelivery( for objectTypes: Set, frequency: HKUpdateFrequency = .immediate ) async throws { @@ -72,13 +68,14 @@ extension HKHealthStore { } } } catch { + Logger.healthKit.error("Could not enable HealthKit Backgound access for \(objectTypes): \(error.localizedDescription)") // Revert all changes as enable background delivery for the object types failed. disableBackgroundDelivery(for: enabledObjectTypes) } } - func disableBackgroundDelivery( + private func disableBackgroundDelivery( for objectTypes: Set ) { for objectType in objectTypes { @@ -87,7 +84,7 @@ extension HKHealthStore { let newActiveObservation = activeObservation - 1 if newActiveObservation <= 0 { HKHealthStore.activeObservations[objectType] = nil - Task { + Task { @MainActor in try await self.disableBackgroundDelivery(for: objectType) } } else { diff --git a/Tests/UITests/TestApp/ExampleStandard.swift b/Tests/UITests/TestApp/ExampleStandard.swift index 899598a..f3ea038 100644 --- a/Tests/UITests/TestApp/ExampleStandard.swift +++ b/Tests/UITests/TestApp/ExampleStandard.swift @@ -7,17 +7,23 @@ // @preconcurrency import HealthKit +import OSLog import Spezi import SpeziHealthKit +import UserNotifications + @Observable private class ResponseList { var addedResponses = [HKSample]() } + /// An example Standard used for the configuration. actor ExampleStandard: Standard, EnvironmentAccessible { + private let logger = Logger(subsystem: "TestApp", category: "ExampleStandard") @MainActor private var responseList = ResponseList() + @MainActor var addedResponses: [HKSample] { _read { yield responseList.addedResponses @@ -33,6 +39,14 @@ extension ExampleStandard: HealthKitConstraint { func add(sample: HKSample) async { _Concurrency.Task { @MainActor in addedResponses.append(sample) + + logger.debug("Added sample: \(sample.debugDescription)") + + let content = UNMutableNotificationContent() + content.title = "Spezi HealthKit Test App" + content.body = "Successfully processed background add for \(sample.debugDescription)" + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + try await UNUserNotificationCenter.current().add(request) } } @@ -41,6 +55,14 @@ extension ExampleStandard: HealthKitConstraint { if let index = addedResponses.firstIndex(where: { $0.uuid == sample.uuid }) { addedResponses.remove(at: index) } + + logger.debug("Removed sample: \(sample.debugDescription)") + + let content = UNMutableNotificationContent() + content.title = "Spezi HealthKit Test App" + content.body = "Successfully processed background remove for \(sample.debugDescription)" + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + try await UNUserNotificationCenter.current().add(request) } } } diff --git a/Tests/UITests/TestApp/Info.plist b/Tests/UITests/TestApp/Info.plist new file mode 100644 index 0000000..d99cf67 --- /dev/null +++ b/Tests/UITests/TestApp/Info.plist @@ -0,0 +1,10 @@ + + + + + UIBackgroundModes + + fetch + + + diff --git a/Tests/UITests/TestApp/TestApp.entitlements b/Tests/UITests/TestApp/TestApp.entitlements index 2ab14a2..dab226c 100644 --- a/Tests/UITests/TestApp/TestApp.entitlements +++ b/Tests/UITests/TestApp/TestApp.entitlements @@ -6,5 +6,7 @@ com.apple.developer.healthkit.access + com.apple.developer.healthkit.background-delivery + diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index 8fdf998..b72f4e7 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -21,6 +21,9 @@ struct UITestsApp: App { HealthKitTestsView() .navigationTitle("HealthKit") .spezi(appDelegate) + .task { + _ = try? await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) + } } } } diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index a85ddf8..7faf143 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -34,6 +34,10 @@ class TestAppDelegate: SpeziAppDelegate { HKQuantityType(.restingHeartRate), deliverySetting: .manual() ) + CollectSample( + HKQuantityType(.vo2Max), + deliverySetting: .background(.automatic) + ) } } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index b55ae06..9d16b8c 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 2F6D139228F5F384007C25D6 /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2F6D139928F5F386007C25D6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2F7CCC6B2B9A652800BAA489 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 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 = ""; }; @@ -87,6 +88,7 @@ 2F6D139428F5F384007C25D6 /* TestApp */ = { isa = PBXGroup; children = ( + 2F7CCC6B2B9A652800BAA489 /* Info.plist */, 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, 2F85827229E776AC0021D637 /* TestAppDelegate.swift */, 390F29602A785A98000A236E /* ExampleStandard.swift */, @@ -166,7 +168,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1410; - LastUpgradeCheck = 1410; + LastUpgradeCheck = 1530; TargetAttributes = { 2F6D139128F5F384007C25D6 = { CreatedOnToolsVersion = 14.1; @@ -274,6 +276,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -334,6 +337,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -394,10 +398,11 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 637867499T; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TestApp/Info.plist; INFOPLIST_KEY_NSHealthShareUsageDescription = "The TestApp accesses your HealthKit data to run the tests."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -433,6 +438,7 @@ ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TestApp/Info.plist; INFOPLIST_KEY_NSHealthShareUsageDescription = "The TestApp accesses your HealthKit data to run the tests."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -496,6 +502,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -566,6 +573,7 @@ ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TestApp/Info.plist; INFOPLIST_KEY_NSHealthShareUsageDescription = "The TestApp accesses your HealthKit data to run the tests."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme index b8e2be0..cc3d715 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme @@ -1,7 +1,7 @@ + LastUpgradeVersion = "1530" + version = "2.0"> @@ -69,7 +69,8 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" - allowLocationSimulation = "YES"> + allowLocationSimulation = "YES" + launchAutomaticallySubstyle = "1"> Date: Fri, 8 Mar 2024 09:29:12 -0800 Subject: [PATCH 3/8] Update Code --- .../HealthKitSampleDataSource.swift | 36 ++++++---- .../HKHealthStore+Observations.swift | 47 ++++++------- .../TestApp/Array+RawRepresentable.swift | 30 ++++++++ Tests/UITests/TestApp/ExampleStandard.swift | 68 ------------------- Tests/UITests/TestApp/HealthKitStore.swift | 65 ++++++++++++++++++ .../TestApp/HealthKitTestAppStandard.swift | 26 +++++++ .../UITests/TestApp/HealthKitTestsView.swift | 34 ++++------ Tests/UITests/TestApp/TestAppDelegate.swift | 6 +- .../UITests/UITests.xcodeproj/project.pbxproj | 33 +++++++-- 9 files changed, 205 insertions(+), 140 deletions(-) create mode 100644 Tests/UITests/TestApp/Array+RawRepresentable.swift delete mode 100644 Tests/UITests/TestApp/ExampleStandard.swift create mode 100644 Tests/UITests/TestApp/HealthKitStore.swift create mode 100644 Tests/UITests/TestApp/HealthKitTestAppStandard.swift diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index 08319fe..112b488 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -114,22 +114,28 @@ final class HealthKitSampleDataSource: HealthKitDataSource { try await anchoredContinuousObjectQuery() case .background: active = true - for try await sampleUpdate in try await healthStore.startBackgroundDelivery(for: [sampleType]) { - guard sampleUpdate.sampleTypes.contains(sampleType) else { - Logger.healthKit.warning("Recieved Observation query types (\(sampleUpdate.sampleTypes)) are not corresponding to the CollectSample type \(self.sampleType)") - sampleUpdate.observerQueryCompletionHandler() - continue + try await healthStore.startBackgroundDelivery(for: [sampleType]) { result in + Task { + guard case let .success((sampleTypes, completionHandler)) = result else { + return + } + + guard sampleTypes.contains(self.sampleType) else { + Logger.healthKit.warning("Recieved Observation query types (\(sampleTypes)) are not corresponding to the CollectSample type \(self.sampleType)") + completionHandler() + return + } + + do { + try await self.anchoredSingleObjectQuery() + Logger.healthKit.debug("Successfully processed background update for \(self.sampleType)") + } catch { + Logger.healthKit.error("Could not query samples in a background update for \(self.sampleType): \(error)") + } + + // Provide feedback to HealthKit that the data has been processed: https://developer.apple.com/documentation/healthkit/hkobserverquerycompletionhandler + completionHandler() } - - do { - try await anchoredSingleObjectQuery() - Logger.healthKit.debug("Successfully processed background update for \(self.sampleType)") - } catch { - Logger.healthKit.error("Could not query samples in a background update for \(self.sampleType): \(error)") - } - - // Provide feedback to HealthKit that the data has been processed: https://developer.apple.com/documentation/healthkit/hkobserverquerycompletionhandler - sampleUpdate.observerQueryCompletionHandler() } } } catch { diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+Observations.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+Observations.swift index f981278..4ef505f 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+Observations.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+Observations.swift @@ -18,37 +18,30 @@ extension HKHealthStore { func startBackgroundDelivery( for sampleTypes: Set, - withPredicate predicate: NSPredicate? = nil - ) async throws -> AsyncThrowingStream<(sampleTypes: Set, observerQueryCompletionHandler: HKObserverQueryCompletionHandler), Error> { - try await enableBackgroundDelivery(for: sampleTypes) + withPredicate predicate: NSPredicate? = nil, + observerQuery: @escaping (Result<(sampleTypes: Set, completionHandler: HKObserverQueryCompletionHandler), Error>) -> Void + ) async throws { + var queryDescriptors: [HKQueryDescriptor] = [] + for sampleType in sampleTypes { + queryDescriptors.append( + HKQueryDescriptor(sampleType: sampleType, predicate: predicate) + ) + } - return AsyncThrowingStream { continuation in - var queryDescriptors: [HKQueryDescriptor] = [] - for sampleType in sampleTypes { - queryDescriptors.append( - HKQueryDescriptor(sampleType: sampleType, predicate: predicate) - ) - } - - let observerQuery = HKObserverQuery(queryDescriptors: queryDescriptors) { query, samples, completionHandler, error in - guard error == nil, - let samples else { - Logger.healthKit.error("Failed HealthKit background delivery for observer query \(query) with error: \(error)") - continuation.finish(throwing: error) - completionHandler() - return - } - - continuation.yield((samples, completionHandler)) + let observerQuery = HKObserverQuery(queryDescriptors: queryDescriptors) { query, samples, completionHandler, error in + guard error == nil, + let samples else { + Logger.healthKit.error("Failed HealthKit background delivery for observer query \(query) with error: \(error)") + observerQuery(.failure(error ?? NSError(domain: "Spezi HealthKit", code: -1))) + completionHandler() + return } - self.execute(observerQuery) - - continuation.onTermination = { @Sendable _ in - self.stop(observerQuery) - self.disableBackgroundDelivery(for: sampleTypes) - } + observerQuery(.success((samples, completionHandler))) } + + self.execute(observerQuery) + try await enableBackgroundDelivery(for: sampleTypes) } diff --git a/Tests/UITests/TestApp/Array+RawRepresentable.swift b/Tests/UITests/TestApp/Array+RawRepresentable.swift new file mode 100644 index 0000000..910b541 --- /dev/null +++ b/Tests/UITests/TestApp/Array+RawRepresentable.swift @@ -0,0 +1,30 @@ +// +// 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 Foundation + + +extension Array: RawRepresentable { + public var rawValue: String { + guard let data = try? JSONEncoder().encode(self), + let result = String(data: data, encoding: .utf8) + else { + return "[]" + } + return result + } + + public init?(rawValue: String) { + guard let data = rawValue.data(using: .utf8), + let result = try? JSONDecoder().decode(Self.self, from: data) + else { + return nil + } + self = result + } +} diff --git a/Tests/UITests/TestApp/ExampleStandard.swift b/Tests/UITests/TestApp/ExampleStandard.swift deleted file mode 100644 index f3ea038..0000000 --- a/Tests/UITests/TestApp/ExampleStandard.swift +++ /dev/null @@ -1,68 +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 OSLog -import Spezi -import SpeziHealthKit -import UserNotifications - - -@Observable -private class ResponseList { - var addedResponses = [HKSample]() -} - - -/// An example Standard used for the configuration. -actor ExampleStandard: Standard, EnvironmentAccessible { - private let logger = Logger(subsystem: "TestApp", category: "ExampleStandard") - @MainActor private var responseList = ResponseList() - - @MainActor var addedResponses: [HKSample] { - _read { - yield responseList.addedResponses - } - _modify { - yield &responseList.addedResponses - } - } -} - - -extension ExampleStandard: HealthKitConstraint { - func add(sample: HKSample) async { - _Concurrency.Task { @MainActor in - addedResponses.append(sample) - - logger.debug("Added sample: \(sample.debugDescription)") - - let content = UNMutableNotificationContent() - content.title = "Spezi HealthKit Test App" - content.body = "Successfully processed background add for \(sample.debugDescription)" - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) - try await UNUserNotificationCenter.current().add(request) - } - } - - func remove(sample: HKDeletedObject) async { - _Concurrency.Task { @MainActor in - if let index = addedResponses.firstIndex(where: { $0.uuid == sample.uuid }) { - addedResponses.remove(at: index) - } - - logger.debug("Removed sample: \(sample.debugDescription)") - - let content = UNMutableNotificationContent() - content.title = "Spezi HealthKit Test App" - content.body = "Successfully processed background remove for \(sample.debugDescription)" - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) - try await UNUserNotificationCenter.current().add(request) - } - } -} diff --git a/Tests/UITests/TestApp/HealthKitStore.swift b/Tests/UITests/TestApp/HealthKitStore.swift new file mode 100644 index 0000000..2463bdc --- /dev/null +++ b/Tests/UITests/TestApp/HealthKitStore.swift @@ -0,0 +1,65 @@ +// +// 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 Foundation +@preconcurrency import HealthKit +import OSLog +import Spezi +import UserNotifications + + +@Observable +class HealthKitStore: Module, DefaultInitializable { + private enum StorageKeys { + static let backgroundPersistance = "HealthKitStore.backgroundPersistance" + } + + private let logger = Logger(subsystem: "TestApp", category: "ExampleStandard") + + private(set) var samples: [HKSample] = [] + private(set) var backgroundPersistance: [String] { + didSet { + UserDefaults.standard.setValue(backgroundPersistance.rawValue, forKey: StorageKeys.backgroundPersistance) + } + } + + required init() { + backgroundPersistance = UserDefaults.standard.string(forKey: StorageKeys.backgroundPersistance).flatMap { [String].init(rawValue: $0) } ?? [] + } + + + func add(sample: HKSample) async { + samples.append(sample) + + logger.debug("Added sample: \(sample.debugDescription)") + + backgroundPersistance.append("Added sample \(sample.sampleType.description) (\(sample.uuid.uuidString) at \(Date.now.formatted(date: .numeric, time: .complete))") + + let content = UNMutableNotificationContent() + content.title = "Spezi HealthKit Test App" + content.body = "Added sample \(sample.sampleType.description) (\(sample.uuid.uuidString) at \(Date.now.formatted(date: .numeric, time: .complete))" + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + try? await UNUserNotificationCenter.current().add(request) + } + + func remove(sample: HKDeletedObject) async { + if let index = samples.firstIndex(where: { $0.uuid == sample.uuid }) { + samples.remove(at: index) + } + + logger.debug("Removed sample: \(sample.debugDescription)") + + backgroundPersistance.append("Removed sample: \(sample.uuid) at \(Date.now.formatted(date: .numeric, time: .complete))") + + let content = UNMutableNotificationContent() + content.title = "Spezi HealthKit Test App" + content.body = "Removed sample: \(sample.uuid) at \(Date.now.formatted(date: .numeric, time: .complete))" + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + try? await UNUserNotificationCenter.current().add(request) + } +} diff --git a/Tests/UITests/TestApp/HealthKitTestAppStandard.swift b/Tests/UITests/TestApp/HealthKitTestAppStandard.swift new file mode 100644 index 0000000..77c29a7 --- /dev/null +++ b/Tests/UITests/TestApp/HealthKitTestAppStandard.swift @@ -0,0 +1,26 @@ +// +// 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 + + +/// An example Standard used for the configuration. +actor HealthKitTestAppStandard: Standard, HealthKitConstraint { + @Dependency private var healthKitStore: HealthKitStore + + + func add(sample: HKSample) async { + await healthKitStore.add(sample: sample) + } + + func remove(sample: HKDeletedObject) async { + await healthKitStore.remove(sample: sample) + } +} diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift index 47d61ec..43db41d 100644 --- a/Tests/UITests/TestApp/HealthKitTestsView.swift +++ b/Tests/UITests/TestApp/HealthKitTestsView.swift @@ -7,40 +7,32 @@ // import SpeziHealthKit +import SpeziViews import SwiftUI struct HealthKitTestsView: View { @Environment(HealthKit.self) var healthKitModule - @Environment(ExampleStandard.self) var standard + @Environment(HealthKitStore.self) var healthKitStore var body: some View { - Button("Ask for authorization") { - askForAuthorization() + AsyncButton("Ask for authorization") { + try? await healthKitModule.askForAuthorization() } .disabled(healthKitModule.authorized) - Button("Trigger data source collection") { - triggerDataSourceCollection() + AsyncButton("Trigger data source collection") { + await healthKitModule.triggerDataSourceCollection() } - HStack { - List(standard.addedResponses, id: \.self) { element in + VStack { + List(healthKitStore.backgroundPersistance, id: \.self) { element in + Text(element) + .multilineTextAlignment(.leading) + .lineLimit(10) + } + List(healthKitStore.samples, id: \.self) { element in Text(element.sampleType.identifier) } } } - - @MainActor - private func askForAuthorization() { - Task { - try await healthKitModule.askForAuthorization() - } - } - - @MainActor - private func triggerDataSourceCollection() { - Task { - await healthKitModule.triggerDataSourceCollection() - } - } } diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index 7faf143..3324bbc 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -12,7 +12,7 @@ import SpeziHealthKit class TestAppDelegate: SpeziAppDelegate { override var configuration: Configuration { - Configuration(standard: ExampleStandard()) { + Configuration(standard: HealthKitTestAppStandard()) { HealthKit { CollectSample( HKQuantityType.electrocardiogramType(), @@ -34,10 +34,6 @@ class TestAppDelegate: SpeziAppDelegate { HKQuantityType(.restingHeartRate), deliverySetting: .manual() ) - CollectSample( - HKQuantityType(.vo2Max), - deliverySetting: .background(.automatic) - ) } } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 9d16b8c..b596f63 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -14,7 +14,10 @@ 2F85827329E776AC0021D637 /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F85827229E776AC0021D637 /* TestAppDelegate.swift */; }; 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 */; }; + 2FE33CB42B9B802200BD886D /* HealthKitStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE33CB32B9B802200BD886D /* HealthKitStore.swift */; }; + 2FE33CB62B9B804700BD886D /* Array+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE33CB52B9B804700BD886D /* Array+RawRepresentable.swift */; }; + 2FE33CB92B9B815B00BD886D /* SpeziViews in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE33CB82B9B815B00BD886D /* SpeziViews */; }; + 390F29612A785A98000A236E /* HealthKitTestAppStandard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390F29602A785A98000A236E /* HealthKitTestAppStandard.swift */; }; 97B029102A5710C800946EF8 /* XCTHealthKit in Frameworks */ = {isa = PBXBuildFile; productRef = 97B0290F2A5710C800946EF8 /* XCTHealthKit */; }; /* End PBXBuildFile section */ @@ -40,7 +43,9 @@ 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 = ""; }; + 2FE33CB32B9B802200BD886D /* HealthKitStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitStore.swift; sourceTree = ""; }; + 2FE33CB52B9B804700BD886D /* Array+RawRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+RawRepresentable.swift"; sourceTree = ""; }; + 390F29602A785A98000A236E /* HealthKitTestAppStandard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitTestAppStandard.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,6 +54,7 @@ buildActionMask = 2147483647; files = ( 2F61BDC329DD02D600D71D33 /* SpeziHealthKit in Frameworks */, + 2FE33CB92B9B815B00BD886D /* SpeziViews in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -91,8 +97,10 @@ 2F7CCC6B2B9A652800BAA489 /* Info.plist */, 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, 2F85827229E776AC0021D637 /* TestAppDelegate.swift */, - 390F29602A785A98000A236E /* ExampleStandard.swift */, + 390F29602A785A98000A236E /* HealthKitTestAppStandard.swift */, + 2FE33CB32B9B802200BD886D /* HealthKitStore.swift */, 2F85827029E776780021D637 /* HealthKitTestsView.swift */, + 2FE33CB52B9B804700BD886D /* Array+RawRepresentable.swift */, 2F85828329E77C4A0021D637 /* TestApp.entitlements */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, ); @@ -133,6 +141,7 @@ name = TestApp; packageProductDependencies = ( 2F61BDC229DD02D600D71D33 /* SpeziHealthKit */, + 2FE33CB82B9B815B00BD886D /* SpeziViews */, ); productName = Example; productReference = 2F6D139228F5F384007C25D6 /* TestApp.app */; @@ -191,6 +200,7 @@ packageReferences = ( 2F85827B29E778110021D637 /* XCRemoteSwiftPackageReference "XCTestExtensions" */, 97B0290E2A5710C800946EF8 /* XCRemoteSwiftPackageReference "XCTHealthKit" */, + 2FE33CB72B9B815B00BD886D /* XCRemoteSwiftPackageReference "SpeziViews" */, ); productRefGroup = 2F6D139328F5F384007C25D6 /* Products */; projectDirPath = ""; @@ -246,10 +256,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2FE33CB42B9B802200BD886D /* HealthKitStore.swift in Sources */, 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, - 390F29612A785A98000A236E /* ExampleStandard.swift in Sources */, + 390F29612A785A98000A236E /* HealthKitTestAppStandard.swift in Sources */, 2F85827129E776780021D637 /* HealthKitTestsView.swift in Sources */, 2F85827329E776AC0021D637 /* TestAppDelegate.swift in Sources */, + 2FE33CB62B9B804700BD886D /* Array+RawRepresentable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -658,6 +670,14 @@ minimumVersion = 0.4.6; }; }; + 2FE33CB72B9B815B00BD886D /* XCRemoteSwiftPackageReference "SpeziViews" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.1; + }; + }; 97B0290E2A5710C800946EF8 /* XCRemoteSwiftPackageReference "XCTHealthKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordBDHG/XCTHealthKit"; @@ -678,6 +698,11 @@ package = 2F85827B29E778110021D637 /* XCRemoteSwiftPackageReference "XCTestExtensions" */; productName = XCTestExtensions; }; + 2FE33CB82B9B815B00BD886D /* SpeziViews */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE33CB72B9B815B00BD886D /* XCRemoteSwiftPackageReference "SpeziViews" */; + productName = SpeziViews; + }; 97B0290F2A5710C800946EF8 /* XCTHealthKit */ = { isa = XCSwiftPackageProductDependency; package = 97B0290E2A5710C800946EF8 /* XCRemoteSwiftPackageReference "XCTHealthKit" */; From e412b6ad2367fe495dcdd777b437e38643e3f025 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 8 Mar 2024 09:55:38 -0800 Subject: [PATCH 4/8] Update TestApp --- .../HealthKitSampleDataSource.swift | 26 ++++++++------- ...=> HKHealthStore+BackgroundDelivery.swift} | 0 Tests/UITests/TestApp/HealthKitStore.swift | 12 +++++-- .../UITests/TestApp/HealthKitTestsView.swift | 32 +++++++++++-------- Tests/UITests/TestApp/TestApp.swift | 3 -- Tests/UITests/TestApp/TestAppDelegate.swift | 8 ++--- .../xcshareddata/xcschemes/TestApp.xcscheme | 5 ++- 7 files changed, 47 insertions(+), 39 deletions(-) rename Sources/SpeziHealthKit/HealthKit Extensions/{HKHealthStore+Observations.swift => HKHealthStore+BackgroundDelivery.swift} (100%) diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index 112b488..5638f8d 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -161,19 +161,21 @@ final class HealthKitSampleDataSource: HealthKitDataSource { let updateQueue = anchorDescriptor.results(for: healthStore) - for try await results in updateQueue { - if Task.isCancelled { - return - } - - for deletedObject in results.deletedObjects { - await standard.remove(sample: deletedObject) - } - - for addedSample in results.addedSamples { - await standard.add(sample: addedSample) + Task { + for try await results in updateQueue { + if Task.isCancelled { + return + } + + for deletedObject in results.deletedObjects { + await standard.remove(sample: deletedObject) + } + + for addedSample in results.addedSamples { + await standard.add(sample: addedSample) + } + self.anchor = results.newAnchor } - self.anchor = results.newAnchor } } diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+Observations.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift similarity index 100% rename from Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+Observations.swift rename to Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift diff --git a/Tests/UITests/TestApp/HealthKitStore.swift b/Tests/UITests/TestApp/HealthKitStore.swift index 2463bdc..ba00603 100644 --- a/Tests/UITests/TestApp/HealthKitStore.swift +++ b/Tests/UITests/TestApp/HealthKitStore.swift @@ -14,7 +14,7 @@ import UserNotifications @Observable -class HealthKitStore: Module, DefaultInitializable { +class HealthKitStore: Module, DefaultInitializable, EnvironmentAccessible { private enum StorageKeys { static let backgroundPersistance = "HealthKitStore.backgroundPersistance" } @@ -33,16 +33,22 @@ class HealthKitStore: Module, DefaultInitializable { } + func configure() { + Task { + try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) + } + } + func add(sample: HKSample) async { samples.append(sample) logger.debug("Added sample: \(sample.debugDescription)") - backgroundPersistance.append("Added sample \(sample.sampleType.description) (\(sample.uuid.uuidString) at \(Date.now.formatted(date: .numeric, time: .complete))") + backgroundPersistance.append("Added sample \(sample.sampleType.description) (\(sample.uuid.uuidString)) at \(Date.now.formatted(date: .numeric, time: .complete)): \((sample as? HKQuantitySample)?.quantity.description ?? "Unknown")") let content = UNMutableNotificationContent() content.title = "Spezi HealthKit Test App" - content.body = "Added sample \(sample.sampleType.description) (\(sample.uuid.uuidString) at \(Date.now.formatted(date: .numeric, time: .complete))" + content.body = "Added sample \(sample.sampleType.description) (\(sample.uuid.uuidString) at \(Date.now.formatted(date: .numeric, time: .complete)): \((sample as? HKQuantitySample)?.quantity.description ?? "Unknown")" let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) try? await UNUserNotificationCenter.current().add(request) } diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift index 43db41d..7738b9f 100644 --- a/Tests/UITests/TestApp/HealthKitTestsView.swift +++ b/Tests/UITests/TestApp/HealthKitTestsView.swift @@ -17,21 +17,25 @@ struct HealthKitTestsView: View { var body: some View { - AsyncButton("Ask for authorization") { - try? await healthKitModule.askForAuthorization() - } - .disabled(healthKitModule.authorized) - AsyncButton("Trigger data source collection") { - await healthKitModule.triggerDataSourceCollection() - } - VStack { - List(healthKitStore.backgroundPersistance, id: \.self) { element in - Text(element) - .multilineTextAlignment(.leading) - .lineLimit(10) + List { + AsyncButton("Ask for authorization") { + try? await healthKitModule.askForAuthorization() + } + .disabled(healthKitModule.authorized) + AsyncButton("Trigger data source collection") { + await healthKitModule.triggerDataSourceCollection() + } + Section("Collected Samples Since App Launch") { + ForEach(healthKitStore.samples, id: \.self) { element in + Text(element.sampleType.identifier) + } } - List(healthKitStore.samples, id: \.self) { element in - Text(element.sampleType.identifier) + Section("Background Persistance Log") { + ForEach(healthKitStore.backgroundPersistance, id: \.self) { element in + Text(element) + .multilineTextAlignment(.leading) + .lineLimit(10) + } } } } diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index b72f4e7..8fdf998 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -21,9 +21,6 @@ struct UITestsApp: App { HealthKitTestsView() .navigationTitle("HealthKit") .spezi(appDelegate) - .task { - _ = try? await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) - } } } } diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index 3324bbc..a2a3977 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -14,11 +14,11 @@ class TestAppDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration(standard: HealthKitTestAppStandard()) { HealthKit { - CollectSample( + CollectSample( // HKQuantityType.electrocardiogramType(), deliverySetting: .background(.manual) ) - CollectSample( + CollectSample( // HKQuantityType(.stepCount), deliverySetting: .background(.automatic) ) @@ -26,11 +26,11 @@ class TestAppDelegate: SpeziAppDelegate { HKQuantityType(.pushCount), deliverySetting: .anchorQuery(.manual) ) - CollectSample( + CollectSample( // HKQuantityType(.activeEnergyBurned), deliverySetting: .anchorQuery(.automatic) ) - CollectSample( + CollectSample( // HKQuantityType(.restingHeartRate), deliverySetting: .manual() ) diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme index cc3d715..e927fa4 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> @@ -69,8 +69,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" - allowLocationSimulation = "YES" - launchAutomaticallySubstyle = "1"> + allowLocationSimulation = "YES"> Date: Fri, 8 Mar 2024 10:05:20 -0800 Subject: [PATCH 5/8] Update Project Permissions --- Tests/UITests/TestApp/Info.plist | 10 ---------- Tests/UITests/UITests.xcodeproj/project.pbxproj | 9 ++------- 2 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 Tests/UITests/TestApp/Info.plist diff --git a/Tests/UITests/TestApp/Info.plist b/Tests/UITests/TestApp/Info.plist deleted file mode 100644 index d99cf67..0000000 --- a/Tests/UITests/TestApp/Info.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - UIBackgroundModes - - fetch - - - diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index b596f63..c293ea3 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -36,15 +36,14 @@ 2F6D139228F5F384007C25D6 /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2F6D139928F5F386007C25D6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 2F7CCC6B2B9A652800BAA489 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 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 = ""; }; - 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 = ""; }; 2FE33CB32B9B802200BD886D /* HealthKitStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitStore.swift; sourceTree = ""; }; 2FE33CB52B9B804700BD886D /* Array+RawRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+RawRepresentable.swift"; sourceTree = ""; }; + 2FE33CBA2B9B8A0200BD886D /* TestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TestApp.entitlements; sourceTree = ""; }; 390F29602A785A98000A236E /* HealthKitTestAppStandard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitTestAppStandard.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -94,14 +93,13 @@ 2F6D139428F5F384007C25D6 /* TestApp */ = { isa = PBXGroup; children = ( - 2F7CCC6B2B9A652800BAA489 /* Info.plist */, + 2FE33CBA2B9B8A0200BD886D /* TestApp.entitlements */, 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, 2F85827229E776AC0021D637 /* TestAppDelegate.swift */, 390F29602A785A98000A236E /* HealthKitTestAppStandard.swift */, 2FE33CB32B9B802200BD886D /* HealthKitStore.swift */, 2F85827029E776780021D637 /* HealthKitTestsView.swift */, 2FE33CB52B9B804700BD886D /* Array+RawRepresentable.swift */, - 2F85828329E77C4A0021D637 /* TestApp.entitlements */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, ); path = TestApp; @@ -414,7 +412,6 @@ ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = TestApp/Info.plist; INFOPLIST_KEY_NSHealthShareUsageDescription = "The TestApp accesses your HealthKit data to run the tests."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -450,7 +447,6 @@ ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = TestApp/Info.plist; INFOPLIST_KEY_NSHealthShareUsageDescription = "The TestApp accesses your HealthKit data to run the tests."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -585,7 +581,6 @@ ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = TestApp/Info.plist; INFOPLIST_KEY_NSHealthShareUsageDescription = "The TestApp accesses your HealthKit data to run the tests."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; From 8cb66fefd6082d5afe16b6ecdd683cf8dd5d7a77 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 8 Mar 2024 10:14:00 -0800 Subject: [PATCH 6/8] Update SwiftLint Configuration --- .swiftlint.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.swiftlint.yml b/.swiftlint.yml index e605657..8be1169 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -372,6 +372,8 @@ deployment_target: # Availability checks or attributes shouldn’t be using olde excluded: # paths to ignore during linting. Takes precedence over `included`. - .build - .swiftpm + - .derivedData + - Tests/UITests/.derivedData closure_body_length: # Closure bodies should not span too many lines. - 35 # warning - default: 20 From 245902f78d20c64cd5e5aadb919aacacb6dd2f18 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 8 Mar 2024 10:55:31 -0800 Subject: [PATCH 7/8] Improve TestApp --- Tests/UITests/TestApp/HealthKitStore.swift | 19 +++++++++++++++---- .../UITests/TestApp/HealthKitTestsView.swift | 12 +++++++----- .../TestAppUITests/SpeziHealthKitTests.swift | 1 + .../UITests/UITests.xcodeproj/project.pbxproj | 6 +++--- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Tests/UITests/TestApp/HealthKitStore.swift b/Tests/UITests/TestApp/HealthKitStore.swift index ba00603..80263c4 100644 --- a/Tests/UITests/TestApp/HealthKitStore.swift +++ b/Tests/UITests/TestApp/HealthKitStore.swift @@ -19,23 +19,34 @@ class HealthKitStore: Module, DefaultInitializable, EnvironmentAccessible { static let backgroundPersistance = "HealthKitStore.backgroundPersistance" } + static let collectedSamplesOnly = CommandLine.arguments.contains("--collectedSamplesOnly") + private let logger = Logger(subsystem: "TestApp", category: "ExampleStandard") private(set) var samples: [HKSample] = [] private(set) var backgroundPersistance: [String] { didSet { - UserDefaults.standard.setValue(backgroundPersistance.rawValue, forKey: StorageKeys.backgroundPersistance) + if !HealthKitStore.collectedSamplesOnly { + UserDefaults.standard.setValue(backgroundPersistance.rawValue, forKey: StorageKeys.backgroundPersistance) + } } } required init() { - backgroundPersistance = UserDefaults.standard.string(forKey: StorageKeys.backgroundPersistance).flatMap { [String].init(rawValue: $0) } ?? [] + if !HealthKitStore.collectedSamplesOnly { + backgroundPersistance = UserDefaults.standard.string(forKey: StorageKeys.backgroundPersistance) + .flatMap { [String].init(rawValue: $0) } ?? [] + } else { + backgroundPersistance = [] + } } func configure() { - Task { - try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) + if !HealthKitStore.collectedSamplesOnly { + Task { + try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) + } } } diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift index 7738b9f..3cd0981 100644 --- a/Tests/UITests/TestApp/HealthKitTestsView.swift +++ b/Tests/UITests/TestApp/HealthKitTestsView.swift @@ -30,11 +30,13 @@ struct HealthKitTestsView: View { Text(element.sampleType.identifier) } } - Section("Background Persistance Log") { - ForEach(healthKitStore.backgroundPersistance, id: \.self) { element in - Text(element) - .multilineTextAlignment(.leading) - .lineLimit(10) + if !HealthKitStore.collectedSamplesOnly { + Section("Background Persistance Log") { + ForEach(healthKitStore.backgroundPersistance, id: \.self) { element in + Text(element) + .multilineTextAlignment(.leading) + .lineLimit(10) + } } } } diff --git a/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift b/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift index fb2d6d0..a84013f 100644 --- a/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift @@ -14,6 +14,7 @@ import XCTHealthKit final class HealthKitTests: XCTestCase { func testHealthKit() throws { // swiftlint:disable:this function_body_length let app = XCUIApplication() + app.launchArguments = ["--collectedSamplesOnly"] app.deleteAndLaunch(withSpringboardAppName: "TestApp") try exitAppAndOpenHealth(.electrocardiograms) diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index c293ea3..ea86946 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -474,7 +474,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 637867499T; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testappuitests; @@ -577,7 +577,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 637867499T; ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -608,7 +608,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 637867499T; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.healthkit.testappuitests; From 7463a86cd575d39bb1ad2bd6076b7d008bfba49a Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 8 Mar 2024 11:30:43 -0800 Subject: [PATCH 8/8] Improve Concurrent Access --- Tests/UITests/TestApp/HealthKitStore.swift | 2 ++ Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Tests/UITests/TestApp/HealthKitStore.swift b/Tests/UITests/TestApp/HealthKitStore.swift index 80263c4..5237756 100644 --- a/Tests/UITests/TestApp/HealthKitStore.swift +++ b/Tests/UITests/TestApp/HealthKitStore.swift @@ -50,6 +50,7 @@ class HealthKitStore: Module, DefaultInitializable, EnvironmentAccessible { } } + @MainActor func add(sample: HKSample) async { samples.append(sample) @@ -64,6 +65,7 @@ class HealthKitStore: Module, DefaultInitializable, EnvironmentAccessible { try? await UNUserNotificationCenter.current().add(request) } + @MainActor func remove(sample: HKDeletedObject) async { if let index = samples.firstIndex(where: { $0.uuid == sample.uuid }) { samples.remove(at: index) diff --git a/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift b/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift index a84013f..2437e15 100644 --- a/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift @@ -34,6 +34,7 @@ final class HealthKitTests: XCTestCase { ] ) + XCTAssert(app.buttons["Trigger data source collection"].waitForExistence(timeout: 2)) app.buttons["Trigger data source collection"].tap() app.hkTypeIdentifierAssert( [ @@ -105,6 +106,7 @@ final class HealthKitTests: XCTestCase { ] ) + XCTAssert(app.buttons["Trigger data source collection"].waitForExistence(timeout: 2)) app.buttons["Trigger data source collection"].tap() app.hkTypeIdentifierAssert( [ @@ -120,6 +122,7 @@ final class HealthKitTests: XCTestCase { app.terminate() app.activate() XCTAssert(app.wait(for: .runningForeground, timeout: 10.0)) + XCTAssert(app.buttons["Trigger data source collection"].waitForExistence(timeout: 2)) app.buttons["Trigger data source collection"].tap() app.hkTypeIdentifierAssert([:]) @@ -138,6 +141,7 @@ final class HealthKitTests: XCTestCase { ] ) + XCTAssert(app.buttons["Trigger data source collection"].waitForExistence(timeout: 2)) app.buttons["Trigger data source collection"].tap() app.hkTypeIdentifierAssert( [