diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 37e9d21..d827c59 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -37,3 +37,5 @@ jobs: uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2 with: coveragereports: SpeziHealthKit.xcresult TestApp.xcresult + secrets: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Package.swift b/Package.swift index c50aab0..1899ae7 100644 --- a/Package.swift +++ b/Package.swift @@ -8,9 +8,17 @@ // SPDX-License-Identifier: MIT // +import class Foundation.ProcessInfo import PackageDescription +#if swift(<6) +let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency") +#else +let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency") +#endif + + let package = Package( name: "SpeziHealthKit", defaultLocalization: "en", @@ -21,21 +29,48 @@ let package = Package( .library(name: "SpeziHealthKit", targets: ["SpeziHealthKit"]) ], dependencies: [ - .package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.0") - ], + .package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.7.0") + ] + swiftLintPackage(), targets: [ .target( name: "SpeziHealthKit", dependencies: [ .product(name: "Spezi", package: "Spezi") - ] + ], + swiftSettings: [ + swiftConcurrency, + .enableUpcomingFeature("InferSendableFromCaptures") + ], + plugins: [] + swiftLintPlugin() ), .testTarget( name: "SpeziHealthKitTests", dependencies: [ .product(name: "XCTSpezi", package: "Spezi"), .target(name: "SpeziHealthKit") - ] + ], + swiftSettings: [ + swiftConcurrency + ], + plugins: [] + swiftLintPlugin() ) ] ) + + +func swiftLintPlugin() -> [Target.PluginUsage] { + // Fully quit Xcode and open again with `open --env SPEZI_DEVELOPMENT_SWIFTLINT /Applications/Xcode.app` + if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil { + [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")] + } else { + [] + } +} + +func swiftLintPackage() -> [PackageDescription.Package.Dependency] { + if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil { + [.package(url: "https://github.com/realm/SwiftLint.git", from: "0.55.1")] + } else { + [] + } +} diff --git a/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift b/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift index a94cb48..5311cec 100644 --- a/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift +++ b/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import Foundation import HealthKit import Spezi @@ -32,8 +33,7 @@ public struct CollectSamples: HealthKitDataSourceDescription { self.predicate = predicate self.deliverySetting = deliverySetting } - - + public func dataSources( healthStore: HKHealthStore, standard: any HealthKitConstraint @@ -49,3 +49,5 @@ public struct CollectSamples: HealthKitDataSourceDescription { } } } + +extension CollectSamples: Hashable {} diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index bb95e74..1debc1e 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import HealthKit +@preconcurrency import HealthKit import OSLog import Spezi import SwiftUI @@ -19,19 +19,16 @@ final class HealthKitSampleDataSource: HealthKitDataSource { let sampleType: HKSampleType let predicate: NSPredicate? let deliverySetting: HealthKitDeliverySetting - var active = false - - private lazy var anchorUserDefaultsKey = UserDefaults.Keys.healthKitAnchorPrefix.appending(sampleType.identifier) - private lazy var anchor: HKQueryAnchor? = loadAnchor() { + @MainActor var active = false + + @MainActor private lazy var anchorUserDefaultsKey = UserDefaults.Keys.healthKitAnchorPrefix.appending(sampleType.identifier) + @MainActor private lazy var anchor: HKQueryAnchor? = loadAnchor() { didSet { saveAnchor() } } - - // 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( + + required init( // swiftlint:disable:this function_default_parameter_at_end healthStore: HKHealthStore, standard: any HealthKitConstraint, sampleType: HKSampleType, @@ -42,18 +39,17 @@ final class HealthKitSampleDataSource: HealthKitDataSource { self.standard = standard self.sampleType = sampleType self.deliverySetting = deliverySetting - - if predicate == nil { + + if let predicate { + self.predicate = predicate + } else { self.predicate = HKQuery.predicateForSamples( withStart: HealthKitSampleDataSource.loadDefaultQueryDate(for: sampleType), end: nil, options: .strictEndDate ) - } else { - self.predicate = predicate } } - // swiftlint:enable function_default_parameter_at_end private static func loadDefaultQueryDate(for sampleType: HKSampleType) -> Date { @@ -73,7 +69,7 @@ final class HealthKitSampleDataSource: HealthKitDataSource { return date } - + func askedForAuthorization() async { guard askedForAuthorization(for: sampleType) && !deliverySetting.isManual && !active else { return @@ -81,12 +77,12 @@ final class HealthKitSampleDataSource: HealthKitDataSource { await triggerManualDataSourceCollection() } - + func startAutomaticDataCollection() async { guard askedForAuthorization(for: sampleType) else { return } - + switch deliverySetting { case let .anchorQuery(startSetting, _) where startSetting == .automatic, let .background(startSetting, _) where startSetting == .automatic: @@ -95,7 +91,7 @@ final class HealthKitSampleDataSource: HealthKitDataSource { break } } - + func triggerManualDataSourceCollection() async { guard !active else { return @@ -111,35 +107,34 @@ final class HealthKitSampleDataSource: HealthKitDataSource { case .background: active = true 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 + guard case let .success((sampleTypes, completionHandler)) = result else { + return + } + + guard sampleTypes.contains(self.sampleType) else { + Logger.healthKit.warning("Received Observation query types (\(sampleTypes)) are not corresponding to the CollectSample type \(self.sampleType)") completionHandler() + return + } + + do { + try await self.anchoredSingleObjectQuery() + Logger.healthKit.debug("Successfully processed background update for \(self.sampleType)") + } 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() } } } catch { Logger.healthKit.error("Could not Process HealthKit data collection: \(error.localizedDescription)") } } - - + + + @MainActor private func anchoredSingleObjectQuery() async throws { let resultsAnchor = try await healthStore.anchoredSingleObjectQuery( for: self.sampleType, @@ -149,20 +144,17 @@ final class HealthKitSampleDataSource: HealthKitDataSource { ) self.anchor = resultsAnchor } - + + @MainActor private func anchoredContinuousObjectQuery() 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) - + Task { for try await results in updateQueue { - if Task.isCancelled { - return - } - for deletedObject in results.deletedObjects { await standard.remove(sample: deletedObject) } @@ -174,7 +166,8 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } } } - + + @MainActor private func saveAnchor() { if deliverySetting.saveAnchor { guard let anchor, @@ -185,7 +178,8 @@ final class HealthKitSampleDataSource: HealthKitDataSource { UserDefaults.standard.set(data, forKey: anchorUserDefaultsKey) } } - + + @MainActor private func loadAnchor() -> HKQueryAnchor? { guard deliverySetting.saveAnchor, let userDefaultsData = UserDefaults.standard.data(forKey: anchorUserDefaultsKey), diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift index 0b4cbf1..d0c3063 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift @@ -6,11 +6,19 @@ // SPDX-License-Identifier: MIT // -import HealthKit +@preconcurrency import HealthKit import Spezi -extension HKSample: Identifiable { +#if compiler(<6) +extension HKSample: Swift.Identifiable {} +#else +extension HKSample: @retroactive Identifiable {} +#endif + + +extension HKSample { + /// The `uuid` identifier. public var id: UUID { uuid } @@ -20,6 +28,7 @@ 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 + @MainActor func anchoredSingleObjectQuery( for sampleType: HKSampleType, using anchor: HKQueryAnchor? = nil, diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift index 4ef505f..ca41ea0 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift @@ -12,14 +12,17 @@ import Spezi extension HKHealthStore { - private static var activeObservations: [HKObjectType: Int] = [:] private static let activeObservationsLock = NSLock() + private static nonisolated(unsafe) var activeObservations: [HKObjectType: Int] = [:] - + + @MainActor func startBackgroundDelivery( for sampleTypes: Set<HKSampleType>, withPredicate predicate: NSPredicate? = nil, - observerQuery: @escaping (Result<(sampleTypes: Set<HKSampleType>, completionHandler: HKObserverQueryCompletionHandler), Error>) -> Void + observerQuery: @escaping @Sendable @MainActor ( + Result<(sampleTypes: Set<HKSampleType>, completionHandler: HKObserverQueryCompletionHandler), Error> + ) async -> Void ) async throws { var queryDescriptors: [HKQueryDescriptor] = [] for sampleType in sampleTypes { @@ -29,15 +32,24 @@ extension HKHealthStore { } let observerQuery = HKObserverQuery(queryDescriptors: queryDescriptors) { query, samples, completionHandler, error in + // From https://developer.apple.com/documentation/healthkit/hkobserverquery/executing_observer_queries: + // "Whenever a matching sample is added to or deleted from the HealthKit store, + // the system calls the query’s update handler on the same background queue (but not necessarily the same thread)." + // So, the observerQuery has to be @Sendable! + 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() + Task { @MainActor in + await observerQuery(.failure(error ?? NSError(domain: "Spezi HealthKit", code: -1))) + completionHandler() + } return } - observerQuery(.success((samples, completionHandler))) + Task { @MainActor in + await observerQuery(.success((samples, completionHandler))) + } } self.execute(observerQuery) diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift index 6428c2d..3b75335 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+SampleQuery.swift @@ -29,20 +29,4 @@ 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, - standard: any HealthKitConstraint - ) { - _Concurrency.Task { - for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) { - await standard.add(sample: sample) - } - } - } - // swiftlint:enable function_default_parameter_at_end } diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/NSPredicate+Sendable.swift b/Sources/SpeziHealthKit/HealthKit Extensions/NSPredicate+Sendable.swift deleted file mode 100644 index a082f1f..0000000 --- a/Sources/SpeziHealthKit/HealthKit Extensions/NSPredicate+Sendable.swift +++ /dev/null @@ -1,12 +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 Foundation - - -extension NSPredicate: @unchecked Sendable { } diff --git a/Sources/SpeziHealthKit/HealthKit.swift b/Sources/SpeziHealthKit/HealthKit.swift index 01e2019..7d8721e 100644 --- a/Sources/SpeziHealthKit/HealthKit.swift +++ b/Sources/SpeziHealthKit/HealthKit.swift @@ -70,35 +70,35 @@ import SwiftUI public final class HealthKit: Module, EnvironmentAccessible, DefaultInitializable { @ObservationIgnored @StandardActor private var standard: any HealthKitConstraint private let healthStore: HKHealthStore - private var initialHealthKitDataSourceDescriptions: [HealthKitDataSourceDescription] = [] - private var healthKitDataSourceDescriptions: [HealthKitDataSourceDescription] = [] + @MainActor private var initialHealthKitDataSourceDescriptions: [HealthKitDataSourceDescription] = [] + @MainActor private var healthKitDataSourceDescriptions: [HealthKitDataSourceDescription] = [] @ObservationIgnored private var healthKitComponents: [any HealthKitDataSource] = [] - private var healthKitSampleTypes: Set<HKSampleType> { + @MainActor private var healthKitSampleTypes: Set<HKSampleType> { (initialHealthKitDataSourceDescriptions + healthKitDataSourceDescriptions).reduce(into: Set()) { $0 = $0.union($1.sampleTypes) } } - private var healthKitSampleTypesIdentifiers: Set<String> { + @MainActor private var healthKitSampleTypesIdentifiers: Set<String> { Set(healthKitSampleTypes.map(\.identifier)) } private var alreadyRequestedSampleTypes: Set<String> { get { access(keyPath: \.alreadyRequestedSampleTypes) - return Set(UserDefaults.standard.stringArray(forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) ?? []) + return UserDefaults.standard.alreadyRequestedSampleTypes } set { withMutation(keyPath: \.alreadyRequestedSampleTypes) { - UserDefaults.standard.set(Array(newValue), forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) + UserDefaults.standard.alreadyRequestedSampleTypes = newValue } } } /// Indicates whether the necessary authorizations to collect all HealthKit data defined by the ``HealthKitDataSourceDescription``s are already granted. - public var authorized: Bool { + @MainActor public var authorized: Bool { healthKitSampleTypesIdentifiers.isSubset(of: alreadyRequestedSampleTypes) } @@ -106,6 +106,7 @@ public final class HealthKit: Module, EnvironmentAccessible, DefaultInitializabl /// 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 variety of `HKSampleTypes`. + @MainActor public convenience init( @HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> [HealthKitDataSourceDescription] ) { @@ -125,20 +126,27 @@ public final class HealthKit: Module, EnvironmentAccessible, DefaultInitializabl } """ ) - + self.healthStore = HKHealthStore() } - - + + static func didAskForAuthorization(for sampleType: HKSampleType) -> Bool { + // `alreadyRequestedSampleTypes` is always just written using `healthKitSampleTypesIdentifiers`, so this can stay + // non-isolated as UserDefaults is generally thread-safe. + UserDefaults.standard.alreadyRequestedSampleTypes.contains(sampleType.identifier) + } + public func configure() { for healthKitDataSourceDescription in initialHealthKitDataSourceDescriptions { execute(healthKitDataSourceDescription) } } - + + /// 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. + @MainActor public func askForAuthorization() async throws { guard !authorized else { return @@ -152,7 +160,8 @@ public final class HealthKit: Module, EnvironmentAccessible, DefaultInitializabl await healthKitComponent.askedForAuthorization() } } - + + @MainActor public func execute(_ healthKitDataSourceDescription: HealthKitDataSourceDescription) { healthKitDataSourceDescriptions.append(healthKitDataSourceDescription) let dataSources = healthKitDataSourceDescription.dataSources(healthStore: healthStore, standard: standard) @@ -164,7 +173,8 @@ public final class HealthKit: Module, EnvironmentAccessible, DefaultInitializabl } } } - + + @MainActor public func execute(@HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> [HealthKitDataSourceDescription]) { for healthKitDataSourceDescription in healthKitDataSourceDescriptions() { execute(healthKitDataSourceDescription) @@ -172,10 +182,11 @@ public final class HealthKit: Module, EnvironmentAccessible, DefaultInitializabl } /// Triggers any ``HealthKitDeliverySetting/manual(safeAnchor:)`` collections and starts the collection for all ``HealthKitDeliveryStartSetting/manual`` HealthKit data collections. + @MainActor public func triggerDataSourceCollection() async { await withTaskGroup(of: Void.self) { group in for healthKitComponent in healthKitComponents { - group.addTask { + group.addTask { @MainActor in await healthKitComponent.triggerManualDataSourceCollection() } } diff --git a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSource.swift b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSource.swift index 346c486..794e95b 100644 --- a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSource.swift +++ b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSource.swift @@ -14,17 +14,31 @@ import SwiftUI /// Requirement for every HealthKit Data Source. public protocol HealthKitDataSource { /// Called after the used was asked for authorization. + @MainActor func askedForAuthorization() async /// Called to trigger the manual data collection. + @MainActor func triggerManualDataSourceCollection() async /// Called to start the automatic data collection. + @MainActor func startAutomaticDataCollection() async } extension HealthKitDataSource { func askedForAuthorization(for sampleType: HKSampleType) -> Bool { - let requestedSampleTypes = Set(UserDefaults.standard.stringArray(forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) ?? []) - return requestedSampleTypes.contains(sampleType.identifier) + HealthKit.didAskForAuthorization(for: sampleType) + } +} + + +extension UserDefaults { + var alreadyRequestedSampleTypes: Set<String> { + get { + Set(stringArray(forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) ?? []) + } + set { + set(Array(newValue), forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) + } } } diff --git a/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift b/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift index 33e3982..04fd1f0 100644 --- a/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift +++ b/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift @@ -39,3 +39,6 @@ public enum HealthKitDeliverySetting: Equatable { } } } + + +extension HealthKitDeliverySetting: Sendable, Hashable {} diff --git a/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliveryStartSetting.swift b/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliveryStartSetting.swift index fd0966c..ce9ce42 100644 --- a/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliveryStartSetting.swift +++ b/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliveryStartSetting.swift @@ -8,22 +8,29 @@ /// Determines when the HealthKit data collection is started. -public enum HealthKitDeliveryStartSetting: Equatable { +public enum HealthKitDeliveryStartSetting { /// The delivery is started the first time the ``HealthKit/triggerDataSourceCollection()`` function is called. 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 automatic - + + /// Legacy delivery setting, start after initialization @available( *, deprecated, + renamed: "automatic", message: """ Please use `.automatic`. """ ) - public static let afterAuthorizationAndApplicationWillLaunch: HealthKitDeliveryStartSetting = .automatic // swiftlint:disable:this identifier_name + @_documentation(visibility: internal) + public static let afterAuthorizationAndApplicationWillLaunch: HealthKitDeliveryStartSetting = .automatic + // swiftlint:disable:previous identifier_name // We use a name longer than 40 characters to indicate the full depth of this setting. } + + +extension HealthKitDeliveryStartSetting: Sendable, Hashable {} diff --git a/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift b/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift index 468f613..392d348 100644 --- a/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift +++ b/Sources/SpeziHealthKit/Logging/Logger+HealthKit.swift @@ -10,11 +10,6 @@ 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") + static let healthKit = Logger(subsystem: "edu.stanford.spezi", category: "HealthKit") } diff --git a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift index 0d1fdf0..eb5c9c8 100644 --- a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift +++ b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift @@ -17,7 +17,7 @@ final class SpeziHealthKitTests: XCTestCase { HKQuantityType(.distanceWalkingRunning) ] - let healthKitModule = HealthKit { + @MainActor let healthKitModule = HealthKit { CollectSamples( collectedSamples, deliverySetting: .anchorQuery(.automatic) @@ -30,11 +30,13 @@ final class SpeziHealthKitTests: XCTestCase { } /// No authorizations for HealthKit data are given in the ``UserDefaults`` + @MainActor func testSpeziHealthKitCollectionNotAuthorized1() { XCTAssert(!healthKitModule.authorized) } /// Not enough authorizations for HealthKit data given in the ``UserDefaults`` + @MainActor func testSpeziHealthKitCollectionNotAuthorized2() { // Set up UserDefaults UserDefaults.standard.set( @@ -46,6 +48,7 @@ final class SpeziHealthKitTests: XCTestCase { } /// Authorization for HealthKit data are given in the ``UserDefaults`` + @MainActor func testSpeziHealthKitCollectionAlreadyAuthorized() { // Set up UserDefaults UserDefaults.standard.set(