Skip to content

Commit

Permalink
Upgrade Spezi to 0.8.0 and move to Observable (#13)
Browse files Browse the repository at this point in the history
# Upgrade Spezi to 0.8.0 and move to Observable

## ♻️ Current situation & Problem
This PR upgrades Spezi to the latest 0.8.0 release. Further, we migrate
our codebase to use the new observation framework.


## ⚙️ Release Notes 
* Migrate to Observation
* Updated Spezi to 0.8.0


## 📚 Documentation
Minor fixes in the documentation.


## ✅ Testing
--


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Nov 11, 2023
1 parent f8f6645 commit 30e12a0
Show file tree
Hide file tree
Showing 18 changed files with 91 additions and 67 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/monthly-markdown-link-check.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 0 additions & 4 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ SpeziHealthKit contributors
====================

* [Paul Schmiedmayer](https://github.com/PSchmiedmayer)
* [Andreas Bauer](https://github.com/Supereg)
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziHealthKit/CollectSample/CollectSample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziHealthKit/CollectSample/CollectSamples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ 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<HKSampleType>
let predicate: NSPredicate?
let deliverySetting: HealthKitDeliverySetting


/// - 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 23 additions & 16 deletions Sources/SpeziHealthKit/HealthKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) }
}()
Expand All @@ -81,20 +82,30 @@ public final class HealthKit: Module {
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) ?? [])
}
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(),
Expand Down Expand Up @@ -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()
}
}


Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziHealthKit/HealthKitConstraint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public protocol HealthKitDataSourceDescription {
var sampleTypes: Set<HKSampleType> { 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions Tests/SpeziHealthKitTests/SpeziHealthKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final class SpeziHealthKitTests: XCTestCase {
HKQuantityType(.distanceWalkingRunning)
]

let healthKitComponent = HealthKit {
let healthKitModule = HealthKit {
CollectSamples(
collectedSamples,
deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch)
Expand All @@ -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``
Expand All @@ -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``
Expand All @@ -53,6 +53,6 @@ final class SpeziHealthKitTests: XCTestCase {
forKey: UserDefaults.Keys.healthKitRequestedSampleTypes
)

XCTAssert(healthKitComponent.authorized)
XCTAssert(healthKitModule.authorized)
}
}
16 changes: 14 additions & 2 deletions Tests/UITests/TestApp/ExampleStandard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}


Expand Down
15 changes: 8 additions & 7 deletions Tests/UITests/TestApp/HealthKitTestsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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()
}
}
}
2 changes: 1 addition & 1 deletion Tests/UITests/TestAppUITests/SpeziHealthKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading

0 comments on commit 30e12a0

Please sign in to comment.