Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Resolve a lot of todos and restructure some parts
Browse files Browse the repository at this point in the history
Supereg committed Jun 24, 2024
1 parent 02fd0c9 commit dbc96bf
Showing 16 changed files with 477 additions and 274 deletions.
18 changes: 12 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -12,7 +12,6 @@ import PackageDescription

// TODO: DOI in citation.cff

let swiftLintPlugin: Target.PluginUsage = .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")

let package = Package(
name: "SpeziDevices",
@@ -32,7 +31,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.1"),
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation", from: "1.1.1"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews.git", from: "1.4.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews.git", branch: "feature/configure-tipkit-module"),
.package(url: "https://github.com/StanfordSpezi/SpeziBluetooth", branch: "feature/accessory-discovery"),
.package(url: "https://github.com/StanfordSpezi/SpeziNetworking", from: "2.0.0"),
.package(url: "https://github.com/JWAutumn/ACarousel", .upToNextMinor(from: "0.2.0")),
@@ -47,7 +46,7 @@ let package = Package(
.product(name: "SpeziBluetooth", package: "SpeziBluetooth"),
.product(name: "SpeziBluetoothServices", package: "SpeziBluetooth")
],
plugins: [swiftLintPlugin]
plugins: [.swiftLintPlugin]
),
.target(
name: "SpeziDevicesUI",
@@ -61,7 +60,7 @@ let package = Package(
resources: [
.process("Resources")
],
plugins: [swiftLintPlugin]
plugins: [.swiftLintPlugin]
),
.target(
name: "SpeziOmron",
@@ -70,7 +69,7 @@ let package = Package(
.product(name: "SpeziBluetooth", package: "SpeziBluetooth"),
.product(name: "SpeziBluetoothServices", package: "SpeziBluetooth")
],
plugins: [swiftLintPlugin]
plugins: [.swiftLintPlugin]
),
.testTarget(
name: "SpeziOmronTests",
@@ -79,7 +78,14 @@ let package = Package(
.product(name: "SpeziBluetooth", package: "SpeziBluetooth"),
.product(name: "XCTByteCoding", package: "SpeziNetworking")
],
plugins: [swiftLintPlugin]
plugins: [.swiftLintPlugin]
)
]
)


extension Target.PluginUsage {
static var swiftLintPlugin: Target.PluginUsage {
.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")
}
}
24 changes: 23 additions & 1 deletion Sources/SpeziDevices/Devices/HealthDevice.swift
Original file line number Diff line number Diff line change
@@ -10,4 +10,26 @@ import HealthKit


/// A generic Bluetooth Health device.
public protocol HealthDevice: GenericDevice {}
public protocol HealthDevice: GenericDevice {
/// The HealthKit device description.
var hkDevice: HKDevice { get }
}


extension HealthDevice {
/// The HealthKit device description.
///
/// Default implementation using the `DeviceInformationService`.
public var hkDevice: HKDevice {
HKDevice(
name: name,
manufacturer: deviceInformation.manufacturerName,
model: deviceInformation.modelNumber,
hardwareVersion: deviceInformation.hardwareRevision,
firmwareVersion: deviceInformation.firmwareRevision,
softwareVersion: deviceInformation.softwareRevision,
localIdentifier: nil,
udiDeviceIdentifier: nil
)
}
}
26 changes: 0 additions & 26 deletions Sources/SpeziDevices/Health/HealthDevice+HKDevice.swift

This file was deleted.

63 changes: 45 additions & 18 deletions Sources/SpeziDevices/HealthMeasurements.swift
Original file line number Diff line number Diff line change
@@ -7,13 +7,20 @@
//

import Foundation
import HealthKit
import OSLog
import Spezi
import SpeziBluetooth
import SpeziBluetoothServices


/// Manage and process incoming health measurements.
///
/// ## Topics
///
/// ### Register Devices
/// - ``configureReceivingMeasurements(for:on:)-8cbd0``
/// - ``configureReceivingMeasurements(for:on:)-87sgc``
@Observable
public class HealthMeasurements { // TODO: code example?

Check warning on line 25 in Sources/SpeziDevices/HealthMeasurements.swift

GitHub Actions / Build and Test Swift Package tvOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (code example?) (todo)

Check warning on line 25 in Sources/SpeziDevices/HealthMeasurements.swift

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (code example?) (todo)

Check warning on line 25 in Sources/SpeziDevices/HealthMeasurements.swift

GitHub Actions / Build and Test Swift Package visionOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (code example?) (todo)

Check warning on line 25 in Sources/SpeziDevices/HealthMeasurements.swift

GitHub Actions / Build and Test Swift Package macOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (code example?) (todo)

Check warning on line 25 in Sources/SpeziDevices/HealthMeasurements.swift

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (code example?) (todo)

Check warning on line 25 in Sources/SpeziDevices/HealthMeasurements.swift

GitHub Actions / CodeQL / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (code example?) (todo)
private let logger = Logger(subsystem: "ENGAGEHF", category: "HealthMeasurements")
@@ -26,41 +33,61 @@ public class HealthMeasurements { // TODO: code example?
@StandardActor @ObservationIgnored private var standard: any HealthMeasurementsConstraint
@Dependency @ObservationIgnored private var bluetooth: Bluetooth?

required public init() {}
/// Initialize the Health Measurements Module.
public required init() {}

/// Configure receiving and processing weight measurements from the provided service.
///
/// Configures the device's weight measurements to be processed by the Health Measurements module.
///
/// - Parameters:
/// - device: The device on which the service is present.
/// - service: The Weight Scale service to register.
public func configureReceivingMeasurements<Device: HealthDevice>(for device: Device, on service: WeightScaleService) {
service.$weightMeasurement.onChange { [weak self, weak device, weak service] measurement in
guard let device, let service else {
let hkDevice = device.hkDevice

// make sure to not capture the device
service.$weightMeasurement.onChange { [weak self, weak service] measurement in
guard let self, let service else {
return
}
self?.handleNewMeasurement(.weight(measurement, service.features ?? []), from: device)
logger.debug("Received new weight measurement: \(String(describing: measurement))")
handleNewMeasurement(.weight(measurement, service.features ?? []), from: hkDevice)
}
}

/// Configure receiving and processing blood pressure measurements form the provided service.
///
/// Configures the device's blood pressure measurements to be processed by the Health Measurements module.
///
/// - Parameters:
/// - device: The device on which the service is present.
/// - service: The Blood Pressure service to register.
public func configureReceivingMeasurements<Device: HealthDevice>(for device: Device, on service: BloodPressureService) {
service.$bloodPressureMeasurement.onChange { [weak self, weak device, weak service] measurement in
guard let device, let service else {
let hkDevice = device.hkDevice

// make sure to not capture the device
service.$bloodPressureMeasurement.onChange { [weak self, weak service] measurement in
guard let self, let service else {
return
}
self?.handleNewMeasurement(.bloodPressure(measurement, service.features ?? []), from: device)
logger.debug("Received new blood pressure measurement: \(String(describing: measurement))")
handleNewMeasurement(.bloodPressure(measurement, service.features ?? []), from: hkDevice)
}
}

// TODO: rename! make private?
public func handleNewMeasurement<Device: HealthDevice>(_ measurement: BluetoothHealthMeasurement, from device: Device) {
let hkDevice = device.hkDevice

private func handleNewMeasurement(_ measurement: BluetoothHealthMeasurement, from source: HKDevice) {
switch measurement {
case let .weight(measurement, feature):
let sample = measurement.weightSample(source: hkDevice, resolution: feature.weightResolution)
let bmiSample = measurement.bmiSample(source: hkDevice)
let heightSample = measurement.heightSample(source: hkDevice, resolution: feature.heightResolution)
let sample = measurement.weightSample(source: source, resolution: feature.weightResolution)
let bmiSample = measurement.bmiSample(source: source)
let heightSample = measurement.heightSample(source: source, resolution: feature.heightResolution)
logger.debug("Measurement loaded: \(String(describing: measurement))")

newMeasurement = .weight(sample, bmi: bmiSample, height: heightSample)
case let .bloodPressure(measurement, _):
let bloodPressureSample = measurement.bloodPressureSample(source: hkDevice)
let heartRateSample = measurement.heartRateSample(source: hkDevice)
let bloodPressureSample = measurement.bloodPressureSample(source: source)
let heartRateSample = measurement.heartRateSample(source: source)

guard let bloodPressureSample else {
logger.debug("Discarding invalid blood pressure measurement ...")
@@ -116,7 +143,7 @@ extension HealthMeasurements {
preconditionFailure("Mock Weight Measurement was never injected!")
}

handleNewMeasurement(.weight(measurement, device.weightScale.features ?? []), from: device)
handleNewMeasurement(.weight(measurement, device.weightScale.features ?? []), from: device.hkDevice)
}

/// Call in preview simulator wrappers.
@@ -130,7 +157,7 @@ extension HealthMeasurements {
preconditionFailure("Mock Blood Pressure Measurement was never injected!")
}

handleNewMeasurement(.bloodPressure(measurement, device.bloodPressure.features ?? []), from: device)
handleNewMeasurement(.bloodPressure(measurement, device.bloodPressure.features ?? []), from: device.hkDevice)
}
}
#endif
63 changes: 53 additions & 10 deletions Sources/SpeziDevices/Model/PairedDeviceInfo.swift
Original file line number Diff line number Diff line change
@@ -10,12 +10,8 @@ import Foundation


/// Persistent information stored of a paired device.
public struct PairedDeviceInfo {
// TODO: observablen => resolves UI update issue!
// TODO: update properties (model, lastSeen, battery) with Observation framework and not via explicit calls in the device class
// => make some things have internal setters(?)
// TODO: additionalData: lastSequenceNumber: UInt16?, userDatabaseNumber: UInt32?, consentCode: UIntX

@Observable
public class PairedDeviceInfo {
/// The CoreBluetooth device identifier.
public let id: UUID
/// The device type.
@@ -28,13 +24,14 @@ public struct PairedDeviceInfo {
public let model: String?

/// The user edit-able name of the device.
public var name: String
public internal(set) var name: String
/// The date the device was last seen.
public var lastSeen: Date
public internal(set) var lastSeen: Date
/// The last reported battery percentage of the device.
public var lastBatteryPercentage: UInt8?
public internal(set) var lastBatteryPercentage: UInt8?

// TODO: how with codability? public var additionalData: [String: Any]
// TODO: additionalData: lastSequenceNumber: UInt16?, userDatabaseNumber: UInt32?, consentCode: UIntX

/// Create new paired device information.
/// - Parameters:
@@ -62,13 +59,59 @@ public struct PairedDeviceInfo {
self.lastSeen = lastSeen
self.lastBatteryPercentage = batteryPercentage
}

public required convenience init(from decoder: any Decoder) throws {

Check warning on line 63 in Sources/SpeziDevices/Model/PairedDeviceInfo.swift

GitHub Actions / Build and Test Swift Package tvOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)

Check warning on line 63 in Sources/SpeziDevices/Model/PairedDeviceInfo.swift

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)

Check warning on line 63 in Sources/SpeziDevices/Model/PairedDeviceInfo.swift

GitHub Actions / Build and Test Swift Package visionOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)

Check warning on line 63 in Sources/SpeziDevices/Model/PairedDeviceInfo.swift

GitHub Actions / Build and Test Swift Package macOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)

Check warning on line 63 in Sources/SpeziDevices/Model/PairedDeviceInfo.swift

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)

Check warning on line 63 in Sources/SpeziDevices/Model/PairedDeviceInfo.swift

GitHub Actions / CodeQL / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)
let container = try decoder.container(keyedBy: CodingKeys.self)

try self.init(
id: container.decode(UUID.self, forKey: .id),
deviceType: container.decode(String.self, forKey: .deviceType),
name: container.decode(String.self, forKey: .name),
model: container.decodeIfPresent(String.self, forKey: .name),
icon: container.decodeIfPresent(ImageReference.self, forKey: .icon),
lastSeen: container.decode(Date.self, forKey: .lastSeen),
batteryPercentage: container.decodeIfPresent(UInt8.self, forKey: .batteryPercentage)
)
}
}


extension PairedDeviceInfo: Identifiable, Codable {}
extension PairedDeviceInfo: Identifiable, Codable {
fileprivate enum CodingKeys: String, CodingKey {
case id
case deviceType
case name
case model
case icon
case lastSeen
case batteryPercentage
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(id, forKey: .id)
try container.encode(deviceType, forKey: .deviceType)
try container.encode(name, forKey: .name)
try container.encodeIfPresent(model, forKey: .model)
try container.encodeIfPresent(icon, forKey: .icon)
try container.encode(lastSeen, forKey: .lastSeen)
try container.encodeIfPresent(lastBatteryPercentage, forKey: .batteryPercentage)
}
}


extension PairedDeviceInfo: Hashable {
public static func == (lhs: PairedDeviceInfo, rhs: PairedDeviceInfo) -> Bool {
lhs.id == rhs.id
&& lhs.deviceType == rhs.deviceType
&& lhs.name == rhs.name
&& lhs.model == rhs.model
&& lhs.icon == rhs.icon
&& lhs.lastSeen == rhs.lastSeen
&& lhs.lastBatteryPercentage == rhs.lastBatteryPercentage
}

public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
85 changes: 85 additions & 0 deletions Sources/SpeziDevices/Model/SavableCollection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// 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
//

import OSLog


struct SavableCollection<Element: Codable> {
private let storage: [Element]

var values: [Element] {
storage
}

init(_ elements: [Element] = []) {
self.storage = elements
}
}


extension SavableCollection: RandomAccessCollection {
public var startIndex: Int {
storage.startIndex
}

public var endIndex: Int {
storage.endIndex
}

public func index(after index: Int) -> Int {
storage.index(after: index)
}

public subscript(position: Int) -> Element {
storage[position]
}
}


extension SavableCollection: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Element...) {
self.init(elements)
}
}


extension SavableCollection: RawRepresentable {
private static var logger: Logger {
Logger(subsystem: "edu.stanford.spezi.SpeziDevices", category: "\(Self.self)")
}

var rawValue: String {
let data: Data
do {
data = try JSONEncoder().encode(storage)
} catch {
Self.logger.error("Failed to encode \(Self.self): \(error)")
return "[]"
}
guard let rawValue = String(data: data, encoding: .utf8) else {
Self.logger.error("Failed to convert data of \(Self.self) to string: \(data)")
return "[]"
}

return rawValue
}

init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8) else {
Self.logger.error("Failed to convert string of \(Self.self) to data: \(rawValue)")
return nil
}

do {
self.storage = try JSONDecoder().decode([Element].self, from: data)
} catch {
Self.logger.error("Failed to decode \(Self.self): \(error)")
return nil
}
}
}
270 changes: 167 additions & 103 deletions Sources/SpeziDevices/PairedDevices.swift

Large diffs are not rendered by default.

8 changes: 2 additions & 6 deletions Sources/SpeziDevices/Testing/MockDevice.swift
Original file line number Diff line number Diff line change
@@ -32,9 +32,8 @@ public final class MockDevice: PairableDevice, HealthDevice {
@Service public var weightScale = WeightScaleService()

public let pairing = PairingContinuation()
public var isInPairingMode = false // TODO: control
public var isInPairingMode = false

// TODO: mandatory setup?
public init() {}
}

@@ -49,7 +48,7 @@ extension MockDevice {
/// - weightMeasurement: The weight measurement loaded into the device.
/// - weightResolution: The weight resolution to use.
/// - heightResolution: The height resolution to use.
/// - Returns: Returns the initialoued Mock Device.
/// - Returns: Returns the initialized Mock Device.
@_spi(TestingSupport)
public static func createMockDevice(
name: String = "Mock Device",
@@ -85,17 +84,14 @@ extension MockDevice {

device.$connect.inject { @MainActor [weak device] in
device?.$state.inject(.connecting)
// TODO: await device?.handleStateChange(.connecting)

try? await Task.sleep(for: .seconds(1))

device?.$state.inject(.connected)
// TODO: await device?.handleStateChange(.connected)
}

device.$disconnect.inject { @MainActor [weak device] in
device?.$state.inject(.disconnected)
// TODO: await device?.handleStateChange(.disconnected)
}

return device
48 changes: 23 additions & 25 deletions Sources/SpeziDevicesUI/Devices/DeviceDetailsView.swift
Original file line number Diff line number Diff line change
@@ -13,10 +13,11 @@ import SwiftUI

/// Show the device details of a paired device.
public struct DeviceDetailsView: View {
private let deviceInfo: PairedDeviceInfo

@Environment(\.dismiss) private var dismiss
@Environment(PairedDevices.self) private var pairedDevices

@Binding private var deviceInfo: PairedDeviceInfo
@State private var presentForgetConfirmation = false

private var image: Image {
@@ -64,7 +65,6 @@ public struct DeviceDetailsView: View {
.navigationBarTitleDisplayMode(.inline)
.confirmationDialog("Do you really want to forget this device?", isPresented: $presentForgetConfirmation, titleVisibility: .visible) {
Button("Forget Device", role: .destructive) {
// TODO: message to check for ConfigureTipKit dependency!
ForgetDeviceTip.hasRemovedPairedDevice = true
pairedDevices.forgetDevice(id: deviceInfo.id)
dismiss()
@@ -91,9 +91,11 @@ public struct DeviceDetailsView: View {
.frame(maxWidth: .infinity)
}

@ViewBuilder private var infoSection: some View {
@ViewBuilder @MainActor private var infoSection: some View {
NavigationLink {
NameEditView($deviceInfo)
NameEditView(deviceInfo) { name in
pairedDevices.updateName(for: deviceInfo, name: name)
}
} label: {
ListRow("Name") {
Text(deviceInfo.name)
@@ -110,24 +112,22 @@ public struct DeviceDetailsView: View {

/// Create a new device details view.
/// - Parameter deviceInfo: The device info of the paired device.
public init(_ deviceInfo: Binding<PairedDeviceInfo>) {
self._deviceInfo = deviceInfo
public init(_ deviceInfo: PairedDeviceInfo) {
self.deviceInfo = deviceInfo
}
}


#if DEBUG
#Preview {
NavigationStack {
DeviceDetailsView(.constant(
PairedDeviceInfo(
id: UUID(),
deviceType: MockDevice.deviceTypeIdentifier,
name: "Blood Pressure Monitor",
model: "BP5250",
icon: .asset("Omron-BP5250"),
batteryPercentage: 100
)
DeviceDetailsView(PairedDeviceInfo(
id: UUID(),
deviceType: MockDevice.deviceTypeIdentifier,
name: "Blood Pressure Monitor",
model: "BP5250",
icon: .asset("Omron-BP5250"),
batteryPercentage: 100
))
}
.previewWith {
@@ -137,16 +137,14 @@ public struct DeviceDetailsView: View {

#Preview {
NavigationStack {
DeviceDetailsView(.constant(
PairedDeviceInfo(
id: UUID(),
deviceType: MockDevice.deviceTypeIdentifier,
name: "Weight Scale",
model: "SC-150",
icon: .asset("Omron-SC-150"),
lastSeen: .now.addingTimeInterval(-60 * 60 * 24),
batteryPercentage: 85
)
DeviceDetailsView(PairedDeviceInfo(
id: UUID(),
deviceType: MockDevice.deviceTypeIdentifier,
name: "Weight Scale",
model: "SC-150",
icon: .asset("Omron-SC-150"),
lastSeen: .now.addingTimeInterval(-60 * 60 * 24),
batteryPercentage: 85
))
}
.previewWith {
47 changes: 12 additions & 35 deletions Sources/SpeziDevicesUI/Devices/DevicesGrid.swift
Original file line number Diff line number Diff line change
@@ -13,10 +13,11 @@ import TipKit

/// Grid view of paired devices.
public struct DevicesGrid: View {
@Binding private var devices: [PairedDeviceInfo]
@Binding private var navigationPath: NavigationPath
private let devices: [PairedDeviceInfo]
@Binding private var presentingDevicePairing: Bool

@State private var detailedDeviceInfo: PairedDeviceInfo?


private var gridItems = [
GridItem(.adaptive(minimum: 120, maximum: 800), spacing: 12),
@@ -29,7 +30,6 @@ public struct DevicesGrid: View {
if devices.isEmpty {
ZStack {
VStack {
// TODO: message to check for ConfigureTipKit dependency!
TipView(ForgetDeviceTip.instance)
.padding([.leading, .trailing], 20)
Spacer()
@@ -43,11 +43,11 @@ public struct DevicesGrid: View {
.tipBackground(Color(uiColor: .secondarySystemGroupedBackground))

LazyVGrid(columns: gridItems) {
ForEach($devices) { device in
ForEach(devices) { device in
Button {
navigationPath.append(device)
detailedDeviceInfo = device
} label: {
DeviceTile(device.wrappedValue)
DeviceTile(device)
}
.foregroundStyle(.primary)
}
@@ -59,8 +59,8 @@ public struct DevicesGrid: View {
}
}
.navigationTitle("Devices")
.navigationDestination(for: Binding<PairedDeviceInfo>.self) { deviceInfo in
DeviceDetailsView(deviceInfo) // TODO: prevents updates :(
.navigationDestination(item: $detailedDeviceInfo) { deviceInfo in
DeviceDetailsView(deviceInfo)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
@@ -75,38 +75,19 @@ public struct DevicesGrid: View {
/// Create a new devices grid.
/// - Parameters:
/// - devices: The list of paired devices to display.
/// - navigation: Binding for the navigation path.
/// - presentingDevicePairing: Binding to indicate if the device discovery menu should be presented.
public init(devices: Binding<[PairedDeviceInfo]>, navigation: Binding<NavigationPath>, presentingDevicePairing: Binding<Bool>) {
// TODO: This Interface is probably not great for public interface
self._devices = devices
self._navigationPath = navigation
public init(devices: [PairedDeviceInfo], presentingDevicePairing: Binding<Bool>) {
self.devices = devices
self._presentingDevicePairing = presentingDevicePairing
}
}


// TODO: does that hurt? probably!!! we need to remove it anyways (for update issues)
extension Binding: Hashable, Equatable where Value: Hashable {
public static func == (lhs: Binding<Value>, rhs: Binding<Value>) -> Bool {
lhs.wrappedValue == rhs.wrappedValue
}

public func hash(into hasher: inout Hasher) {
hasher.combine(wrappedValue)
}
}


#if DEBUG
#Preview {
NavigationStack {
DevicesGrid(devices: .constant([]), navigation: .constant(NavigationPath()), presentingDevicePairing: .constant(false))
DevicesGrid(devices: [], presentingDevicePairing: .constant(false))
}
.onAppear {
Tips.showAllTipsForTesting()
try? Tips.configure() // TODO: use ConfigureTipKit Module
}
.previewWith {
PairedDevices()
}
@@ -119,12 +100,8 @@ extension Binding: Hashable, Equatable where Value: Hashable {
]

return NavigationStack {
DevicesGrid(devices: .constant(devices), navigation: .constant(NavigationPath()), presentingDevicePairing: .constant(false))
DevicesGrid(devices: devices, presentingDevicePairing: .constant(false))
}
.onAppear {
Tips.showAllTipsForTesting()
try? Tips.configure() // TODO: use ConfigureTipKit Module
}
.previewWith {
PairedDevices()
}
45 changes: 21 additions & 24 deletions Sources/SpeziDevicesUI/Devices/DevicesTab.swift
Original file line number Diff line number Diff line change
@@ -12,35 +12,30 @@


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

@Environment(Bluetooth.self) private var bluetooth
@Environment(PairedDevices.self) private var pairedDevices

@State private var path = NavigationPath() // TODO: can we remove that? if so, might want to remove the NavigationStack from view!

public var body: some View {
@Bindable var pairedDevices = pairedDevices

NavigationStack(path: $path) { // TODO: not really reusable because of the navigation stack!!!
DevicesGrid(devices: $pairedDevices.pairedDevices, navigation: $path, presentingDevicePairing: $pairedDevices.shouldPresentDevicePairing)
// TODO: advertisementStaleInterval: 15
// automatically search if no devices are paired
.scanNearbyDevices(enabled: pairedDevices.isScanningForNearbyDevices, with: bluetooth)
// TODO: automatic pop-up is a bit unexpected!
.sheet(isPresented: $pairedDevices.shouldPresentDevicePairing) {
AccessorySetupSheet(pairedDevices.discoveredDevices.values, appName: appName)
}
.toolbar {
// indicate that we are scanning in the background
if pairedDevices.isScanningForNearbyDevices && !pairedDevices.shouldPresentDevicePairing {
ToolbarItem(placement: .cancellationAction) { // TODO: shall we do primary action (what about order then?)
ProgressView()
}
}
DevicesGrid(devices: pairedDevices.pairedDevices, presentingDevicePairing: $pairedDevices.shouldPresentDevicePairing)
// automatically search if no devices are paired
.scanNearbyDevices(enabled: pairedDevices.isScanningForNearbyDevices, with: bluetooth, advertisementStaleInterval: 15)
// TODO: advertisementStaleInterval: 15

Check failure on line 29 in Sources/SpeziDevicesUI/Devices/DevicesTab.swift

GitHub Actions / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (advertisementStaleInterval: 15) (todo)

Check warning on line 29 in Sources/SpeziDevicesUI/Devices/DevicesTab.swift

GitHub Actions / Build and Test Swift Package tvOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (advertisementStaleInterval: 15) (todo)

Check warning on line 29 in Sources/SpeziDevicesUI/Devices/DevicesTab.swift

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (advertisementStaleInterval: 15) (todo)

Check warning on line 29 in Sources/SpeziDevicesUI/Devices/DevicesTab.swift

GitHub Actions / Build and Test Swift Package visionOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (advertisementStaleInterval: 15) (todo)

Check warning on line 29 in Sources/SpeziDevicesUI/Devices/DevicesTab.swift

GitHub Actions / Build and Test Swift Package macOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (advertisementStaleInterval: 15) (todo)

Check warning on line 29 in Sources/SpeziDevicesUI/Devices/DevicesTab.swift

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (advertisementStaleInterval: 15) (todo)

Check warning on line 29 in Sources/SpeziDevicesUI/Devices/DevicesTab.swift

GitHub Actions / CodeQL / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (advertisementStaleInterval: 15) (todo)
.sheet(isPresented: $pairedDevices.shouldPresentDevicePairing) {
AccessorySetupSheet(pairedDevices.discoveredDevices.values, appName: appName)
}
.toolbar {
// indicate that we are scanning in the background
if pairedDevices.isScanningForNearbyDevices && !pairedDevices.shouldPresentDevicePairing {
ProgressView()
}
}
}
}

/// Create a new devices tab
@@ -53,10 +48,12 @@

#if DEBUG
#Preview {
DevicesTab(appName: "Example")
.previewWith {
Bluetooth {}
PairedDevices()
}
NavigationStack {
DevicesTab(appName: "Example")
.previewWith {
Bluetooth {}
PairedDevices()
}
}
}
#endif
33 changes: 18 additions & 15 deletions Sources/SpeziDevicesUI/Devices/NameEditView.swift
Original file line number Diff line number Diff line change
@@ -12,9 +12,11 @@ import SwiftUI


struct NameEditView: View {
private let deviceInfo: PairedDeviceInfo
private let save: (String) -> Void

@Environment(\.dismiss) private var dismiss

@Binding private var deviceInfo: PairedDeviceInfo
@State private var name: String

@ValidationState private var validation
@@ -30,17 +32,18 @@ struct NameEditView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button("Done") {
deviceInfo.name = name
save(name)
dismiss()
}
.disabled(deviceInfo.name == name || !validation.allInputValid)
}
}


init(_ deviceInfo: Binding<PairedDeviceInfo>) {
self._deviceInfo = deviceInfo
self._name = State(wrappedValue: deviceInfo.wrappedValue.name)
init(_ deviceInfo: PairedDeviceInfo, save: @escaping (String) -> Void) {
self.deviceInfo = deviceInfo
self.save = save
self._name = State(wrappedValue: deviceInfo.name)
}
}

@@ -57,16 +60,16 @@ extension ValidationRule {
#if DEBUG
#Preview {
NavigationStack {
NameEditView(.constant(
PairedDeviceInfo(
id: UUID(),
deviceType: MockDevice.deviceTypeIdentifier,
name: "Blood Pressure Monitor",
model: "BP5250",
icon: .asset("Omron-BP5250"),
batteryPercentage: 100
)
))
NameEditView(PairedDeviceInfo(
id: UUID(),
deviceType: MockDevice.deviceTypeIdentifier,
name: "Blood Pressure Monitor",
model: "BP5250",
icon: .asset("Omron-BP5250"),
batteryPercentage: 100
)) { name in
print("New Name is \(name)")
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -38,7 +38,9 @@
Text("Measurement Recorded")
.font(.title)
.fixedSize(horizontal: false, vertical: true)
// TODO: subtitle with the date of the measurement?

Check failure on line 41 in Sources/SpeziDevicesUI/Measurements/MeasurementRecordedSheet.swift

GitHub Actions / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (subtitle with the date of the ...) (todo)
} content: {
// TODO: caoursel!

Check failure on line 43 in Sources/SpeziDevicesUI/Measurements/MeasurementRecordedSheet.swift

GitHub Actions / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (caoursel!) (todo)
MeasurementLayer(measurement: measurement)
} action: {
ConfirmMeasurementButton(viewState: $viewState) {
14 changes: 12 additions & 2 deletions Sources/SpeziDevicesUI/Pairing/AccessorySetupSheet.swift
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
// SPDX-License-Identifier: MIT
//

import OSLog
import SpeziBluetooth
@_spi(TestingSupport) import SpeziDevices
import SpeziViews
@@ -14,6 +15,10 @@

/// Accessory Setup view displayed in a sheet.
public struct AccessorySetupSheet<Collection: RandomAccessCollection>: View where Collection.Element == any PairableDevice {
private static var logger: Logger {
Logger(subsystem: "edu.stanford.sepzi.SpeziDevices", category: "AccessorySetupSheet")
}

private let devices: Collection
private let appName: String

@@ -26,14 +31,19 @@
public var body: some View {
NavigationStack {
VStack {
// TODO: make ONE PaneContent? => animation of image transfer?

Check failure on line 34 in Sources/SpeziDevicesUI/Pairing/AccessorySetupSheet.swift

GitHub Actions / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (make ONE PaneContent? => anima...) (todo)

Check warning on line 34 in Sources/SpeziDevicesUI/Pairing/AccessorySetupSheet.swift

GitHub Actions / Build and Test Swift Package tvOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (make ONE PaneContent? => anima...) (todo)

Check warning on line 34 in Sources/SpeziDevicesUI/Pairing/AccessorySetupSheet.swift

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (make ONE PaneContent? => anima...) (todo)

Check warning on line 34 in Sources/SpeziDevicesUI/Pairing/AccessorySetupSheet.swift

GitHub Actions / Build and Test Swift Package macOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (make ONE PaneContent? => anima...) (todo)

Check warning on line 34 in Sources/SpeziDevicesUI/Pairing/AccessorySetupSheet.swift

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (make ONE PaneContent? => anima...) (todo)

Check warning on line 34 in Sources/SpeziDevicesUI/Pairing/AccessorySetupSheet.swift

GitHub Actions / CodeQL / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (make ONE PaneContent? => anima...) (todo)
if case let .error(error) = pairingState {
PairingFailureView(error)
} else if case let .paired(device) = pairingState {
PairedDeviceView(device, appName: appName)
} else if !devices.isEmpty {
PairDeviceView(devices: devices, appName: appName, state: $pairingState) { device in
try await device.pair()
do {
try await device.pair()
} catch {
Self.logger.error("Failed to pair device \(device.id), \(device.name ?? "unnamed"): \(error)")
throw error
}
await pairedDevices.registerPairedDevice(device)
}
} else {
@@ -44,7 +54,7 @@
DismissButton()
}
}
.scanNearbyDevices(with: bluetooth) // TODO: advertisementStaleInterval: 15
.scanNearbyDevices(with: bluetooth, advertisementStaleInterval: 15) // TODO: advertisementStaleInterval: 15

Check failure on line 57 in Sources/SpeziDevicesUI/Pairing/AccessorySetupSheet.swift

GitHub Actions / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (advertisementStaleInterval: 15) (todo)
.presentationDetents([.medium])
.presentationCornerRadius(25)
.interactiveDismissDisabled()
3 changes: 1 addition & 2 deletions Sources/SpeziDevicesUI/Pairing/PairDeviceView.swift
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ struct PairDeviceView<Collection: RandomAccessCollection>: View where Collection
guard selectedDeviceIndex < devices.count else {
return nil
}
let index = devices.index(devices.startIndex, offsetBy: selectedDeviceIndex) // TODO: compare that against end index?
let index = devices.index(devices.startIndex, offsetBy: selectedDeviceIndex)
return devices[index]
}

@@ -63,7 +63,6 @@ struct PairDeviceView<Collection: RandomAccessCollection>: View where Collection
try await pairClosure(selectedDevice)
pairingState = .paired(selectedDevice)
} catch {
print(error) // TODO: logger?
pairingState = .error(AnyLocalizedError(error: error))
}
} label: {
2 changes: 1 addition & 1 deletion Sources/SpeziDevicesUI/Tips/ForgetDeviceTip.swift
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import SwiftUI
import TipKit


struct ForgetDeviceTip: Tip { // TODO: document that the user needs to set up tip kit? We could just create a Module for that?
struct ForgetDeviceTip: Tip {
static let instance = ForgetDeviceTip()

@Parameter static var hasRemovedPairedDevice: Bool = false

0 comments on commit dbc96bf

Please sign in to comment.