Skip to content

Commit 285cc36

Browse files
committed
Merge branch 'master' into feature/android-skip
2 parents 264a7f0 + 9418b33 commit 285cc36

99 files changed

Lines changed: 4021 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
//
2+
// CentralListViewModel.swift
3+
// BluetoothExplorer
4+
//
5+
// Created by Alsey Coleman Miller on 7/11/25.
6+
//
7+
8+
import Foundation
9+
import Observation
10+
import Bluetooth
11+
import GATT
12+
13+
@MainActor
14+
@Observable
15+
public final class CentralListViewModel {
16+
17+
let store: Store
18+
19+
var scanToggleTask: Task<Void, Never>?
20+
21+
public init(store: Store) {
22+
self.store = store
23+
}
24+
25+
var state: State {
26+
State(
27+
.init(
28+
store: store,
29+
didToggle: scanToggleTask != nil
30+
)
31+
)
32+
}
33+
34+
public var scanResults: [ScanResult] {
35+
state.scanResults
36+
}
37+
38+
public var isEnabled: Bool {
39+
state.isEnabled
40+
}
41+
42+
public var isScanning: Bool {
43+
state.isScanning
44+
}
45+
46+
public var canToggleScan: Bool {
47+
state.canToggleScan
48+
}
49+
50+
public func scanToggle() {
51+
scanToggleTask = Task {
52+
defer { scanToggleTask = nil }
53+
if store.isScanning {
54+
await store.stopScan()
55+
} else {
56+
do {
57+
try await store.scan()
58+
}
59+
catch {
60+
store.log(error: error)
61+
}
62+
}
63+
}
64+
}
65+
}
66+
67+
public extension CentralListViewModel {
68+
69+
struct State: Equatable, Hashable, Sendable {
70+
71+
let input: Input
72+
73+
init(_ input: Input) {
74+
self.input = input
75+
}
76+
77+
public var scanResults: [ScanResult] {
78+
input.scanResults
79+
.values
80+
.lazy
81+
.sorted(by: { $0.id.description < $1.id.description })
82+
.sorted(by: { ($0.name ?? "") < ($1.name ?? "") })
83+
.sorted(by: { $0.name != nil && $1.name == nil })
84+
.sorted(by: { $0.beacon != nil && $1.beacon == nil })
85+
.map { ScanResult($0) }
86+
}
87+
88+
public var isEnabled: Bool {
89+
input.isEnabled
90+
}
91+
92+
public var isScanning: Bool {
93+
input.isScanning
94+
}
95+
96+
public var canToggleScan: Bool {
97+
input.didToggle == false && input.isEnabled
98+
}
99+
}
100+
}
101+
102+
public extension CentralListViewModel.State {
103+
104+
struct Input: Equatable, Hashable, Sendable {
105+
106+
let scanResults: [Peripheral: Store.ScanResult]
107+
108+
let isEnabled: Bool
109+
110+
let isScanning: Bool
111+
112+
let didToggle: Bool
113+
114+
init(scanResults: [Peripheral : Store.ScanResult], isEnabled: Bool, isScanning: Bool, didToggle: Bool) {
115+
self.scanResults = scanResults
116+
self.isEnabled = isEnabled
117+
self.isScanning = isScanning
118+
self.didToggle = didToggle
119+
}
120+
121+
@MainActor
122+
init(store: Store, didToggle: Bool) {
123+
self.scanResults = store.scanResults
124+
self.isEnabled = store.isEnabled
125+
self.isScanning = store.isScanning
126+
self.didToggle = didToggle
127+
}
128+
}
129+
}
130+
131+
public extension CentralListViewModel {
132+
133+
struct ScanResult: Equatable, Hashable, Sendable, Identifiable {
134+
135+
typealias ScanData = ScanDataCache<NativeCentral.Peripheral, NativeCentral.Advertisement>
136+
137+
let scanData: ScanData
138+
139+
init(_ scanData: ScanData) {
140+
self.scanData = scanData
141+
}
142+
143+
public var id: String {
144+
scanData.id.description
145+
}
146+
147+
public var name: String {
148+
scanData.name ?? (beacon != nil ? "iBeacon" : "Unknown")
149+
}
150+
151+
public var company: String? {
152+
scanData.manufacturerData?.companyIdentifier.name
153+
}
154+
155+
public var services: String? {
156+
let services = scanData.serviceUUIDs
157+
.sorted(by: { $0.description < $1.description })
158+
.map { $0.metadata?.name ?? $0.rawValue }
159+
guard services.isEmpty == false
160+
else { return nil }
161+
return "Services: " + services.reduce("", { ($0.isEmpty ? "" : ", ") + $1 })
162+
}
163+
164+
public var beacon: CentralListViewModel.Beacon? {
165+
scanData.beacon.flatMap(Beacon.init)
166+
}
167+
}
168+
}
169+
170+
public extension CentralListViewModel {
171+
172+
struct Beacon: Equatable, Hashable, Sendable {
173+
174+
let beacon: AppleBeacon
175+
176+
init(_ beacon: AppleBeacon) {
177+
self.beacon = beacon
178+
}
179+
180+
public var uuid: String {
181+
beacon.uuid.uuidString
182+
}
183+
184+
public var major: String {
185+
"Major: 0x\(beacon.major.toHexadecimal())"
186+
}
187+
188+
public var minor: String {
189+
"Minor: 0x\(beacon.minor.toHexadecimal())"
190+
}
191+
192+
public var rssi: String {
193+
"RSSI: \(beacon.rssi)"
194+
}
195+
}
196+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// PeripheralViewModel.swift
3+
// bluetooth-explorer
4+
//
5+
// Created by Alsey Coleman Miller on 7/12/25.
6+
//
7+
8+
import Foundation
9+
import Observation
10+
import Bluetooth
11+
import GATT
12+
13+
@MainActor
14+
@Observable
15+
public final class PeripheralViewModel {
16+
17+
let store: Store
18+
19+
let peripheral: Store.Peripheral
20+
21+
init(store: Store, peripheral: Peripheral) {
22+
self.store = store
23+
self.peripheral = peripheral
24+
}
25+
26+
public convenience init(store: Store, peripheral: String) {
27+
guard let peripheral = store.scanResults.first(where: { $0.key.description == peripheral })?.key else {
28+
fatalError("Invalid peripheral: \(peripheral)")
29+
}
30+
self.init(store: store, peripheral: peripheral)
31+
}
32+
33+
var title: String {
34+
store.scanResults[peripheral]?.name ?? "Device"
35+
}
36+
37+
var isConnected: Bool {
38+
store.connected.contains(peripheral)
39+
}
40+
41+
var services: [Store.Service] {
42+
store.services[peripheral] ?? []
43+
}
44+
45+
var showActivity: Bool {
46+
store.activity[peripheral] ?? false
47+
}
48+
49+
public func connect() {
50+
51+
}
52+
53+
public func reload() {
54+
55+
}
56+
}

Sources/BluetoothExplorerUI/AsyncButton.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Alsey Coleman Miller on 23/12/21.
66
//
77

8+
#if canImport(SwiftUI)
89
import SwiftUI
910

1011
enum ActionOption: CaseIterable {
@@ -60,3 +61,4 @@ struct AsyncButton<Label: View>: View {
6061
.disabled(isDisabled)
6162
}
6263
}
64+
#endif

Sources/BluetoothExplorerUI/AttributeCell.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Alsey Coleman Miller on 19/12/21.
66
//
77

8+
#if canImport(SwiftUI)
89
import SwiftUI
910
import Bluetooth
1011
import GATT
@@ -47,3 +48,4 @@ struct AttributeCell_Preview: PreviewProvider {
4748
}
4849
}
4950
#endif
51+
#endif

Sources/BluetoothExplorerUI/AttributeValueCell.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Alsey Coleman Miller on 22/12/21.
66
//
77

8+
#if canImport(SwiftUI)
89
import Foundation
910
import Bluetooth
1011
import SwiftUI
@@ -121,3 +122,4 @@ extension AttributeValueCell {
121122
)
122123
}
123124
#endif
125+
#endif

Sources/BluetoothExplorerUI/AttributeValuesSection.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Alsey Coleman Miller on 22/12/21.
66
//
77

8+
#if canImport(SwiftUI)
89
import Foundation
910
import Bluetooth
1011
import SwiftUI
@@ -101,3 +102,4 @@ struct AttributeValuesSection_Preview: PreviewProvider {
101102
}
102103
#endif
103104
*/
105+
#endif

Sources/BluetoothExplorerUI/CharacteristicView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Alsey Coleman Miller on 22/12/21.
66
//
77

8+
#if canImport(SwiftUI)
89
import SwiftUI
910
import Bluetooth
1011
import GATT
@@ -268,3 +269,4 @@ struct CharacteristicView_Preview: PreviewProvider {
268269
}
269270
}
270271
#endif
272+
#endif

Sources/BluetoothExplorerUI/DescriptorView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Alsey Coleman Miller on 23/12/21.
66
//
77

8+
#if canImport(SwiftUI)
89
import SwiftUI
910
import Bluetooth
1011
import GATT
@@ -201,3 +202,4 @@ struct DescriptorView_Preview: PreviewProvider {
201202
}
202203
#endif
203204

205+
#endif

Sources/BluetoothExplorerUI/ServiceView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Alsey Coleman Miller on 22/12/21.
66
//
77

8+
#if canImport(SwiftUI)
89
import SwiftUI
910
import Bluetooth
1011
import GATT
@@ -146,3 +147,4 @@ struct ServiceView_Preview: PreviewProvider {
146147
}
147148
}
148149
#endif
150+
#endif

Sources/BluetoothExplorerUI/WriteAttributeView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Alsey Coleman Miller on 23/12/21.
66
//
77

8+
#if canImport(SwiftUI)
89
import Foundation
910
import SwiftUI
1011
import Bluetooth
@@ -67,3 +68,4 @@ internal extension WriteAttributeView {
6768
return Data(hexadecimal: text)
6869
}
6970
}
71+
#endif

0 commit comments

Comments
 (0)