Skip to content

Commit

Permalink
Documentation progress and resolved some feebdack
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Jul 2, 2024
1 parent 5daa2c1 commit 8af78d0
Show file tree
Hide file tree
Showing 15 changed files with 182 additions and 39 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ let package = Package(
.package(url: "https://github.com/StanfordSpezi/SpeziViews.git", from: "1.5.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziBluetooth", from: "2.0.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziNetworking", from: "2.1.1"),
.package(url: "https://github.com/StanfordBDHG/XCTestExtensions.git", .upToNextMinor(from: "0.4.11"))
.package(url: "https://github.com/StanfordBDHG/XCTestExtensions.git", .upToNextMinor(from: "0.4.12"))
] + swiftLintPackage(),
targets: [
.target(
Expand Down
1 change: 1 addition & 0 deletions Sources/SpeziDevices/Devices/PairableDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public protocol PairableDevice: GenericDevice {
/// ```
///
var connect: BluetoothConnectAction { get }

/// Disconnect action.
///
/// Use the [`DeviceAction`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/deviceaction) property wrapper to
Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziDevices/HealthMeasurements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import SwiftUI

/// Manage and process health measurements from nearby Bluetooth Peripherals.
///
/// Use the `HealthMeasurements` module to collect health measurements from nearby Bluetooth Peripherals 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.
/// - Note: Implement your device as a [`BluetoothDevice`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetoothdevice)
/// using [SpeziBluetooth](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth).
Expand Down Expand Up @@ -66,7 +66,7 @@ import SwiftUI
/// ```
///
/// - Important: Don't forget to configure the `HealthMeasurements` module in
/// your [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate)
/// your [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate).
///
/// ## Topics
///
Expand Down
11 changes: 7 additions & 4 deletions Sources/SpeziDevices/Measurements/StoredMeasurement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private struct CodableHKDevice {


/// Copy of the `BloodPressureMeasurement` type that just uses plain RawValue types to work around SwiftData coding issues and crashes.
private struct BloodPressureMeasurementCopy {
private struct BloodPressureMeasurementSwiftDataWorkaroundContainer { // swiftlint:disable:this type_name
let systolicValue: UInt16
let diastolicValue: UInt16
let meanArterialPressure: UInt16
Expand Down Expand Up @@ -75,7 +75,7 @@ private struct SwiftDataBluetoothHealthMeasurementWorkaroundContainer {

private let type: MeasurementType

private var bloodPressureMeasurement: BloodPressureMeasurementCopy?
private var bloodPressureMeasurement: BloodPressureMeasurementSwiftDataWorkaroundContainer?
private var bloodPressureFeatures: BloodPressureFeature.RawValue?

private var weightMeasurement: WeightMeasurement?
Expand Down Expand Up @@ -172,7 +172,7 @@ extension CodableHKDevice {
}


extension BloodPressureMeasurementCopy: Codable {
extension BloodPressureMeasurementSwiftDataWorkaroundContainer: Codable {
enum CodingKeys: CodingKey {
case systolicValue
case diastolicValue
Expand Down Expand Up @@ -224,7 +224,10 @@ extension SwiftDataBluetoothHealthMeasurementWorkaroundContainer: Codable {
self.type = try container.decode(SwiftDataBluetoothHealthMeasurementWorkaroundContainer.MeasurementType.self, forKey: .type)
switch type {
case .bloodPressure:
self.bloodPressureMeasurement = try container.decodeIfPresent(BloodPressureMeasurementCopy.self, forKey: .bloodPressureMeasurement)
self.bloodPressureMeasurement = try container.decodeIfPresent(
BloodPressureMeasurementSwiftDataWorkaroundContainer.self,
forKey: .bloodPressureMeasurement
)
self.bloodPressureFeatures = try container.decodeIfPresent(BloodPressureFeature.RawValue.self, forKey: .bloodPressureFeatures)
case .weight:
self.weightMeasurement = try container.decodeIfPresent(WeightMeasurement.self, forKey: .weightMeasurement)
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziDevices/Model/PairingContinuation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ final class PairingContinuation {
}


extension PairingContinuation: @unchecked Sendable {}
extension PairingContinuation {}
13 changes: 8 additions & 5 deletions Sources/SpeziDevices/PairedDevices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ import SwiftUI

/// Persistently pair with Bluetooth devices and automatically manage connections.
///
/// Use the `PairedDevices` module to discover and pair ``PairedDevices`` and automatically manage connection establishment
/// Use the `PairedDevices` module to discover and pair ``PairableDevice``s and automatically manage connection establishment
/// of connected devices.
/// - Note: Implement your device as a [`BluetoothDevice`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetoothdevice)
/// using [SpeziBluetooth](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth).
///
/// To support `PairedDevices`, you need to adopt the ``PairedDevices`` protocol for your device.
/// Optionally you can adopt ``BatteryPoweredDevice`` if your device supports the `BatteryService`.
/// To support `PairedDevices`, you need to adopt the ``PairableDevice`` protocol for your device.
/// Optionally you can adopt ``BatteryPoweredDevice`` 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 ``configure(device:accessing:_:_:)`` method.
///
/// - Important: Don't forget to configure the `PairedDevices` module in
/// your [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate).
///
/// ```swift
/// import SpeziDevices
///
Expand Down Expand Up @@ -60,13 +64,12 @@ import SwiftUI
/// }
/// ```
///
/// 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 ``DevicesTab`` view.
///
/// ## Topics
///
/// ### Configuring Paired Devices
/// - ``init()``
/// - ``init(_:)``
///
/// ### Register Devices
/// - ``configure(device:accessing:_:_:)``
Expand Down
24 changes: 22 additions & 2 deletions Sources/SpeziDevices/SpeziDevices.docc/HealthKit.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,34 @@ SPDX-License-Identifier: MIT

## Overview

<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
SpeziDevices helps developers converting measurements received from Bluetooth devices to HealthKit sample types.

### Device Information

As soon as you conform your [SpeziBluetooth `BluetoothDevice`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetoothdevice)
to the ``HealthDevice`` protocol and implement the [`DeviceInformationService`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/deviceinformationservice),
you can access the [`HKDevice`](https://developer.apple.com/documentation/healthkit/hkdevice)
description using the ``HealthDevice/hkDevice-32s1d`` property

### Converting Measurements

SpeziDevices can convert your Bluetooth Health Measurement characteristics into HealthKit samples.
This is support for characteristics like [`BloodPressureMeasurement`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/bloodpressuremeasurement)
or [`WeightMeasurement`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/weightmeasurement).

Use methods like ``SpeziBluetoothServices/BloodPressureMeasurement/bloodPressureSample(source:)`` or
``SpeziBluetoothServices/WeightMeasurement/weightSample(source:resolution:)`` to convert these measurements to their respective HealthKit Sample
representation.

> Tip: By using the [`resource`](https://swiftpackageindex.com/stanfordbdhg/healthkitonfhir/documentation/healthkitonfhir/healthkit/hksample/resource)
provided through [`HealthKitOnFHIR`](https://swiftpackageindex.com/StanfordBDHG/HealthKitOnFHIR/documentation/healthkitonfhir) you can convert
your Bluetooth measurements to [HL7 FHIR Observation Resources](http://hl7.org/fhir/R4/observation.html).

## Topics

### Device

- ``HealthDevice/hkDevice``
- ``HealthDevice/hkDevice-32s1d``

### Blood Pressure Measurement

Expand Down
110 changes: 108 additions & 2 deletions Sources/SpeziDevices/SpeziDevices.docc/SpeziDevices.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ``SpeziDevices``

<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
Support interactions with Bluetooth Devices.

<!--
Expand All @@ -14,7 +14,113 @@ SPDX-License-Identifier: MIT

## Overview

Below is a list of important symbols of SpeziDevices.
SpeziDevices abstracts common interactions with Bluetooth devices that are implemented using
[SpeziBluetooth](https://swiftpackageindex.com/StanfordSpezi/SpeziBluetooth/documentation/spezibluetooth).
It supports pairing with devices and process health measurements.

### Pairing Devices

Pairing devices is a good way of making sure that your application only connects to fixed set of devices and doesn't accept data from
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.

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
[`BatteryService`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetoothservices/batteryservice).
Once your device is loaded, register it with the `PairedDevices` module by calling the ``configure(device:accessing:_:_:)`` method.


> Important: Don't forget to configure the `PairedDevices` module in
your [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate).

```swift
import SpeziDevices

class MyDevice: PairableDevice {
@DeviceState(\.id) var id
@DeviceState(\.name) var name
@DeviceState(\.state) var state
@DeviceState(\.advertisementData) var advertisementData
@DeviceState(\.nearby) var nearby

@Service var deviceInformation = DeviceInformationService()

@DeviceAction(\.connect) var connect
@DeviceAction(\.disconnect) var disconnect

var isInPairingMode: Bool {
// determine if a nearby device is in pairing mode
}

@Dependency private var pairedDevices: PairedDevices?

required init() {}

func configure() {
pairedDevices?.configure(device: self, accessing: $state, $advertisementData, $nearby)
}

func handleSuccessfulPairing() { // called on events where a device can be considered paired (e.g., incoming notifications)
pairedDevices?.signalDevicePaired(self)
}
}
```

> Tip: To display and manage paired devices and support adding new paired devices, you can use the full-featured ``DevicesView`` view.
### Health Measurements

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:)`.

```swift
import SpeziDevices

class MyDevice: HealthDevice {
@Service var deviceInformation = DeviceInformationService()
@Service var weightScale = WeightScaleService()

@Dependency private var measurements: HealthMeasurements?

required init() {}

func configure() {
measurements?.configureReceivingMeasurements(for: self, on: weightScale)
}
}
```

To display new measurements to the user and save them to your external data store, you can use ``MeasurementsRecordedSheet``.
Below is a short code example.

```swift
import SpeziDevices
import SpeziDevicesUI

struct MyHomeView: View {
@Environment(HealthMeasurements.self) private var measurements

var body: some View {
@Bindable var measurements = measurements
ContentView()
.sheet(isPresented: $measurements.shouldPresentMeasurements) {
MeasurementsRecordedSheet { measurement in
// handle saving the measurement
}
}
}
}
```

> Important: Don't forget to configure the `HealthMeasurements` module in
your [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate).

## Topics

Expand Down
9 changes: 1 addition & 8 deletions Sources/SpeziDevicesUI/Devices/DevicesGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,24 +62,17 @@ public struct DevicesGrid: View {
ProgressView()
}
}
.navigationTitle("Devices")
.navigationDestination(item: $detailedDeviceInfo) { deviceInfo in
DeviceDetailsView(deviceInfo)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Add Device", systemImage: "plus") {
presentingDevicePairing = true
}
}
}
}


/// Create a new devices grid.
/// - Parameters:
/// - devices: The list of paired devices to display.
/// - presentingDevicePairing: Binding to indicate if the device discovery menu should be presented.
/// The view shows an `ContentUnavailableView` if no paired devices exists and uses the binding to provide an action that present device pairing.
public init(devices: [PairedDeviceInfo]?, presentingDevicePairing: Binding<Bool>) {
// swiftlint:disable:previous discouraged_optional_collection
self.devices = devices
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import SpeziDevices
import SwiftUI


/// Devices tab showing grid of paired devices and functionality to pair new devices.
/// Devices view showing grid of paired devices and provides functionality to pair new devices.
///
/// - Note: Make sure to place this view into an `NavigationStack`.
public struct DevicesTab<PairingHint: View>: View {
public struct DevicesView<PairingHint: View>: View {
private let appName: String
private let pairingHint: PairingHint

Expand All @@ -25,17 +25,25 @@ public struct DevicesTab<PairingHint: View>: View {
@Bindable var pairedDevices = pairedDevices

DevicesGrid(devices: pairedDevices.pairedDevices, presentingDevicePairing: $pairedDevices.shouldPresentDevicePairing)
.navigationTitle("Devices")
// automatically search if no devices are paired
.scanNearbyDevices(enabled: pairedDevices.isScanningForNearbyDevices, with: bluetooth)
.sheet(isPresented: $pairedDevices.shouldPresentDevicePairing) {
AccessorySetupSheet(pairedDevices.discoveredDevices.values, appName: appName) {
pairingHint
}
}
.toolbar {
// indicate that we are scanning in the background
if pairedDevices.isScanningForNearbyDevices && !pairedDevices.shouldPresentDevicePairing {
ProgressView()
.toolbar { // TODO: verify order!

Check failure on line 36 in Sources/SpeziDevicesUI/Devices/DevicesView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (verify order!) (todo)
ToolbarItem(placement: .primaryAction) {
// indicate that we are scanning in the background
if pairedDevices.isScanningForNearbyDevices && !pairedDevices.shouldPresentDevicePairing {
ProgressView()
}
}
ToolbarItem(placement: .primaryAction) {
Button("Add Device", systemImage: "plus") {
pairedDevices.shouldPresentDevicePairing = true
}
}
}
}
Expand Down Expand Up @@ -72,7 +80,7 @@ public struct DevicesTab<PairingHint: View>: View {
#if DEBUG
#Preview {
NavigationStack {
DevicesTab(appName: "Example")
DevicesView(appName: "Example")
.previewWith {
Bluetooth {}
PairedDevices()
Expand Down
1 change: 1 addition & 0 deletions Sources/SpeziDevicesUI/Pairing/PairDeviceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct PairDeviceView<Collection: RandomAccessCollection>: View where Collection
@State private var selectedDeviceId: UUID?
@State private var selectedDevice: (any PairableDevice)?


private var forcedUnwrappedDeviceId: Binding<UUID> {
Binding {
guard let selectedDeviceId else {
Expand Down
6 changes: 3 additions & 3 deletions Sources/SpeziDevicesUI/SpeziDevicesUI.docc/SpeziDevicesUI.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ``SpeziDevicesUI``

<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
Visualize Bluetooth device interactions.

<!--
Expand All @@ -14,7 +14,7 @@ SPDX-License-Identifier: MIT

## Overview

<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
SpeziDevicesUI helps you to visualize Bluetooth device state and communicate interactions to the user.

## Topics

Expand All @@ -32,7 +32,7 @@ Views that are helpful when building a nearby devices view.

### Paired Devices

- ``DevicesTab``
- ``DevicesView``
- ``DevicesGrid``
- ``DeviceTile``
- ``DeviceDetailsView``
Expand Down
Loading

0 comments on commit 8af78d0

Please sign in to comment.