Skip to content

Commit

Permalink
Decompose Nearby Device Row
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Nov 17, 2024
1 parent 397c0e8 commit b338e02
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 86 deletions.
43 changes: 43 additions & 0 deletions Sources/SpeziDevicesUI/Scanning/ListInfoButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// 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 SwiftUI


public struct ListInfoButton: View { // TODO: move to SpeziViews!

Check failure on line 12 in Sources/SpeziDevicesUI/Scanning/ListInfoButton.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (move to SpeziViews!) (todo)
private let label: Text
private let action: () -> Void

public var body: some View {
Button(action: action) {
Label {
label
} icon: {
Image(systemName: "info.circle") // swiftlint:disable:this accessibility_label_for_image
}
}
.labelStyle(.iconOnly)
.font(.title3)
.foregroundColor(.accentColor)
.buttonStyle(.plain) // ensure button is clickable next to the other button
.accessibilityAction(named: label, action)
#if TEST || targetEnvironment(simulator)
.accessibilityHidden(true) // accessibility actions cannot be unit tested
#endif
}

public init(_ label: Text, action: @escaping () -> Void) {
self.label = label
self.action = action
}

public init(_ resource: LocalizedStringResource, action: @escaping () -> Void) {
self.label = Text(resource)
self.action = action
}
}
119 changes: 33 additions & 86 deletions Sources/SpeziDevicesUI/Scanning/NearbyDeviceRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,126 +6,71 @@
// SPDX-License-Identifier: MIT
//


import SpeziBluetooth
@_spi(TestingSupport) import SpeziDevices
import SpeziViews
import SwiftUI


/// A row that displays information of a nearby Bluetooth peripheral in a List view.
public struct NearbyDeviceRow: View {
public struct NearbyDeviceRow<Label: View>: View {
private let peripheral: any GenericBluetoothPeripheral
private let label: Label
private let devicePrimaryActionClosure: () -> Void
private let secondaryActionClosure: (() -> Void)?


var showDetailsButton: Bool {
secondaryActionClosure != nil && peripheral.state == .connected
}

var localizationSecondaryLabel: LocalizedStringResource? {
if peripheral.requiresUserAttention {
return .init("Intervention Required", bundle: .atURL(from: .module))
}
switch peripheral.state {
case .connecting:
return .init("Connecting", bundle: .atURL(from: .module))
case .connected:
return .init("Connected", bundle: .atURL(from: .module))
case .disconnecting:
return .init("Disconnecting", bundle: .atURL(from: .module))
case .disconnected:
return nil
}
}

public var body: some View {
let stack = HStack {
HStack {
Button(action: devicePrimaryAction) {
HStack {
ListRow(verbatim: peripheral.label) {
deviceSecondaryLabel
.foregroundStyle(.secondary)
}
if peripheral.state == .connecting || peripheral.state == .disconnecting {
ProgressView()
.accessibilityRemoveTraits(.updatesFrequently)
}
}
label
}
.foregroundStyle(.primary)
.tint(.primary)

if showDetailsButton {
Button(action: deviceDetailsAction) {
Label {
Text("Device Details", bundle: .module)
} icon: {
Image(systemName: "info.circle") // swiftlint:disable:this accessibility_label_for_image
}
}
.labelStyle(.iconOnly)
.font(.title3)
.buttonStyle(.plain) // ensure button is clickable next to the other button
.foregroundColor(.accentColor)
if secondaryActionClosure != nil && peripheral.state == .connected {
ListInfoButton(Text("Device Details", bundle: .module), action: deviceDetailsAction)
}
}

#if TEST || targetEnvironment(simulator)
// accessibility actions cannot be unit tested
stack
#else
stack.accessibilityRepresentation {
accessibilityRepresentation
}
#endif
.accessibilityElement(children: .combine)
}

@ViewBuilder var accessibilityRepresentation: some View {
let button = Button(action: devicePrimaryAction) {
Text(verbatim: peripheral.accessibilityLabel)
if let localizationSecondaryLabel {
Text(localizationSecondaryLabel)
}
}

if showDetailsButton {
button
.accessibilityAction(named: Text("Device Details", bundle: .module), deviceDetailsAction)
} else {
button
}
}

@ViewBuilder var deviceSecondaryLabel: some View {
if peripheral.requiresUserAttention {
Text("Requires Attention", bundle: .module)
} else {
switch peripheral.state {
case .connecting, .disconnecting:
EmptyView()
case .connected:
Text("Connected", bundle: .module)
case .disconnected:
EmptyView()
/// Create a new nearby device row.
/// - Parameters:
/// - peripheral: The nearby peripheral.
/// - primaryAction: The action that is executed when tapping the peripheral.
/// It is recommended to connect or disconnect devices when tapping on them.
/// - secondaryAction: The action that is executed when the device details button is pressed.
/// The device details button is displayed once the peripheral is connected.
public init(
peripheral: any GenericBluetoothPeripheral,
primaryAction: @escaping () -> Void,
secondaryAction: (() -> Void)? = nil
) where Label == ListRow<PeripheralLabel, PeripheralSecondaryLabel> {
self.init(peripheral: peripheral, primaryAction: primaryAction, secondaryAction: secondaryAction) {
ListRow {
PeripheralLabel(peripheral)
} content: {
PeripheralSecondaryLabel(peripheral)
}
}
}


/// Create a new nearby device row.

/// Creates a new nearby device row.
/// - Parameters:
/// - peripheral: The nearby peripheral.
/// - primaryAction: The action that is executed when tapping the peripheral.
/// It is recommended to connect or disconnect devices when tapping on them.
/// - secondaryAction: The action that is executed when the device details button is pressed.
/// The device details button is displayed once the peripheral is connected.
/// - label: The label that is displayed for the row.
public init(
peripheral: any GenericBluetoothPeripheral,
primaryAction: @escaping () -> Void,
secondaryAction: (() -> Void)? = nil
secondaryAction: (() -> Void)? = nil,
@ViewBuilder label: () -> Label
) {
self.peripheral = peripheral
self.label = label()
self.devicePrimaryActionClosure = primaryAction
self.secondaryActionClosure = secondaryAction
}
Expand Down Expand Up @@ -153,10 +98,12 @@ public struct NearbyDeviceRow: View {
NearbyDeviceRow(peripheral: MockBluetoothPeripheral(label: "MyDevice 2", state: .connected)) {
print("Clicked")
} secondaryAction: {
print("Secondary Clicked!")
}
NearbyDeviceRow(peripheral: MockBluetoothPeripheral(label: "Long MyDevice 3", state: .connected, requiresUserAttention: true)) {
print("Clicked")
} secondaryAction: {
print("Secondary Clicked!")
}
NearbyDeviceRow(peripheral: MockBluetoothPeripheral(label: "MyDevice 4", state: .disconnecting)) {
print("Clicked")
Expand Down
34 changes: 34 additions & 0 deletions Sources/SpeziDevicesUI/Scanning/PeripheralLabel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// 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 SpeziBluetooth
@_spi(TestingSupport) import SpeziDevices
import SwiftUI


public struct PeripheralLabel: View {
private let peripheral: any GenericBluetoothPeripheral

public var body: some View {
Text(peripheral.label)
.accessibilityLabel(Text(peripheral.accessibilityLabel))
}

init(_ peripheral: any GenericBluetoothPeripheral) {
self.peripheral = peripheral
}
}


#if DEBUG
#Preview {
List {
PeripheralLabel(MockBluetoothPeripheral(label: "MyDevice 1", state: .connected))
}
}
#endif
72 changes: 72 additions & 0 deletions Sources/SpeziDevicesUI/Scanning/PeripheralSecondaryLabel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// 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 SpeziBluetooth
@_spi(TestingSupport) import SpeziDevices
import SwiftUI


public struct PeripheralSecondaryLabel: View {
private let peripheral: any GenericBluetoothPeripheral

private var localizationSecondaryLabel: LocalizedStringResource? {
if peripheral.requiresUserAttention {
return .init("Intervention Required", bundle: .atURL(from: .module))
}
switch peripheral.state {
case .connecting:
return .init("Connecting", bundle: .atURL(from: .module))
case .connected:
return .init("Connected", bundle: .atURL(from: .module))
case .disconnecting:
return .init("Disconnecting", bundle: .atURL(from: .module))
case .disconnected:
return nil
}
}

public var body: some View {
Group {
if peripheral.requiresUserAttention {
Text("Requires Attention", bundle: .module)
} else {
switch peripheral.state {
case .connecting, .disconnecting:
ProgressView()
.accessibilityRemoveTraits(.updatesFrequently)
case .connected:
Text("Connected", bundle: .module)
case .disconnected:
EmptyView()
}
}
}
.accessibilityRepresentation {
if let localizationSecondaryLabel {
Text(localizationSecondaryLabel)
}
}
}

init(_ peripheral: any GenericBluetoothPeripheral) {
self.peripheral = peripheral
}
}


#if DEBUG
#Preview {
List {
PeripheralSecondaryLabel(MockBluetoothPeripheral(label: "MyDevice 1", state: .connecting))
PeripheralSecondaryLabel(MockBluetoothPeripheral(label: "MyDevice 1", state: .connected))
PeripheralSecondaryLabel(MockBluetoothPeripheral(label: "MyDevice 1", state: .connected, requiresUserAttention: true))
PeripheralSecondaryLabel(MockBluetoothPeripheral(label: "MyDevice 1", state: .disconnecting))
PeripheralSecondaryLabel(MockBluetoothPeripheral(label: "MyDevice 1", state: .disconnected))
}
}
#endif

0 comments on commit b338e02

Please sign in to comment.