diff --git a/.github/workflows/monthly-markdown-link-check.yml b/.github/workflows/monthly-markdown-link-check.yml new file mode 100644 index 0000000..6c16315 --- /dev/null +++ b/.github/workflows/monthly-markdown-link-check.yml @@ -0,0 +1,19 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +name: Monthly Markdown Link Check + +on: + # Runs at midnight on the first of every month + schedule: + - cron: "0 0 1 * *" + +jobs: + markdown_link_check: + name: Markdown Link Check + uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 \ No newline at end of file diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index eb53387..5a7d62c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -19,3 +19,6 @@ jobs: swiftlint: name: SwiftLint uses: StanfordSpezi/.github/.github/workflows/swiftlint.yml@v2 + markdown_link_check: + name: Markdown Link Check + uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 diff --git a/.swiftlint.yml b/.swiftlint.yml index ac04d63..e605657 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -139,8 +139,6 @@ only_rules: - implicitly_unwrapped_optional # Identifiers should use inclusive language that avoids discrimination against groups of people based on race, gender, or socioeconomic status - inclusive_language - # If defer is at the end of its parent scope, it will be executed right where it is anyway. - - inert_defer # Prefer using Set.isDisjoint(with:) over Set.intersection(_:).isEmpty. - is_disjoint # Discouraged explicit usage of the default separator. @@ -327,8 +325,6 @@ only_rules: - unowned_variable_capture # Catch statements should not declare error variables without type casting. - untyped_error_in_catch - # Unused reference in a capture list should be removed. - - unused_capture_list # Unused parameter in a closure should be replaced with _. - unused_closure_parameter # Unused control flow label should be removed. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c5056f3..90e3838 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -12,3 +12,4 @@ SpeziHealthKit contributors ==================== * [Paul Schmiedmayer](https://github.com/PSchmiedmayer) +* [Andreas Bauer](https://github.com/Supereg) diff --git a/Package.swift b/Package.swift index fef817d..34de109 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.9 // // This source file is part of the Stanford Spezi open-source project @@ -15,13 +15,13 @@ let package = Package( name: "SpeziHealthKit", defaultLocalization: "en", platforms: [ - .iOS(.v16) + .iOS(.v17) ], products: [ .library(name: "SpeziHealthKit", targets: ["SpeziHealthKit"]) ], dependencies: [ - .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.0")) + .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.8.0")) ], targets: [ .target( diff --git a/Sources/SpeziHealthKit/CollectSample/CollectSample.swift b/Sources/SpeziHealthKit/CollectSample/CollectSample.swift index 5da8845..3649ebc 100644 --- a/Sources/SpeziHealthKit/CollectSample/CollectSample.swift +++ b/Sources/SpeziHealthKit/CollectSample/CollectSample.swift @@ -10,7 +10,7 @@ import HealthKit import Spezi -/// Collects a specificied `HKSampleType` in the ``HealthKit`` component. +/// Collects a specified `HKSampleType` in the ``HealthKit`` module. public struct CollectSample: HealthKitDataSourceDescription { private let collectSamples: CollectSamples diff --git a/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift b/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift index efc642a..a94cb48 100644 --- a/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift +++ b/Sources/SpeziHealthKit/CollectSample/CollectSamples.swift @@ -10,7 +10,7 @@ import HealthKit import Spezi -/// Collects `HKSampleType`s in the ``HealthKit`` component. +/// Collects `HKSampleType`s in the ``HealthKit`` module. public struct CollectSamples: HealthKitDataSourceDescription { public let sampleTypes: Set let predicate: NSPredicate? @@ -18,7 +18,7 @@ public struct CollectSamples: HealthKitDataSourceDescription { /// - Parameters: - /// - sampleType: The set of `HKSampleType`s that should be collected + /// - sampleTypes: The set of `HKSampleType`s that should be collected /// - predicate: A custom predicate that should be passed to the HealthKit query. /// The default predicate collects all samples that have been collected from the first time that the user /// provided the application authorization to collect the samples. diff --git a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift index 8bbe76f..d19a1fc 100644 --- a/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/SpeziHealthKit/CollectSample/HealthKitSampleDataSource.swift @@ -111,7 +111,7 @@ final class HealthKitSampleDataSource: HealthKitDataSource { anchoredSingleObjectQuery() case .anchorQuery: active = true - try await anchoredContinousObjectQuery() + try await anchoredContinuousObjectQuery() case .background: active = true for try await _ in healthStore.startObservation(for: [sampleType], withPredicate: predicate) { @@ -136,7 +136,7 @@ final class HealthKitSampleDataSource: HealthKitDataSource { } } - private func anchoredContinousObjectQuery() async throws { + private func anchoredContinuousObjectQuery() async throws { try await healthStore.requestAuthorization(toShare: [], read: [sampleType]) let anchorDescriptor = healthStore.anchorDescriptor(sampleType: sampleType, predicate: predicate, anchor: anchor) diff --git a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift index 172fdef..0b4cbf1 100644 --- a/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift +++ b/Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift @@ -25,7 +25,7 @@ extension HKHealthStore { using anchor: HKQueryAnchor? = nil, withPredicate predicate: NSPredicate? = nil, standard: any HealthKitConstraint - ) async throws -> (HKQueryAnchor) { + ) async throws -> HKQueryAnchor { try await self.requestAuthorization(toShare: [], read: [sampleType]) let anchorDescriptor = anchorDescriptor(sampleType: sampleType, predicate: predicate, anchor: anchor) diff --git a/Sources/SpeziHealthKit/HealthKit.swift b/Sources/SpeziHealthKit/HealthKit.swift index 5a78c97..8b647a5 100644 --- a/Sources/SpeziHealthKit/HealthKit.swift +++ b/Sources/SpeziHealthKit/HealthKit.swift @@ -30,7 +30,7 @@ import SwiftUI /// ``` /// /// Use the ``HealthKit/init(_:)`` initializer to define different ``HealthKitDataSourceDescription``s to define the data collection. -/// You can, e.g., use ``CollectSample`` to collect a wide variaty of `HKSampleTypes`: +/// You can, e.g., use ``CollectSample`` to collect a wide variety of `HKSampleTypes`: /// ```swift /// class ExampleAppDelegate: SpeziAppDelegate { /// override var configuration: Configuration { @@ -63,11 +63,12 @@ import SwiftUI /// } /// } /// ``` -public final class HealthKit: Module { - @StandardActor var standard: any HealthKitConstraint +@Observable +public final class HealthKit: Module, LifecycleHandler, EnvironmentAccessible { + @ObservationIgnored @StandardActor var standard: any HealthKitConstraint let healthStore: HKHealthStore let healthKitDataSourceDescriptions: [HealthKitDataSourceDescription] - lazy var healthKitComponents: [any HealthKitDataSource] = { + @ObservationIgnored lazy var healthKitComponents: [any HealthKitDataSource] = { healthKitDataSourceDescriptions .flatMap { $0.dataSources(healthStore: healthStore, standard: standard) } }() @@ -81,20 +82,30 @@ public final class HealthKit: Module { private var healthKitSampleTypesIdentifiers: Set { Set(healthKitSampleTypes.map(\.identifier)) } + + private var alreadyRequestedSampleTypes: Set { + get { + access(keyPath: \.alreadyRequestedSampleTypes) + return Set(UserDefaults.standard.stringArray(forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) ?? []) + } + set { + withMutation(keyPath: \.alreadyRequestedSampleTypes) { + UserDefaults.standard.set(Array(newValue), forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) + } + } + } /// Indicates whether the necessary authorizations to collect all HealthKit data defined by the ``HealthKitDataSourceDescription``s are already granted. public var authorized: Bool { - let alreadyRequestedSampleTypes = Set(UserDefaults.standard.stringArray(forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) ?? []) - - return healthKitSampleTypesIdentifiers.isSubset(of: alreadyRequestedSampleTypes) + healthKitSampleTypesIdentifiers.isSubset(of: alreadyRequestedSampleTypes) } /// Creates a new instance of the ``HealthKit`` module. /// - Parameters: - /// - healthKitDataSourceDescriptions: The ``HealthKitDataSourceDescription``s define what data is collected by the ``HealthKit`` module. You can, e.g., use ``CollectSample`` to collect a wide variaty of `HKSampleTypes`. + /// - 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`. public init( - @HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> ([HealthKitDataSourceDescription]) + @HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> [HealthKitDataSourceDescription] ) { precondition( HKHealthStore.isHealthDataAvailable(), @@ -125,17 +136,13 @@ public final class HealthKit: Module { } try await healthStore.requestAuthorization(toShare: [], read: healthKitSampleTypes) - - UserDefaults.standard.set(Array(healthKitSampleTypesIdentifiers), forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) + + alreadyRequestedSampleTypes = healthKitSampleTypesIdentifiers for healthKitComponent in healthKitComponents { + // reads the above userDefault! healthKitComponent.askedForAuthorization() } - - // Triggers an update of the UI in case the HealthKit authorizations are changed - Task { @MainActor in - self.objectWillChange.send() - } } diff --git a/Sources/SpeziHealthKit/HealthKitConstraint.swift b/Sources/SpeziHealthKit/HealthKitConstraint.swift index 69c5276..7420c13 100644 --- a/Sources/SpeziHealthKit/HealthKitConstraint.swift +++ b/Sources/SpeziHealthKit/HealthKitConstraint.swift @@ -28,11 +28,11 @@ import Spezi /// ``` /// public protocol HealthKitConstraint: Standard { - /// Notifies the ``Standard`` about the addition of a HealthKit ``HKSample`` sample instance. + /// Notifies the `Standard` about the addition of a HealthKit ``HKSample`` sample instance. /// - Parameter sample: The `HKSample` that should be added. func add(sample: HKSample) async - /// Notifies the ``Standard`` about the removal of a HealthKit sample as defined by the `HKDeletedObject`. + /// Notifies the `Standard` about the removal of a HealthKit sample as defined by the `HKDeletedObject`. /// - Parameter sample: The `HKDeletedObject` is a sample that should be removed. func remove(sample: HKDeletedObject) async } diff --git a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift index ae3cb57..27ad97e 100644 --- a/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift +++ b/Sources/SpeziHealthKit/HealthKitDataSource/HealthKitDataSourceDescription.swift @@ -16,7 +16,8 @@ public protocol HealthKitDataSourceDescription { var sampleTypes: Set { get } - /// The ``HealthKitDataSourceDescription/dataSources(healthStore:standard:)`` method creates ``HealthKitDataSource`` swhen the HealthKit component is instantiated. + /// The ``HealthKitDataSourceDescription/dataSources(healthStore:standard:)`` method creates ``HealthKitDataSource`` + /// when the HealthKit module is instantiated. /// - Parameters: /// - healthStore: The `HKHealthStore` instance that the queries should be performed on. /// - standard: The `Standard` instance that is used in the software system. diff --git a/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift b/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift index 3f9e550..b4b58fe 100644 --- a/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift +++ b/Sources/SpeziHealthKit/HealthKitDeliverySetting/HealthKitDeliverySetting.swift @@ -7,7 +7,7 @@ // -/// Determines the data delivery settings for any ``HealthKitDataSource`` used in the HealthKit component. +/// Determines the data delivery settings for any ``HealthKitDataSource`` used in the HealthKit module. public enum HealthKitDeliverySetting: Equatable { /// The HealthKit data is manually collected when the ``HealthKit/triggerDataSourceCollection()`` function is called. case manual(safeAnchor: Bool = true) diff --git a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift index c0d047b..681f119 100644 --- a/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift +++ b/Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift @@ -17,7 +17,7 @@ final class SpeziHealthKitTests: XCTestCase { HKQuantityType(.distanceWalkingRunning) ] - let healthKitComponent = HealthKit { + let healthKitModule = HealthKit { CollectSamples( collectedSamples, deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch) @@ -31,7 +31,7 @@ final class SpeziHealthKitTests: XCTestCase { /// No authorizations for HealthKit data are given in the ``UserDefaults`` func testSpeziHealthKitCollectionNotAuthorized1() { - XCTAssert(!healthKitComponent.authorized) + XCTAssert(!healthKitModule.authorized) } /// Not enough authorizations for HealthKit data given in the ``UserDefaults`` @@ -42,7 +42,7 @@ final class SpeziHealthKitTests: XCTestCase { forKey: UserDefaults.Keys.healthKitRequestedSampleTypes ) - XCTAssert(!healthKitComponent.authorized) + XCTAssert(!healthKitModule.authorized) } /// Authorization for HealthKit data are given in the ``UserDefaults`` @@ -53,6 +53,6 @@ final class SpeziHealthKitTests: XCTestCase { forKey: UserDefaults.Keys.healthKitRequestedSampleTypes ) - XCTAssert(healthKitComponent.authorized) + XCTAssert(healthKitModule.authorized) } } diff --git a/Tests/UITests/TestApp/ExampleStandard.swift b/Tests/UITests/TestApp/ExampleStandard.swift index 45fc792..899598a 100644 --- a/Tests/UITests/TestApp/ExampleStandard.swift +++ b/Tests/UITests/TestApp/ExampleStandard.swift @@ -10,10 +10,22 @@ import Spezi import SpeziHealthKit +@Observable +private class ResponseList { + var addedResponses = [HKSample]() +} /// An example Standard used for the configuration. -actor ExampleStandard: Standard, ObservableObject, ObservableObjectProvider { - @Published @MainActor var addedResponses = [HKSample]() +actor ExampleStandard: Standard, EnvironmentAccessible { + @MainActor private var responseList = ResponseList() + @MainActor var addedResponses: [HKSample] { + _read { + yield responseList.addedResponses + } + _modify { + yield &responseList.addedResponses + } + } } diff --git a/Tests/UITests/TestApp/HealthKitTestsView.swift b/Tests/UITests/TestApp/HealthKitTestsView.swift index eefe23e..47d61ec 100644 --- a/Tests/UITests/TestApp/HealthKitTestsView.swift +++ b/Tests/UITests/TestApp/HealthKitTestsView.swift @@ -11,15 +11,15 @@ import SwiftUI struct HealthKitTestsView: View { - @EnvironmentObject var healthKitComponent: HealthKit - @EnvironmentObject var standard: ExampleStandard - + @Environment(HealthKit.self) var healthKitModule + @Environment(ExampleStandard.self) var standard + var body: some View { Button("Ask for authorization") { askForAuthorization() } - .disabled(healthKitComponent.authorized) + .disabled(healthKitModule.authorized) Button("Trigger data source collection") { triggerDataSourceCollection() } @@ -30,16 +30,17 @@ struct HealthKitTestsView: View { } } - + @MainActor private func askForAuthorization() { Task { - try await healthKitComponent.askForAuthorization() + try await healthKitModule.askForAuthorization() } } + @MainActor private func triggerDataSourceCollection() { Task { - await healthKitComponent.triggerDataSourceCollection() + await healthKitModule.triggerDataSourceCollection() } } } diff --git a/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift b/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift index de12174..fb2d6d0 100644 --- a/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift @@ -115,7 +115,7 @@ final class HealthKitTests: XCTestCase { ] ) - // Relaunch App to test delivery after the app has been terminted. + // Relaunch App to test delivery after the app has been terminated. app.terminate() app.activate() XCTAssert(app.wait(for: .runningForeground, timeout: 10.0)) diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index a84d0a0..b55ae06 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 2F85826E29E776690021D637 /* SpeziHealthKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F85826D29E776690021D637 /* SpeziHealthKitTests.swift */; }; 2F85827129E776780021D637 /* HealthKitTestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F85827029E776780021D637 /* HealthKitTestsView.swift */; }; 2F85827329E776AC0021D637 /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F85827229E776AC0021D637 /* TestAppDelegate.swift */; }; - 2F85827629E776D10021D637 /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F85827529E776D10021D637 /* Spezi */; }; 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 */; }; @@ -49,7 +48,6 @@ buildActionMask = 2147483647; files = ( 2F61BDC329DD02D600D71D33 /* SpeziHealthKit in Frameworks */, - 2F85827629E776D10021D637 /* Spezi in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -133,7 +131,6 @@ name = TestApp; packageProductDependencies = ( 2F61BDC229DD02D600D71D33 /* SpeziHealthKit */, - 2F85827529E776D10021D637 /* Spezi */, ); productName = Example; productReference = 2F6D139228F5F384007C25D6 /* TestApp.app */; @@ -190,7 +187,6 @@ ); mainGroup = 2F6D138928F5F384007C25D6; packageReferences = ( - 2F85827429E776D10021D637 /* XCRemoteSwiftPackageReference "Spezi" */, 2F85827B29E778110021D637 /* XCRemoteSwiftPackageReference "XCTestExtensions" */, 97B0290E2A5710C800946EF8 /* XCRemoteSwiftPackageReference "XCTHealthKit" */, ); @@ -225,6 +221,7 @@ /* Begin PBXShellScriptBuildPhase section */ 390F295F2A78596E000A236E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -323,7 +320,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -377,7 +374,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -545,7 +542,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -645,14 +642,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2F85827429E776D10021D637 /* XCRemoteSwiftPackageReference "Spezi" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/Spezi.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.7.0; - }; - }; 2F85827B29E778110021D637 /* XCRemoteSwiftPackageReference "XCTestExtensions" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/XCTestExtensions"; @@ -676,11 +665,6 @@ isa = XCSwiftPackageProductDependency; productName = SpeziHealthKit; }; - 2F85827529E776D10021D637 /* Spezi */ = { - isa = XCSwiftPackageProductDependency; - package = 2F85827429E776D10021D637 /* XCRemoteSwiftPackageReference "Spezi" */; - productName = Spezi; - }; 2F85827E29E7782C0021D637 /* XCTestExtensions */ = { isa = XCSwiftPackageProductDependency; package = 2F85827B29E778110021D637 /* XCRemoteSwiftPackageReference "XCTestExtensions" */;