Skip to content

Commit ee859a7

Browse files
NickKibishgithub-actions[bot]
authored andcommitted
Copied files from native CoreBluetooth version to CoreBluetoothMock
1 parent 6f01c77 commit ee859a7

File tree

8 files changed

+222
-14
lines changed

8 files changed

+222
-14
lines changed

Sources/iOS-BLE-Library-Mock/CentralManager/CentralManager.swift

+18-8
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ extension CentralManager {
1818
public var localizedDescription: String {
1919
switch self {
2020
case .wrongManager:
21-
return "Incorrect manager instance provided."
21+
return
22+
"Incorrect manager instance provided. Delegate must be of type ReactiveCentralManagerDelegate."
2223
case .badState(let state):
23-
return "Bad state: \(state)"
24+
return "Bad state: \(state)."
2425
case .unknownError:
2526
return "An unknown error occurred."
2627
}
@@ -53,14 +54,18 @@ private class Observer: NSObject {
5354
}
5455
}
5556

56-
/// A custom Central Manager class that extends the functionality of the standard CBCentralManager.
57-
/// This class brings a reactive approach and is based on the Swift Combine framework.
57+
/// A Custom Central Manager class.
58+
///
59+
/// It wraps the standard CBCentralManager and has similar API. However, instead of using delegate, it uses publishers, thus bringing the reactive programming paradigm to the CoreBluetooth framework.
5860
public class CentralManager {
5961
private let isScanningSubject = CurrentValueSubject<Bool, Never>(false)
6062
private let killSwitchSubject = PassthroughSubject<Void, Never>()
6163
private lazy var observer = Observer(cm: centralManager, publisher: isScanningSubject)
6264

65+
/// The underlying CBCentralManager instance.
6366
public let centralManager: CBCentralManager
67+
68+
/// The reactive delegate for the ``centralManager``.
6469
public let centralManagerDelegate: ReactiveCentralManagerDelegate
6570

6671
/// Initializes a new instance of `CentralManager`.
@@ -134,7 +139,8 @@ extension CentralManager {
134139
/// Cancels the connection with the specified peripheral.
135140
/// - Parameter peripheral: The peripheral to disconnect from.
136141
/// - Returns: A publisher that emits the disconnected peripheral.
137-
public func cancelPeripheralConnection(_ peripheral: CBPeripheral) -> Publishers.Peripheral
142+
public func cancelPeripheralConnection(_ peripheral: CBPeripheral)
143+
-> Publishers.BluetoothPublisher<CBPeripheral, Error>
138144
{
139145
return self.disconnectedPeripheralsChannel
140146
.tryFilter { r in
@@ -150,14 +156,15 @@ extension CentralManager {
150156
}
151157
.map { $0.0 }
152158
.first()
153-
.peripheral {
159+
.bluetooth {
154160
self.centralManager.cancelPeripheralConnection(peripheral)
155161
}
156162
}
157163
}
158164

159165
// MARK: Retrieving Lists of Peripherals
160166
extension CentralManager {
167+
#warning("check `connect` method")
161168
/// Returns a list of the peripherals connected to the system whose
162169
/// services match a given set of criteria.
163170
///
@@ -188,14 +195,17 @@ extension CentralManager {
188195

189196
// MARK: Scanning or Stopping Scans of Peripherals
190197
extension CentralManager {
198+
#warning("Question: Should we throw an error if the scan is already running?")
191199
/// Initiates a scan for peripherals with the specified services.
200+
///
201+
/// Calling this method stops an ongoing scan if it is already running and finishes the publisher returned by ``scanForPeripherals(withServices:)``.
202+
///
192203
/// - Parameter services: The services to scan for.
193-
/// - Returns: A publisher that emits scan results or errors.
204+
/// - Returns: A publisher that emits scan results or an error.
194205
public func scanForPeripherals(withServices services: [CBUUID]?)
195206
-> Publishers.BluetoothPublisher<ScanResult, Error>
196207
{
197208
stopScan()
198-
// TODO: Change to BluetoothPublisher
199209
return centralManagerDelegate.stateSubject
200210
.tryFirst { state in
201211
guard let determined = state.ready else { return false }

Sources/iOS-BLE-Library-Mock/CentralManager/ReactiveCentralManagerDelegate.swift

-6
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,6 @@ open class ReactiveCentralManagerDelegate: NSObject, CBCentralManagerDelegate {
6969
stateSubject.send(central.state)
7070
}
7171

72-
public func centralManager(
73-
_ central: CBCentralManager, willRestoreState dict: [String: Any]
74-
) {
75-
unimplementedError()
76-
}
77-
7872
// MARK: Monitoring the Central Manager’s Authorization
7973
#if !os(macOS)
8074
public func centralManager(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# ``iOS_BLE_Library/CentralManager``
2+
3+
### Create a Central Manager
4+
5+
Since it's not recommended to override the `CBCentralManager`'s methods, ``CentralManager`` is merely a wrapper around `CBCentralManager` with an instance of it inside.
6+
7+
The new instance of `CBCentralManager` can be created during initialization using ``init(centralManagerDelegate:queue:)``, or an existing instance can be passed using ``init(centralManager:)``.
8+
9+
If you pass a central manager inside ``init(centralManager:)``, it should already have a delegate set. The delegate should be an instance of ``ReactiveCentralManagerDelegate``; otherwise, an error will be thrown.
10+
11+
### Connection
12+
13+
Use ``CentralManager/connect(_:options:)`` to connect to a peripheral.
14+
The returned publisher will emit the connected peripheral or an error if the connection fails.
15+
The publisher will not complete until the peripheral is disconnected.
16+
If the connection fails, or the peripheral is unexpectedly disconnected, the publisher will fail with an error.
17+
18+
> The publisher returned by ``CentralManager/connect(_:options:)`` is a `ConnectablePublisher`. Therefore, you need to call `connect()` or `autoconnect()` to initiate the connection process.
19+
20+
```swift
21+
centralManager.connect(peripheral)
22+
.autoconnect()
23+
.sink { completion in
24+
switch completion {
25+
case .finished:
26+
print("Peripheral disconnected successfully")
27+
case .failure(let error):
28+
print("Error: \(error)")
29+
}
30+
} receiveValue: { peripheral in
31+
print("Peripheral connected: \(peripheral)")
32+
}
33+
.store(in: &cancellables)
34+
```
35+
36+
### Channels
37+
38+
Channels are used to pass through data from the `CBCentralManagerDelegate` methods.
39+
You can consider them as a reactive version of the `CBCentralManagerDelegate` methods.
40+
41+
In most cases, you will not need to use them directly, as `centralManager`'s methods return proper publishers. However, if you need to handle the data differently (e.g., log all the events), you can subscribe to the channels directly.
42+
43+
All channels have `Never` as their failure type because they never fail. Some channels, like `CentralManager/connectedPeripheralChannel` or `CentralManager/disconnectedPeripheralsChannel`, send tuples with the peripheral and the error, allowing you to handle errors if needed. Despite this, the failure type remains `Never`, so it will not complete even if an error occurs during the connection or disconnection of the peripheral.
44+
45+
```swift
46+
centralManager.connectedPeripheralChannel
47+
.sink { peripheral, error in
48+
if let error = error {
49+
print("Error: \(error)")
50+
} else {
51+
print("New peripheral connected: \(peripheral)"
52+
}
53+
}
54+
.store(in: &cancellables)
55+
```
56+
57+
## Topics
58+
59+
### Initializers
60+
61+
- ``init(centralManagerDelegate:queue:)``
62+
- ``init(centralManager:)``
63+
64+
### Instance Properties
65+
66+
- ``centralManager``
67+
- ``centralManagerDelegate``
68+
69+
### Scan
70+
71+
- ``scanForPeripherals(withServices:)``
72+
- ``stopScan()``
73+
- ``retrievePeripherals(withIdentifiers:)``
74+
75+
### Connection
76+
77+
- ``connect(_:options:)``
78+
- ``cancelPeripheralConnection(_:)``
79+
- ``retrieveConnectedPeripherals(withServices:)``
80+
81+
### Channels
82+
83+
- ``stateChannel``
84+
- ``isScanningChannel``
85+
- ``scanResultsChannel``
86+
- ``connectedPeripheralChannel``
87+
- ``disconnectedPeripheralsChannel``
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# ``iOS_BLE_Library/ReactiveCentralManagerDelegate``
2+
3+
Implementation of the `CBCentralManagerDelegate`.
4+
5+
`ReactiveCentralManagerDelegate` is a class that implements the `CBCentralManagerDelegate` and is an essential part of the ``CentralManager`` class.
6+
7+
It brings a reactive programming approach, utilizing Combine publishers to seamlessly handle Bluetooth events and data.
8+
This class allows to monitor and respond to events such as peripheral connection, disconnection, and scanning for peripherals.
9+
10+
It has all needed publishers that are used for handling Bluetooth events and data.
11+
12+
## Override
13+
14+
It's possible to override the default implementation of the `ReactiveCentralManagerDelegate` by creating a new class that inherits from `ReactiveCentralManagerDelegate` and overriding the needed methods.
15+
16+
However, it's important to call the `super` implementation of the method, otherwise it will break the `CentralManager` functionality.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# ``iOS_BLE_Library``
2+
3+
This library is a wrapper around the CoreBluetooth framework which provides a modern async API based on Combine Framework.
4+
5+
The library has been designed to have a simple API similar to the one provided by the CoreBluetooth framework.
6+
So if you are familiar with the CoreBluetooth framework, you will be able to use this library without any problem.
7+
8+
## Topics
9+
10+
### Central Manager
11+
- ``CentralManager``
12+
- ``ReactiveCentralManagerDelegate``
13+
14+
### Peripheral
15+
- ``Peripheral``
16+
- ``ReactivePeripheralDelegate``
17+
18+
### Essentials
19+
- ``iOS_BLE_Library/Combine/Publishers/BluetoothPublisher``
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``iOS_BLE_Library/Peripheral``
2+
3+
### Create a Peripheral
4+
5+

Sources/iOS-BLE-Library-Mock/Peripheral/Peripheral.swift

+57
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ public class Peripheral {
7474
case badDelegate
7575
}
7676

77+
/// The underlying CBPeripheral instance.
7778
public let peripheral: CBPeripheral
79+
80+
/// The delegate for handling peripheral events.
7881
public let peripheralDelegate: ReactivePeripheralDelegate
7982

8083
private let stateSubject = CurrentValueSubject<CBPeripheralState, Never>(.disconnected)
@@ -92,6 +95,11 @@ public class Peripheral {
9295
)
9396

9497
// TODO: Why don't we use default delegate?
98+
/// Initializes a Peripheral instance.
99+
///
100+
/// - Parameters:
101+
/// - peripheral: The CBPeripheral to manage.
102+
/// - delegate: The delegate for handling peripheral events.
95103
public init(peripheral: CBPeripheral, delegate: ReactivePeripheralDelegate) {
96104
self.peripheral = peripheral
97105
self.peripheralDelegate = delegate
@@ -109,13 +117,18 @@ public class Peripheral {
109117

110118
// MARK: - Channels
111119
extension Peripheral {
120+
/// A publisher for the current state of the peripheral.
112121
public var peripheralStateChannel: AnyPublisher<CBPeripheralState, Never> {
113122
stateSubject.eraseToAnyPublisher()
114123
}
115124
}
116125

117126
extension Peripheral {
118127
// TODO: Extract repeated code
128+
/// Discover services for the peripheral.
129+
///
130+
/// - Parameter serviceUUIDs: An optional array of service UUIDs to filter the discovery results. If nil, all services will be discovered.
131+
/// - Returns: A publisher emitting discovered services or an error.
119132
public func discoverServices(serviceUUIDs: [CBUUID]?)
120133
-> Publishers.BluetoothPublisher<CBService, Error>
121134
{
@@ -145,6 +158,12 @@ extension Peripheral {
145158
}
146159
}
147160

161+
/// Discover characteristics for a given service.
162+
///
163+
/// - Parameters:
164+
/// - characteristicUUIDs: An optional array of characteristic UUIDs to filter the discovery results. If nil, all characteristics will be discovered.
165+
/// - service: The service for which to discover characteristics.
166+
/// - Returns: A publisher emitting discovered characteristics or an error.
148167
public func discoverCharacteristics(
149168
_ characteristicUUIDs: [CBUUID]?, for service: CBService
150169
) -> Publishers.BluetoothPublisher<CBCharacteristic, Error> {
@@ -180,6 +199,10 @@ extension Peripheral {
180199
}
181200
}
182201

202+
/// Discover descriptors for a given characteristic.
203+
///
204+
/// - Parameter characteristic: The characteristic for which to discover descriptors.
205+
/// - Returns: A publisher emitting discovered descriptors or an error.
183206
public func discoverDescriptors(for characteristic: CBCharacteristic)
184207
-> Publishers.BluetoothPublisher<CBDescriptor, Error>
185208
{
@@ -205,6 +228,12 @@ extension Peripheral {
205228

206229
// MARK: - Writing Characteristic and Descriptor Values
207230
extension Peripheral {
231+
/// Write data to a characteristic and wait for a response.
232+
///
233+
/// - Parameters:
234+
/// - data: The data to write.
235+
/// - characteristic: The characteristic to write to.
236+
/// - Returns: A publisher indicating success or an error.
208237
public func writeValueWithResponse(_ data: Data, for characteristic: CBCharacteristic)
209238
-> Publishers.BluetoothPublisher<Void, Error>
210239
{
@@ -223,21 +252,39 @@ extension Peripheral {
223252
}
224253
}
225254

255+
/// Write data to a characteristic without waiting for a response.
256+
///
257+
/// - Parameters:
258+
/// - data: The data to write.
259+
/// - characteristic: The characteristic to write to.
226260
public func writeValueWithoutResponse(_ data: Data, for characteristic: CBCharacteristic) {
227261
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
228262
}
229263

264+
/// Write data to a descriptor.
265+
///
266+
/// - Parameters:
267+
/// - data: The data to write.
268+
/// - descriptor: The descriptor to write to.
230269
public func writeValue(_ data: Data, for descriptor: CBDescriptor) {
231270
fatalError()
232271
}
233272
}
234273

235274
// MARK: - Reading Characteristic and Descriptor Values
236275
extension Peripheral {
276+
/// Read the value of a characteristic.
277+
///
278+
/// - Parameter characteristic: The characteristic to read from.
279+
/// - Returns: A future emitting the read data or an error.
237280
public func readValue(for characteristic: CBCharacteristic) -> Future<Data?, Error> {
238281
return reader.readValue(from: characteristic)
239282
}
240283

284+
/// Listen for updates to the value of a characteristic.
285+
///
286+
/// - Parameter characteristic: The characteristic to monitor for updates.
287+
/// - Returns: A publisher emitting characteristic values or an error.
241288
public func listenValues(for characteristic: CBCharacteristic) -> AnyPublisher<Data, Error>
242289
{
243290
return peripheralDelegate.updatedCharacteristicValuesSubject
@@ -252,13 +299,23 @@ extension Peripheral {
252299
.eraseToAnyPublisher()
253300
}
254301

302+
/// Read the value of a descriptor.
303+
///
304+
/// - Parameter descriptor: The descriptor to read from.
305+
/// - Returns: A future emitting the read data or an error.
255306
public func readValue(for descriptor: CBDescriptor) -> Future<Data, Error> {
256307
fatalError()
257308
}
258309
}
259310

260311
// MARK: - Setting Notifications for a Characteristic’s Value
261312
extension Peripheral {
313+
/// Set notification state for a characteristic.
314+
///
315+
/// - Parameters:
316+
/// - isEnabled: Whether notifications should be enabled or disabled.
317+
/// - characteristic: The characteristic for which to set the notification state.
318+
/// - Returns: A publisher indicating success or an error.
262319
public func setNotifyValue(_ isEnabled: Bool, for characteristic: CBCharacteristic)
263320
-> Publishers.BluetoothPublisher<Bool, Error>
264321
{

Sources/iOS-BLE-Library-Mock/Utilities/Publishers/Publishers+Bluetooth.swift

+20
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@ extension Publisher {
1717
}
1818

1919
extension Publishers {
20+
21+
/**
22+
A publisher that is used for most of the Bluetooth operations.
23+
24+
# Overview
25+
This publisher conforms to the `ConnectablePublisher` protocol because most of the Bluetooth operations have to be set up before they can be used.
26+
27+
It means that the publisher will not emit any values until it is connected. The connection is established by calling the `connect()` or `autoconnect()` methods.
28+
To learn more about the `ConnectablePublisher` protocol, see [Apple's documentation](https://developer.apple.com/documentation/combine/connectablepublisher).
29+
30+
```swift
31+
let publisher = centralManager.scanForPeripherals(withServices: nil)
32+
.autoconnect()
33+
// chain of publishers
34+
.sink {
35+
// . . .
36+
}
37+
.store(in: &cancellables)
38+
```
39+
*/
2040
public class BluetoothPublisher<Output, Failure: Error>: ConnectablePublisher {
2141

2242
private let inner: BaseConnectable<Output, Failure>

0 commit comments

Comments
 (0)