Skip to content

Commit

Permalink
Flag to check if authorizations to collect HealthKit data are already…
Browse files Browse the repository at this point in the history
… present (#7)

Co-authored-by: Paul Schmiedmayer <[email protected]>
  • Loading branch information
philippzagar and PSchmiedmayer authored Jul 6, 2023
1 parent 7ae7c34 commit 0a26f41
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 39 deletions.
2 changes: 0 additions & 2 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
only_rules:
# All Images that provide context should have an accessibility label. Purely decorative images can be hidden from accessibility.
- accessibility_label_for_image
# Attributes should be on their own lines in functions and types, but on the same line as variables and imports.
- attributes
# Prefer using Array(seq) over seq.map { $0 } to convert a sequence into an Array.
- array_init
# Prefer the new block based KVO API with keypaths when using Swift 3.2 or later.
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ let package = Package(
.testTarget(
name: "SpeziHealthKitTests",
dependencies: [
.product(name: "XCTSpezi", package: "Spezi"),
.target(name: "SpeziHealthKit")
]
)
Expand Down
35 changes: 25 additions & 10 deletions Sources/SpeziHealthKit/HealthKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ public final class HealthKit<ComponentStandard: Standard>: Module {
.flatMap { $0.dataSources(healthStore: healthStore, standard: standard, adapter: adapter) }
}()

private var healthKitSampleTypes: Set<HKSampleType> {
healthKitDataSourceDescriptions.reduce(into: Set()) {
$0 = $0.union($1.sampleTypes)
}
}

private var healthKitSampleTypesIdentifiers: Set<String> {
Set(healthKitSampleTypes.map(\.identifier))
}

/// 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)
}


/// Creates a new instance of the ``HealthKit`` module.
/// - Parameters:
Expand Down Expand Up @@ -99,24 +116,22 @@ public final class HealthKit<ComponentStandard: Standard>: Module {
///
/// Call this function when you want to start HealthKit data collection.
public func askForAuthorization() async throws {
var sampleTypes: Set<HKSampleType> = []

for healthKitDataSourceDescription in healthKitDataSourceDescriptions {
sampleTypes = sampleTypes.union(healthKitDataSourceDescription.sampleTypes)
}

let requestedSampleTypes = Set(UserDefaults.standard.stringArray(forKey: UserDefaults.Keys.healthKitRequestedSampleTypes) ?? [])
guard !Set(sampleTypes.map { $0.identifier }).isSubset(of: requestedSampleTypes) else {
guard !authorized else {
return
}

try await healthStore.requestAuthorization(toShare: [], read: sampleTypes)
try await healthStore.requestAuthorization(toShare: [], read: healthKitSampleTypes)

UserDefaults.standard.set(sampleTypes.map { $0.identifier }, forKey: UserDefaults.Keys.healthKitRequestedSampleTypes)
UserDefaults.standard.set(Array(healthKitSampleTypesIdentifiers), forKey: UserDefaults.Keys.healthKitRequestedSampleTypes)

for healthKitComponent in healthKitComponents {
healthKitComponent.askedForAuthorization()
}

// Triggers an update of the UI in case the HealthKit authorizations are changed
Task { @MainActor in
self.objectWillChange.send()
}
}


Expand Down
31 changes: 31 additions & 0 deletions Tests/SpeziHealthKitTests/Shared/MockAdapterActor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import HealthKit
import Spezi
import SpeziHealthKit
import XCTSpezi

actor MockAdapterActor: Adapter {
typealias InputElement = HKSample
typealias InputRemovalContext = HKSampleRemovalContext
typealias OutputElement = TestAppStandard.BaseType
typealias OutputRemovalContext = TestAppStandard.RemovalContext


func transform(
_ asyncSequence: some TypedAsyncSequence<DataChange<InputElement, InputRemovalContext>>
) async -> any TypedAsyncSequence<DataChange<OutputElement, OutputRemovalContext>> {
asyncSequence.map { element in
element.map(
element: { OutputElement(id: String(describing: $0.id)) },
removalContext: { OutputRemovalContext(id: $0.id.uuidString) }
)
}
}
}
17 changes: 0 additions & 17 deletions Tests/SpeziHealthKitTests/SpeziContactTests.swift

This file was deleted.

60 changes: 60 additions & 0 deletions Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import HealthKit
@testable import SpeziHealthKit
import XCTest
import XCTSpezi

final class SpeziHealthKitTests: XCTestCase {
static let collectedSamples: Set<HKSampleType> = [
HKQuantityType(.stepCount),
HKQuantityType(.distanceWalkingRunning)
]

let healthKitComponent: HealthKit<TestAppStandard> = HealthKit {
CollectSamples(
collectedSamples,
deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch)
)
} adapter: {
MockAdapterActor()
}

override func tearDown() {
// Clean up UserDefaults
UserDefaults.standard.removeObject(forKey: UserDefaults.Keys.healthKitRequestedSampleTypes)
}

/// No authorizations for HealthKit data are given in the ``UserDefaults``
func testSpeziHealthKitCollectionNotAuthorized1() {
XCTAssert(!healthKitComponent.authorized)
}

/// Not enough authorizations for HealthKit data given in the ``UserDefaults``
func testSpeziHealthKitCollectionNotAuthorized2() {
// Set up UserDefaults
UserDefaults.standard.set(
Array(Self.collectedSamples.map { $0.identifier }.dropLast()), // Drop one of the required authorizations
forKey: UserDefaults.Keys.healthKitRequestedSampleTypes
)

XCTAssert(!healthKitComponent.authorized)
}

/// Authorization for HealthKit data are given in the ``UserDefaults``
func testSpeziHealthKitCollectionAlreadyAuthorized() {
// Set up UserDefaults
UserDefaults.standard.set(
Self.collectedSamples.map { $0.identifier },
forKey: UserDefaults.Keys.healthKitRequestedSampleTypes
)

XCTAssert(healthKitComponent.authorized)
}
}
2 changes: 2 additions & 0 deletions Tests/UITests/TestApp/HealthKitTestsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ struct HealthKitTestsView: View {
Button("Ask for authorization") {
askForAuthorization()
}
.disabled(healthKitComponent.authorized)

Button("Trigger data source collection") {
triggerDataSourceCollection()
}
Expand Down
23 changes: 23 additions & 0 deletions Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,29 @@ final class HealthKitTests: XCTestCase {
]
)
}

func testRepeatedHealthKitAuthorization() throws {
let app = XCUIApplication()
app.deleteAndLaunch(withSpringboardAppName: "TestApp")

app.activate()
XCTAssert(app.buttons["Ask for authorization"].waitForExistence(timeout: 2))
XCTAssert(app.buttons["Ask for authorization"].isEnabled)
app.buttons["Ask for authorization"].tap()

try app.handleHealthKitAuthorization()

// Wait for button to become disabled
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate { _, _ in
!app.buttons["Ask for authorization"].isEnabled
},
object: .none
)
wait(for: [expectation], timeout: 2)

XCTAssert(!app.buttons["Ask for authorization"].isEnabled)
}
}


Expand Down
20 changes: 10 additions & 10 deletions Tests/UITests/UITests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
2F85827829E776D10021D637 /* XCTSpezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F85827729E776D10021D637 /* XCTSpezi */; };
2F85827A29E777980021D637 /* TestAppHealthKitAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F85827929E777980021D637 /* TestAppHealthKitAdapter.swift */; };
2F85827F29E7782C0021D637 /* XCTestExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 2F85827E29E7782C0021D637 /* XCTestExtensions */; };
2F85828229E7784C0021D637 /* XCTHealthKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2F85828129E7784C0021D637 /* XCTHealthKit */; };
2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; };
97B029102A5710C800946EF8 /* XCTHealthKit in Frameworks */ = {isa = PBXBuildFile; productRef = 97B0290F2A5710C800946EF8 /* XCTHealthKit */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -60,7 +60,7 @@
buildActionMask = 2147483647;
files = (
2F85827F29E7782C0021D637 /* XCTestExtensions in Frameworks */,
2F85828229E7784C0021D637 /* XCTHealthKit in Frameworks */,
97B029102A5710C800946EF8 /* XCTHealthKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -157,7 +157,7 @@
name = TestAppUITests;
packageProductDependencies = (
2F85827E29E7782C0021D637 /* XCTestExtensions */,
2F85828129E7784C0021D637 /* XCTHealthKit */,
97B0290F2A5710C800946EF8 /* XCTHealthKit */,
);
productName = ExampleUITests;
productReference = 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */;
Expand Down Expand Up @@ -194,7 +194,7 @@
packageReferences = (
2F85827429E776D10021D637 /* XCRemoteSwiftPackageReference "Spezi" */,
2F85827B29E778110021D637 /* XCRemoteSwiftPackageReference "XCTestExtensions" */,
2F85828029E7784C0021D637 /* XCRemoteSwiftPackageReference "XCTHealthKit" */,
97B0290E2A5710C800946EF8 /* XCRemoteSwiftPackageReference "XCTHealthKit" */,
);
productRefGroup = 2F6D139328F5F384007C25D6 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -634,15 +634,15 @@
repositoryURL = "https://github.com/StanfordSpezi/XCTestExtensions";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.4.1;
minimumVersion = 0.4.6;
};
};
2F85828029E7784C0021D637 /* XCRemoteSwiftPackageReference "XCTHealthKit" */ = {
97B0290E2A5710C800946EF8 /* XCRemoteSwiftPackageReference "XCTHealthKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/StanfordBDHG/XCTHealthKit";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.3.3;
kind = upToNextMajorVersion;
minimumVersion = 0.3.5;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand All @@ -667,9 +667,9 @@
package = 2F85827B29E778110021D637 /* XCRemoteSwiftPackageReference "XCTestExtensions" */;
productName = XCTestExtensions;
};
2F85828129E7784C0021D637 /* XCTHealthKit */ = {
97B0290F2A5710C800946EF8 /* XCTHealthKit */ = {
isa = XCSwiftPackageProductDependency;
package = 2F85828029E7784C0021D637 /* XCRemoteSwiftPackageReference "XCTHealthKit" */;
package = 97B0290E2A5710C800946EF8 /* XCRemoteSwiftPackageReference "XCTHealthKit" */;
productName = XCTHealthKit;
};
/* End XCSwiftPackageProductDependency section */
Expand Down

0 comments on commit 0a26f41

Please sign in to comment.