diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 0666dd5..9dbe74d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -24,6 +24,16 @@ jobs: scheme: SpeziDevices-Package resultBundle: SpeziDevices-iOS.xcresult artifactname: SpeziDevices-iOS.xcresult + packageios_latest: + name: Build and Test Swift Package iOS Latest + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + with: + runsonlabels: '["macOS", "self-hosted"]' + scheme: SpeziDevices-Package + xcodeversion: latest + swiftVersion: 6 + resultBundle: SpeziDevices-iOS-Latest.xcresult + artifactname: SpeziDevices-iOS-Latest.xcresult ios: name: Build and Test iOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 @@ -33,6 +43,17 @@ jobs: scheme: TestApp resultBundle: TestApp-iOS.xcresult artifactname: TestApp-iOS.xcresult + ios_latest: + name: Build and Test iOS Latest + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + with: + runsonlabels: '["macOS", "self-hosted"]' + path: 'Tests/UITests' + scheme: TestApp + xcodeversion: latest + swiftVersion: 6 + resultBundle: TestApp-iOS-Latest.xcresult + artifactname: TestApp-iOS-Latest.xcresult codeql: name: CodeQL uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 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 4d46666..80d694c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -19,3 +19,6 @@ jobs: swiftlint: name: SwiftLint uses: StanfordBDHG/.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/CITATION.cff b/CITATION.cff index 8e4bc03..10f9678 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -16,5 +16,5 @@ authors: given-names: "Andreas" orcid: "https://orcid.org/0000-0002-1680-237X" title: "SpeziDevices" -doi: 10.5281/zenodo.7538165 +doi: 10.5281/zenodo.12627487 url: "https://github.com/StanfordSpezi/SpeziDevices" diff --git a/README.md b/README.md index e3bfda5..a358449 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,21 @@ SPDX-License-Identifier: MIT [![Build and Test](https://github.com/StanfordSpezi/SpeziDevices/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/StanfordSpezi/SpeziDevices/actions/workflows/build-and-test.yml) [![codecov](https://codecov.io/gh/StanfordSpezi/SpeziDevices/graph/badge.svg?token=pZeJyWYhAk)](https://codecov.io/gh/StanfordSpezi/SpeziDevices) - - +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.12627487.svg)](https://doi.org/10.5281/zenodo.12627487) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziDevices%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/StanfordSpezi/SpeziDevices) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziDevices%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/StanfordSpezi/SpeziDevices) Support interactions with Bluetooth Devices. ## Overview -SpeziDevices provides three different targets: `SpeziDevices`, `SpeziDevicesUI` and `SpeziOmron`. +SpeziDevices provides three different targets: [`SpeziDevices`](https://swiftpackageindex.com/StanfordSpezi/SpeziDevices/documentation/spezidevices), +[`SpeziDevicesUI`](https://swiftpackageindex.com/StanfordSpezi/SpeziDevices/documentation/spezidevicesui) +and [`SpeziOmron`](https://swiftpackageindex.com/StanfordSpezi/SpeziDevices/documentation/speziomron). + +|![Screenshot showing paired devices in a grid layout. A sheet is presented in the foreground showing a nearby devices able to pair.](Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices.png#gh-light-mode-only) ![Screenshot showing paired devices in a grid layout. A sheet is presented in the foreground showing a nearby devices able to pair.](Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices~dark.png#gh-dark-mode-only)|![Displaying the device details of a paired device with information like Model number and battery percentage.](Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails.png#gh-light-mode-only) ![Displaying the device details of a paired device with information like Model number and battery percentage.](Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails~dark.png#gh-dark-mode-only)| ![Showing a newly recorded blood pressure measurement.](Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure.png#gh-light-mode-only) ![Showing a newly recorded blood pressure measurement.](Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure~dark.png#gh-dark-mode-only) | +|:--:|:--:|:--:| +|Display paired in a grid-layout devices using [`DevicesView`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/devicesview).|Display device details using [`DeviceDetailsView`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/devicedetailsview).|Display recorded measurements using [`MeasurementsRecordedSheet`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/measurementsrecordedsheet).| ### SpeziDevices @@ -33,13 +40,18 @@ Pairing devices is a good way of making sure that your application only connects non-authorized devices. Further, it might be necessary to ensure certain operations stay secure. -Use the ``PairedDevices`` module to discover and pair ``PairableDevice``s and automatically manage connection establishment -of connected devices. +Use the [`PairedDevices`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/paireddevices) +module to discover and pair [`PairableDevice`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/pairabledevice)s +and automatically manage connection establishment of connected devices. -To support `PairedDevices`, you need to adopt the ``PairableDevice`` protocol for your device. -Optionally you can adopt the ``BatteryPoweredDevice`` protocol, if your device supports the +To support `PairedDevices`, you need to adopt the +[`PairableDevice`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/pairabledevice) protocol for your device. +Optionally you can adopt the [`BatteryPoweredDevice`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/batterypowereddevice) +protocol, if your device supports the [`BatteryService`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/batteryservice). -Once your device is loaded, register it with the `PairedDevices` module by calling the ``PairedDevices/configure(device:accessing:_:_:)`` method. +Once your device is loaded, register it with the `PairedDevices` module by calling the +[`PairedDevices/configure(device:accessing:_:_:)`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/paireddevices/configure(device:accessing:_:_:)) +method. > [!IMPORTANT] @@ -80,16 +92,18 @@ class MyDevice: PairableDevice { ``` > [!TIP] -> To display and manage paired devices and support adding new paired devices, you can use the full-featured ``DevicesView`` view. +> To display and manage paired devices and support adding new paired devices, you can use the full-featured + [`DevicesView`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/devicesview). #### Health Measurements -Use the ``HealthMeasurements`` module to collect health measurements from nearby Bluetooth devices like connected weight scales or +Use the [`HealthMeasurements`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/healthmeasurements) +module to collect health measurements from nearby Bluetooth devices like connected weight scales or blood pressure cuffs. -To support `HealthMeasurements`, you need to adopt the ``HealthDevice`` protocol for your device. +To support `HealthMeasurements`, you need to adopt the [`HealthDevice`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/healthdevice) protocol for your device. One your device is loaded, register its measurement service with the `HealthMeasurements` module -by calling a suitable variant of `configureReceivingMeasurements(for:on:)`. +by calling a suitable variant of [`configureReceivingMeasurements(for:on:)`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/healthmeasurements#register-devices). ```swift import SpeziDevices @@ -108,7 +122,8 @@ class MyDevice: HealthDevice { } ``` -To display new measurements to the user and save them to your external data store, you can use ``MeasurementsRecordedSheet``. +To display new measurements to the user and save them to your external data store, you can use +[`MeasurementsRecordedSheet`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/measurementsrecordedsheet). Below is a short code example. ```swift @@ -140,11 +155,14 @@ SpeziDevicesUI helps you to visualize Bluetooth device state and communicate int #### Displaying paired devices -When managing paired devices using ``PairedDevices``, SpeziDevicesUI provides reusable View components to display paired devices. +When managing paired devices using [`PairedDevices`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/paireddevices), +SpeziDevicesUI provides reusable View components to display paired devices. -The ``DevicesView`` provides everything you need to pair and manage paired devices. -It shows already paired devices in a grid layout using the ``DevicesGrid``. Additionally, it places an add button in the toolbar -to discover new devices using the ``AccessorySetupSheet`` view. +The [`DevicesView`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/devicesview) +provides everything you need to pair and manage paired devices. +It shows already paired devices in a grid layout using the [`DevicesGrid`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/devicesgrid). +Additionally, it places an add button in the toolbar to discover new devices using the +[`AccessorySetupSheet`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/accessorysetupsheet) view. ```swift struct MyHomeView: View { @@ -165,7 +183,9 @@ struct MyHomeView: View { #### Displaying Measurements -When managing measurements using ``HealthMeasurements``, you can use the ``MeasurementsRecordedSheet`` to display pending measurements. +When managing measurements using [`HealthMeasurements`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/healthmeasurements), +you can use the [`MeasurementsRecordedSheet`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/measurementsrecordedsheet) +to display pending measurements. Below is a short code example on how you would configure this view. ```swift @@ -195,9 +215,12 @@ device support. #### Omron Devices -The ``OmronBloodPressureCuff`` and ``OmronWeightScale`` devices provide reusable device implementations for the Omron `BP5250` blood pressure cuff +The [`OmronBloodPressureCuff`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/speziomron/omronbloodpressurecuff) +and [`OmronWeightScale`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/speziomron/omronweightscale) +devices provide reusable device implementations for the Omron `BP5250` blood pressure cuff and the Omron `SC-150` weight scale. -Both devices automatically integrate with the ``HealthMeasurements`` and ``PairedDevices`` modules of SpeziDevices. +Both devices automatically integrate with the [`HealthMeasurements`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/healthmeasurements) +and [`PairedDevices`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/paireddevices) modules of SpeziDevices. You just need to configure them for use with the [`Bluetooth`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth#Configure-the-Bluetooth-Module) module. diff --git a/Sources/SpeziDevices/HealthMeasurements.swift b/Sources/SpeziDevices/HealthMeasurements.swift index 6396394..75478db 100644 --- a/Sources/SpeziDevices/HealthMeasurements.swift +++ b/Sources/SpeziDevices/HealthMeasurements.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -@preconcurrency import HealthKit +import HealthKit import OSLog import Spezi import SpeziBluetooth @@ -43,7 +43,8 @@ import SwiftUI /// } /// ``` /// -/// To display new measurements to the user and save them to your external data store, you can use ``MeasurementsRecordedSheet``. +/// To display new measurements to the user and save them to your external data store, you can use +/// [`MeasurementsRecordedSheet`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/measurementsrecordedsheet). /// Below is a short code example. /// /// ```swift @@ -145,12 +146,12 @@ public final class HealthMeasurements: @unchecked Sendable { let hkDevice = device.hkDevice // make sure to not capture the device - service.$weightMeasurement.onChange { [weak self, weak service] measurement in + service.$weightMeasurement.onChange { @MainActor [weak self, weak service] measurement in guard let self, let service else { return } logger.debug("Received new weight measurement: \(String(describing: measurement))") - await handleNewMeasurement(.weight(measurement, service.features ?? []), from: hkDevice) + handleNewMeasurement(.weight(measurement, service.features ?? []), from: hkDevice) } } @@ -165,12 +166,12 @@ public final class HealthMeasurements: @unchecked Sendable { let hkDevice = device.hkDevice // make sure to not capture the device - service.$bloodPressureMeasurement.onChange { [weak self, weak service] measurement in + service.$bloodPressureMeasurement.onChange { @MainActor [weak self, weak service] measurement in guard let self, let service else { return } logger.debug("Received new blood pressure measurement: \(String(describing: measurement))") - await handleNewMeasurement(.bloodPressure(measurement, service.features ?? []), from: hkDevice) + handleNewMeasurement(.bloodPressure(measurement, service.features ?? []), from: hkDevice) } logger.debug("Registered device \(device.label), \(device.id) with HealthMeasurements") diff --git a/Sources/SpeziDevices/Measurements/StoredMeasurement.swift b/Sources/SpeziDevices/Measurements/StoredMeasurement.swift index d3d84b7..96fd083 100644 --- a/Sources/SpeziDevices/Measurements/StoredMeasurement.swift +++ b/Sources/SpeziDevices/Measurements/StoredMeasurement.swift @@ -155,10 +155,10 @@ private struct SwiftDataBluetoothHealthMeasurementWorkaroundContainer { final class StoredMeasurement { @Attribute(.unique) var associatedMeasurement: UUID - private let measurement: SwiftDataBluetoothHealthMeasurementWorkaroundContainer - fileprivate let codableDevice: CodableHKDevice - - let storageDate: Date + private var measurement: SwiftDataBluetoothHealthMeasurementWorkaroundContainer + fileprivate var codableDevice: CodableHKDevice + + var storageDate: Date var device: HKDevice { codableDevice.hkDevice diff --git a/Sources/SpeziDevices/Model/PairedDeviceInfo.swift b/Sources/SpeziDevices/Model/PairedDeviceInfo.swift index 64ca7da..17a3d9c 100644 --- a/Sources/SpeziDevices/Model/PairedDeviceInfo.swift +++ b/Sources/SpeziDevices/Model/PairedDeviceInfo.swift @@ -14,13 +14,13 @@ import SwiftData @Model public final class PairedDeviceInfo { /// The CoreBluetooth device identifier. - @Attribute(.unique) public let id: UUID + @Attribute(.unique) public var id: UUID /// The device type. /// /// Stores the associated ``PairableDevice/deviceTypeIdentifier-9wsed`` device type used to locate the device implementation. - public let deviceType: String + public var deviceType: String /// A model string of the device. - public let model: String? + public var model: String? /// The user edit-able name of the device. public internal(set) var name: String @@ -30,7 +30,7 @@ public final class PairedDeviceInfo { public internal(set) var lastBatteryPercentage: UInt8? /// The date at which the device was paired. - public let pairedAt: Date + public var pairedAt: Date /// Could not retrieve the device from the Bluetooth central. @Transient public internal(set) var notLocatable: Bool = false diff --git a/Sources/SpeziDevices/PairedDevices.swift b/Sources/SpeziDevices/PairedDevices.swift index 0a1dd97..fc7b390 100644 --- a/Sources/SpeziDevices/PairedDevices.swift +++ b/Sources/SpeziDevices/PairedDevices.swift @@ -64,7 +64,8 @@ import SwiftUI /// } /// ``` /// -/// - Tip: To display and manage paired devices and support adding new paired devices, you can use the full-featured ``DevicesTab`` view. +/// - Tip: To display and manage paired devices and support adding new paired devices, you can use the full-featured +/// [`DevicesView`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/devicesview). /// /// ## Topics /// @@ -213,6 +214,13 @@ public final class PairedDevices: @unchecked Sendable { _ advertisements: DeviceStateAccessor, _ nearby: DeviceStateAccessor ) { + if bluetooth?.configuredPairableDevices[Device.deviceTypeIdentifier] == nil { + logger.warning(""" + Device \(Device.self) was configured with the PairedDevices module but wasn't configured with the Bluetooth module. \ + The device won't be able to be retrieved on a fresh app start. Please make sure the device is configured with Bluetooth. + """) + } + state.onChange { [weak self, weak device] oldValue, newValue in if let device { await self?.handleDeviceStateUpdated(device, old: oldValue, new: newValue) @@ -382,10 +390,12 @@ extension PairedDevices { await device.connect() let id = device.id - async let _ = withTimeout(of: timeout) { @MainActor in - ongoingPairings.removeValue(forKey: id)?.signalTimeout() + let timeoutHandler = { @Sendable @MainActor in + _ = self.ongoingPairings.removeValue(forKey: id)?.signalTimeout() } + async let _ = withTimeout(of: timeout, perform: timeoutHandler) + try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in ongoingPairings[id] = PairingContinuation(continuation) @@ -670,7 +680,7 @@ extension Bluetooth { extension PairableDevice { fileprivate static func retrieveDevice(from bluetooth: Bluetooth, with id: UUID) async -> Self? { - await bluetooth.retrieveDevice(for: id) + await bluetooth.retrieveDevice(for: id, as: Self.self) } } diff --git a/Sources/SpeziDevices/SpeziDevices.docc/SpeziDevices.md b/Sources/SpeziDevices/SpeziDevices.docc/SpeziDevices.md index 5d8fc92..2b9ae17 100644 --- a/Sources/SpeziDevices/SpeziDevices.docc/SpeziDevices.md +++ b/Sources/SpeziDevices/SpeziDevices.docc/SpeziDevices.md @@ -69,16 +69,18 @@ class MyDevice: PairableDevice { } ``` -> Tip: To display and manage paired devices and support adding new paired devices, you can use the full-featured ``DevicesView`` view. +> Tip: To display and manage paired devices and support adding new paired devices, you can use the full-featured +[`DevicesView`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/devicesview). ### Health Measurements -Use the ``HealthMeasurements`` module to collect health measurements from nearby Bluetooth devices like connected weight scales or +Use the ``HealthMeasurements`` +module to collect health measurements from nearby Bluetooth devices like connected weight scales or blood pressure cuffs. To support `HealthMeasurements`, you need to adopt the ``HealthDevice`` protocol for your device. One your device is loaded, register its measurement service with the `HealthMeasurements` module -by calling a suitable variant of `configureReceivingMeasurements(for:on:)`. +by calling a suitable variant of [`configureReceivingMeasurements(for:on:)`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/healthmeasurements#register-devices). ```swift import SpeziDevices @@ -97,7 +99,8 @@ class MyDevice: HealthDevice { } ``` -To display new measurements to the user and save them to your external data store, you can use ``MeasurementsRecordedSheet``. +To display new measurements to the user and save them to your external data store, you can use + [`MeasurementsRecordedSheet`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/measurementsrecordedsheet). Below is a short code example. ```swift diff --git a/Sources/SpeziDevicesUI/Measurements/MeasurementsRecordedSheet.swift b/Sources/SpeziDevicesUI/Measurements/MeasurementsRecordedSheet.swift index e9ce9c6..d97f306 100644 --- a/Sources/SpeziDevicesUI/Measurements/MeasurementsRecordedSheet.swift +++ b/Sources/SpeziDevicesUI/Measurements/MeasurementsRecordedSheet.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -@preconcurrency import HealthKit +import HealthKit import OSLog @_spi(TestingSupport) import SpeziDevices import SpeziViews @@ -15,10 +15,11 @@ import SwiftUI /// A sheet view displaying one or many newly recorded measurements. /// -/// This view retrieves the pending measurements from the ``HealthMeasurements`` Module that is present in the SwiftUI environment. +/// This view retrieves the pending measurements from the [`HealthMeasurements`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/healthmeasurements) +/// Module that is present in the SwiftUI environment. public struct MeasurementsRecordedSheet: View { private let logger = Logger(subsystem: "edu.stanford.spezi.SpeziDevices", category: "MeasurementsRecordedSheet") - private let saveSamples: ([HKSample]) async throws -> Void + private let saveSamples: @MainActor ([HKSample]) async throws -> Void @Environment(HealthMeasurements.self) private var measurements @Environment(\.dismiss) private var dismiss @@ -132,7 +133,7 @@ public struct MeasurementsRecordedSheet: View { /// Create a new measurement sheet. - public init(save saveSamples: @escaping ([HKSample]) async throws -> Void) { + public init(save saveSamples: @MainActor @escaping ([HKSample]) async throws -> Void) { self.saveSamples = saveSamples } diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails.png b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails.png new file mode 100644 index 0000000..91a741f Binary files /dev/null and b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails.png differ diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails.png.license b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails.png.license new file mode 100644 index 0000000..a648e99 --- /dev/null +++ b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails.png.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails~dark.png b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails~dark.png new file mode 100644 index 0000000..a858f63 Binary files /dev/null and b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails~dark.png differ diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails~dark.png.license b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails~dark.png.license new file mode 100644 index 0000000..a648e99 --- /dev/null +++ b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/DeviceDetails~dark.png.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure.png b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure.png new file mode 100644 index 0000000..66166bd Binary files /dev/null and b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure.png differ diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure.png.license b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure.png.license new file mode 100644 index 0000000..a648e99 --- /dev/null +++ b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure.png.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure~dark.png b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure~dark.png new file mode 100644 index 0000000..10d9863 Binary files /dev/null and b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure~dark.png differ diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure~dark.png.license b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure~dark.png.license new file mode 100644 index 0000000..a648e99 --- /dev/null +++ b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_BloodPressure~dark.png.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight.png b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight.png new file mode 100644 index 0000000..e7e583e Binary files /dev/null and b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight.png differ diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight.png.license b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight.png.license new file mode 100644 index 0000000..a648e99 --- /dev/null +++ b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight.png.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight~dark.png b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight~dark.png new file mode 100644 index 0000000..d65aed5 Binary files /dev/null and b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight~dark.png differ diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight~dark.png.license b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight~dark.png.license new file mode 100644 index 0000000..a648e99 --- /dev/null +++ b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/MeasurementRecorded_Weight~dark.png.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices.png b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices.png new file mode 100644 index 0000000..3e30316 Binary files /dev/null and b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices.png differ diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices.png.license b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices.png.license new file mode 100644 index 0000000..a648e99 --- /dev/null +++ b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices.png.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices~dark.png b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices~dark.png new file mode 100644 index 0000000..c481ced Binary files /dev/null and b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices~dark.png differ diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices~dark.png.license b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices~dark.png.license new file mode 100644 index 0000000..a648e99 --- /dev/null +++ b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/Resources/PairedDevices~dark.png.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/SpeziDevicesUI.md b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/SpeziDevicesUI.md index 773a6f4..648d613 100644 --- a/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/SpeziDevicesUI.md +++ b/Sources/SpeziDevicesUI/SpeziDevicesUI.docc/SpeziDevicesUI.md @@ -16,9 +16,28 @@ SPDX-License-Identifier: MIT SpeziDevicesUI helps you to visualize Bluetooth device state and communicate interactions to the user. +@Row { + @Column { + @Image(source: "PairedDevices", alt: "Screenshot showing paired devices in a grid layout. A sheet is presented in the foreground showing a nearby devices able to pair.") { + Display paired in a grid-layout devices using ``DevicesView``. + } + } + @Column { + @Image(source: "DeviceDetails", alt: "Displaying the device details of a paired device with information like Model number and battery percentage.") { + Display device details using ``DeviceDetailsView``. + } + } + @Column { + @Image(source: "MeasurementRecorded_BloodPressure", alt: "Showing a newly recorded blood pressure measurement.") { + Display recorded measurements using ``MeasurementsRecordedSheet``. + } + } +} + ### Displaying paired devices -When managing paired devices using ``PairedDevices``, SpeziDevicesUI provides reusable View components to display paired devices. +When managing paired devices using [`PairedDevices`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/paireddevices), +SpeziDevicesUI provides reusable View components to display paired devices. The ``DevicesView`` provides everything you need to pair and manage paired devices. It shows already paired devices in a grid layout using the ``DevicesGrid``. Additionally, it places an add button in the toolbar @@ -43,7 +62,9 @@ struct MyHomeView: View { ### Displaying Measurements -When managing measurements using ``HealthMeasurements``, you can use the ``MeasurementsRecordedSheet`` to display pending measurements. +When managing measurements using [`HealthMeasurements`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/healthmeasurements), +you can use the [`MeasurementsRecordedSheet`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevicesui/measurementsrecordedsheet) +to display pending measurements. Below is a short code example on how you would configure this view. ```swift diff --git a/Sources/SpeziOmron/Characteristic/OmronRecordAccessOperand.swift b/Sources/SpeziOmron/Characteristic/OmronRecordAccessOperand.swift index d2c3e08..d4df99d 100644 --- a/Sources/SpeziOmron/Characteristic/OmronRecordAccessOperand.swift +++ b/Sources/SpeziOmron/Characteristic/OmronRecordAccessOperand.swift @@ -19,11 +19,13 @@ public enum OmronRecordAccessOperand { // RESPONSE - /// The general response operand used with the ``RecordAccessOpCode/responseCode`` operation. + /// The general response operand used with the [`responseCode`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/recordaccessopcode/responsecode) + /// operation. case generalResponse(RecordAccessGeneralResponse) - /// Reports the number of records in the ``RecordAccessOpCode/numberOfStoredRecordsResponse`` operation. + /// Reports the number of records in the [`numberOfStoredRecordsResponse`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/recordaccessopcode/numberofstoredrecordsresponse) + /// operation. case numberOfRecords(UInt16) - /// Reports the sequence number of the latest records in the ``BluetoothServices/RecordAccessOpCode/omronSequenceNumberOfLatestRecordsResponse`` operation. + /// Reports the sequence number of the latest records in the ``SpeziBluetoothServices/RecordAccessOpCode/omronSequenceNumberOfLatestRecordsResponse`` operation. case sequenceNumber(UInt16) } diff --git a/Sources/SpeziOmron/Characteristic/RecordAccessControlPoint+Omron.swift b/Sources/SpeziOmron/Characteristic/RecordAccessControlPoint+Omron.swift index 731d942..7ec7dc6 100644 --- a/Sources/SpeziOmron/Characteristic/RecordAccessControlPoint+Omron.swift +++ b/Sources/SpeziOmron/Characteristic/RecordAccessControlPoint+Omron.swift @@ -13,10 +13,11 @@ extension RecordAccessControlPoint { /// Report the sequence number of the latest records. /// /// Reports the the sequence number of the latest records on the peripheral. - /// The operator is ``RecordAccessOperator/null`` and no operand is used. + /// The operator is [`null`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/recordaccessoperator/null) + /// and no operand is used. /// - /// The number of stored records is returned using ``BluetoothServices/RecordAccessOpCode/omronSequenceNumberOfLatestRecordsResponse``. - /// Erroneous conditions are returned using the ``responseCode`` code. + /// The number of stored records is returned using ``SpeziBluetoothServices/RecordAccessOpCode/omronSequenceNumberOfLatestRecordsResponse``. + /// Erroneous conditions are returned using the [`responseCode`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/recordaccessopcode/responsecode). /// /// - Returns: The Record Access Control Point value. public static func reportSequenceNumberOfLatestRecords() -> RecordAccessControlPoint { diff --git a/Sources/SpeziOmron/Characteristic/RecordAccessOpCode+Omron.swift b/Sources/SpeziOmron/Characteristic/RecordAccessOpCode+Omron.swift index 830f4b1..4e90bf7 100644 --- a/Sources/SpeziOmron/Characteristic/RecordAccessOpCode+Omron.swift +++ b/Sources/SpeziOmron/Characteristic/RecordAccessOpCode+Omron.swift @@ -13,15 +13,16 @@ extension RecordAccessOpCode { /// Report the sequence number of the latest records. /// /// Reports the the sequence number of the latest records on the peripheral. - /// The operator is ``RecordAccessOperator/null`` and no operand is used. + /// The operator is [`null`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/recordaccessoperator/null) + /// and no operand is used. /// /// The number of stored records is returned using ``omronSequenceNumberOfLatestRecordsResponse``. - /// Erroneous conditions are returned using the ``responseCode`` code. + /// Erroneous conditions are returned using the [`responseCode`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/recordaccessopcode/responsecode). public static let omronReportSequenceNumberOfLatestRecords = RecordAccessOpCode(rawValue: 0x10) /// Response returning the sequence number of the latest records. /// /// This is the response code to ``omronReportSequenceNumberOfLatestRecords``. - /// The operator is ``RecordAccessOperator/null``. + /// The operator is [`null`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/recordaccessoperator/null). /// The operand contains the number of stored records as a `UInt16`. public static let omronSequenceNumberOfLatestRecordsResponse = RecordAccessOpCode(rawValue: 0x11) // swiftlint:disable:this identifier_name } diff --git a/Sources/SpeziOmron/Devices/OmronBloodPressureCuff.swift b/Sources/SpeziOmron/Devices/OmronBloodPressureCuff.swift index f4240e7..fa89d50 100644 --- a/Sources/SpeziOmron/Devices/OmronBloodPressureCuff.swift +++ b/Sources/SpeziOmron/Devices/OmronBloodPressureCuff.swift @@ -112,7 +112,7 @@ extension OmronBloodPressureCuff { let device = OmronBloodPressureCuff() device.$id.inject(UUID()) - device.$name.inject("Mock Blood Pressure Cuff") + device.$name.inject("BP5250") device.$state.inject(state) device.$nearby.inject(nearby) @@ -154,10 +154,15 @@ extension OmronBloodPressureCuff { device.$state.inject(.connecting) - try? await Task.sleep(for: .seconds(1)) + try? await Task.sleep(for: .seconds(2)) if case .connecting = device.state { device.$state.inject(.connected) + + if case .pairingMode = device.manufacturerData?.pairingMode { + try? await Task.sleep(for: .seconds(1)) + device.battery.$batteryLevel.inject(100) + } } } @@ -172,6 +177,9 @@ extension OmronBloodPressureCuff { device.battery.$batteryLevel.enableSubscriptions() device.battery.$batteryLevel.enablePeripheralSimulation() + device.time.$currentTime.enableSubscriptions() + device.time.$currentTime.enablePeripheralSimulation() + device.bloodPressure.$bloodPressureMeasurement.enableSubscriptions() device.bloodPressure.$bloodPressureMeasurement.enablePeripheralSimulation() diff --git a/Sources/SpeziOmron/Devices/OmronWeightScale.swift b/Sources/SpeziOmron/Devices/OmronWeightScale.swift index 3a35342..9312a14 100644 --- a/Sources/SpeziOmron/Devices/OmronWeightScale.swift +++ b/Sources/SpeziOmron/Devices/OmronWeightScale.swift @@ -110,7 +110,7 @@ extension OmronWeightScale { let device = OmronWeightScale() device.$id.inject(UUID()) - device.$name.inject("Mock Health Scale") + device.$name.inject("SC-150") device.$state.inject(state) device.$nearby.inject(nearby) @@ -146,10 +146,15 @@ extension OmronWeightScale { device.$state.inject(.connecting) - try? await Task.sleep(for: .seconds(1)) + try? await Task.sleep(for: .seconds(2)) if case .connecting = device.state { device.$state.inject(.connected) + + if case .pairingMode = device.manufacturerData?.pairingMode { + try? await Task.sleep(for: .seconds(1)) + device.time.$currentTime.inject(CurrentTime(time: .init(from: .now))) + } } } @@ -161,6 +166,9 @@ extension OmronWeightScale { device.$advertisementData.enableSubscriptions() device.$nearby.enableSubscriptions() + device.time.$currentTime.enableSubscriptions() + device.time.$currentTime.enablePeripheralSimulation() + device.weightScale.$weightMeasurement.enableSubscriptions() device.weightScale.$weightMeasurement.enablePeripheralSimulation() diff --git a/Sources/SpeziOmron/SpeziOmron.docc/SpeziOmron.md b/Sources/SpeziOmron/SpeziOmron.docc/SpeziOmron.md index 1f432ff..6337728 100644 --- a/Sources/SpeziOmron/SpeziOmron.docc/SpeziOmron.md +++ b/Sources/SpeziOmron/SpeziOmron.docc/SpeziOmron.md @@ -21,7 +21,8 @@ device support. The ``OmronBloodPressureCuff`` and ``OmronWeightScale`` devices provide reusable device implementations for the Omron `BP5250` blood pressure cuff and the Omron `SC-150` weight scale. -Both devices automatically integrate with the ``HealthMeasurements`` and ``PairedDevices`` modules of SpeziDevices. +Both devices automatically integrate with the [`HealthMeasurements`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/healthmeasurements) +and [`PairedDevices`](https://swiftpackageindex.com/stanfordspezi/spezidevices/documentation/spezidevices/paireddevices) modules of SpeziDevices. You just need to configure them for use with the [`Bluetooth`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth#Configure-the-Bluetooth-Module) module. diff --git a/Tests/SpeziOmronTests/SpeziOmronTests.swift b/Tests/SpeziOmronTests/SpeziOmronTests.swift index f002656..d96ee80 100644 --- a/Tests/SpeziOmronTests/SpeziOmronTests.swift +++ b/Tests/SpeziOmronTests/SpeziOmronTests.swift @@ -183,4 +183,8 @@ final class SpeziOmronTests: XCTestCase { } -extension MockDevice: OmronHealthDevice {} +#if compiler(<6) +extension SpeziDevices.MockDevice: SpeziOmron.OmronHealthDevice {} +#else +extension MockDevice: @retroactive OmronHealthDevice {} +#endif diff --git a/Tests/UITests/TestApp/DevicesTestView.swift b/Tests/UITests/TestApp/DevicesTestView.swift index 6172694..83125e9 100644 --- a/Tests/UITests/TestApp/DevicesTestView.swift +++ b/Tests/UITests/TestApp/DevicesTestView.swift @@ -6,10 +6,13 @@ // SPDX-License-Identifier: MIT // +import CoreBluetooth @_spi(APISupport) import Spezi @_spi(TestingSupport) import SpeziBluetooth +import SpeziBluetoothServices @_spi(TestingSupport) import SpeziDevices import SpeziDevicesUI +@_spi(TestingSupport) import SpeziOmron import SpeziViews import SwiftUI @@ -19,6 +22,7 @@ class MockDeviceLoading: Module, EnvironmentAccessible { init() {} + @MainActor func loadMockDevice(_ device: some PairableDevice) { spezi.loadModule(device, ownership: .external) } @@ -31,10 +35,12 @@ struct DevicesTestView: View { @State private var didRegister = false @State private var device = MockDevice.createMockDevice() + @State private var weightScale = OmronWeightScale.createMockDevice(manufacturerData: .omronManufacturerData(mode: .transferMode)) + @State private var bloodPressureCuff = OmronBloodPressureCuff.createMockDevice(manufacturerData: .omronManufacturerData(mode: .transferMode)) var body: some View { NavigationStack { - DevicesView(appName: "TestApp", pairingHint: "Enable pairing mode on the device.") + DevicesView(appName: "Example", pairingHint: "Enable pairing mode on the device.") .toolbar { ToolbarItemGroup(placement: .secondaryAction) { Button("Discover Device", systemImage: "plus.rectangle.fill.on.rectangle.fill") { @@ -43,14 +49,31 @@ struct DevicesTestView: View { } AsyncButton { await device.connect() + await weightScale.connect() + await bloodPressureCuff.connect() } label: { Label("Connect", systemImage: "cable.connector") } AsyncButton { await device.disconnect() + await weightScale.disconnect() + await bloodPressureCuff.disconnect() } label: { Label("Disconnect", systemImage: "cable.connector.slash") } + + Menu("Omron Devices", systemImage: "heart.text.square") { + Button("Discover Weight Scale", systemImage: "scalemass.fill") { + weightScale.$advertisementData.inject(AdvertisementData([ + CBAdvertisementDataManufacturerDataKey: OmronManufacturerData.omronManufacturerData(mode: .pairingMode).encode() + ])) + } + Button("Discovery Blood Pressure Cuff", systemImage: "heart.fill") { + bloodPressureCuff.$advertisementData.inject(AdvertisementData([ + CBAdvertisementDataManufacturerDataKey: OmronManufacturerData.omronManufacturerData(mode: .pairingMode).encode() + ])) + } + } } } } @@ -60,19 +83,40 @@ struct DevicesTestView: View { } moduleLoading.loadMockDevice(device) + moduleLoading.loadMockDevice(weightScale) + moduleLoading.loadMockDevice(bloodPressureCuff) + // simulator this being called in the configure method of the device pairedDevices.configure(device: device, accessing: device.$state, device.$advertisementData, device.$nearby) + pairedDevices.configure(device: weightScale, accessing: weightScale.$state, weightScale.$advertisementData, weightScale.$nearby) + pairedDevices.configure( + device: bloodPressureCuff, + accessing: bloodPressureCuff.$state, + bloodPressureCuff.$advertisementData, + bloodPressureCuff.$nearby + ) didRegister = true } } } +extension OmronManufacturerData { + static func omronManufacturerData(mode: OmronManufacturerData.PairingMode) -> OmronManufacturerData { + OmronManufacturerData(pairingMode: mode, users: [ + .init(id: 1, sequenceNumber: 2, recordsNumber: 1) + ]) + } +} + + #Preview { DevicesTestView() .previewWith { PairedDevices() MockDeviceLoading() - Bluetooth {} + Bluetooth { + Discover(MockDevice.self, by: .accessory(manufacturer: .init(rawValue: 0x01), advertising: BloodPressureService.self)) + } } } diff --git a/Tests/UITests/TestApp/MeasurementsTestView.swift b/Tests/UITests/TestApp/MeasurementsTestView.swift index 1eec233..afa43fc 100644 --- a/Tests/UITests/TestApp/MeasurementsTestView.swift +++ b/Tests/UITests/TestApp/MeasurementsTestView.swift @@ -17,17 +17,22 @@ struct MeasurementsTestView: View { @Environment(HealthMeasurements.self) private var healthMeasurements @State private var samples: [HKSample] = [] + @State private var hideUnavailableView = false var body: some View { @Bindable var healthMeasurements = healthMeasurements NavigationStack { // swiftlint:disable:this closure_body_length Group { if samples.isEmpty { - ContentUnavailableView( - "No Samples", - systemImage: "heart.text.square", - description: Text("Please add new measurements.") - ) + if !hideUnavailableView { + ContentUnavailableView( + "No Samples", + systemImage: "heart.text.square", + description: Text("Please add new measurements.") + ) + } else { + Text(verbatim: "") + } } else { List { ForEach(samples, id: \.uuid) { sample in @@ -55,6 +60,9 @@ struct MeasurementsTestView: View { Button("Simulate Blood Pressure", systemImage: "heart.fill") { healthMeasurements.loadMockBloodPressureMeasurement() } + Button("\(hideUnavailableView ? "Show" : "Hide") Unavailable View", systemImage: "macwindow.on.rectangle") { + hideUnavailableView.toggle() + } } } } diff --git a/Tests/UITests/TestAppUITests/BluetoothViewsTests.swift b/Tests/UITests/TestAppUITests/BluetoothViewsTests.swift index d67d3a4..f3d0fcc 100644 --- a/Tests/UITests/TestAppUITests/BluetoothViewsTests.swift +++ b/Tests/UITests/TestAppUITests/BluetoothViewsTests.swift @@ -17,7 +17,7 @@ class BluetoothViewsTests: XCTestCase { } @MainActor - func testBluetoothUnavailableViews() { + func testBluetoothUnavailableViews() async throws { let app = XCUIApplication() app.launch() @@ -46,6 +46,7 @@ class BluetoothViewsTests: XCTestCase { app.buttons["Open Settings"].tap() let settingsApp = XCUIApplication(bundleIdentifier: "com.apple.Preferences") + try await Task.sleep(for: .seconds(2)) XCTAssertEqual(settingsApp.state, .runningForeground) } diff --git a/Tests/UITests/TestAppUITests/PairedDevicesTests.swift b/Tests/UITests/TestAppUITests/PairedDevicesTests.swift index 7eef0b6..2f3eb02 100644 --- a/Tests/UITests/TestAppUITests/PairedDevicesTests.swift +++ b/Tests/UITests/TestAppUITests/PairedDevicesTests.swift @@ -19,7 +19,7 @@ class PairedDevicesTests: XCTestCase { @MainActor - func testTipsView() throws { + func testTipsView() async throws { let app = XCUIApplication() app.launchArguments = ["--testTips"] app.launch() @@ -33,6 +33,7 @@ class PairedDevicesTests: XCTestCase { app.buttons["Open Settings"].tap() let settingsApp = XCUIApplication(bundleIdentifier: "com.apple.Preferences") + try await Task.sleep(for: .seconds(2)) XCTAssertEqual(settingsApp.state, .runningForeground) } @@ -72,12 +73,12 @@ class PairedDevicesTests: XCTestCase { app.buttons["Discover Device"].tap() XCTAssert(app.staticTexts["Pair Accessory"].waitForExistence(timeout: 2.0)) - XCTAssert(app.staticTexts["Do you want to pair \"Mock Device\" with the TestApp app?"].exists) + XCTAssert(app.staticTexts["Do you want to pair \"Mock Device\" with the Example app?"].exists) XCTAssert(app.buttons["Pair"].exists) app.buttons["Pair"].tap() XCTAssert(app.staticTexts["Accessory Paired"].waitForExistence(timeout: 5.0)) - XCTAssert(app.staticTexts["\"Mock Device\" was successfully paired with the TestApp app."].exists) + XCTAssert(app.staticTexts["\"Mock Device\" was successfully paired with the Example app."].exists) XCTAssert(app.buttons["Done"].exists) app.buttons["Done"].tap() diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index f69e7cb..256a52b 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ A922BB1C2C2CB0AB009DD0E1 /* SpeziDevicesUI in Frameworks */ = {isa = PBXBuildFile; productRef = A922BB1B2C2CB0AB009DD0E1 /* SpeziDevicesUI */; }; A922BB1E2C2CB276009DD0E1 /* DevicesTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A922BB1D2C2CB276009DD0E1 /* DevicesTestView.swift */; }; A922BB202C2CB280009DD0E1 /* MeasurementsTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A922BB1F2C2CB280009DD0E1 /* MeasurementsTestView.swift */; }; + A92C76662C4561D900175B8E /* SpeziOmron in Frameworks */ = {isa = PBXBuildFile; productRef = A92C76652C4561D900175B8E /* SpeziOmron */; }; A959B7E62C2CBF0900ACA775 /* HKSampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A959B7E52C2CBF0900ACA775 /* HKSampleView.swift */; }; A959B7E82C2CBF1400ACA775 /* HKQuantitySampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A959B7E72C2CBF1400ACA775 /* HKQuantitySampleView.swift */; }; A959B7EB2C2CC05A00ACA775 /* HKCorrelationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A959B7EA2C2CC05A00ACA775 /* HKCorrelationView.swift */; }; @@ -75,6 +76,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A92C76662C4561D900175B8E /* SpeziOmron in Frameworks */, A922BB1C2C2CB0AB009DD0E1 /* SpeziDevicesUI in Frameworks */, 2F68C3C8292EA52000B3E12C /* SpeziDevices in Frameworks */, ); @@ -183,6 +185,7 @@ packageProductDependencies = ( 2F68C3C7292EA52000B3E12C /* SpeziDevices */, A922BB1B2C2CB0AB009DD0E1 /* SpeziDevicesUI */, + A92C76652C4561D900175B8E /* SpeziOmron */, ); productName = Example; productReference = 2F6D139228F5F384007C25D6 /* TestApp.app */; @@ -734,6 +737,10 @@ isa = XCSwiftPackageProductDependency; productName = SpeziDevicesUI; }; + A92C76652C4561D900175B8E /* SpeziOmron */ = { + isa = XCSwiftPackageProductDependency; + productName = SpeziOmron; + }; A959B7F22C2D646500ACA775 /* XCTestExtensions */ = { isa = XCSwiftPackageProductDependency; package = A959B7F12C2D646500ACA775 /* XCRemoteSwiftPackageReference "XCTestExtensions" */;