Skip to content

Commit 1b67562

Browse files
committed
Updated documentation
1 parent 1768778 commit 1b67562

17 files changed

Lines changed: 180 additions & 60 deletions

.swift-format

Lines changed: 0 additions & 11 deletions
This file was deleted.

README.md

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,32 @@ It is currently supported on:
1313
iOS 13.0+
1414
macOS 10.15+
1515

16+
# Architecture Overview
17+
18+
BLECombineKit wraps CoreBluetooth with a reactive layer using Combine and providing modern Swift Concurrency (Async/Await) extensions.
19+
20+
- **BLECentralManager**: A wrapper for `CBCentralManager` that handles scanning and connecting to peripherals.
21+
- **BLEPeripheral**: A wrapper for `CBPeripheral` that facilitates service and characteristic discovery, and data operations.
22+
- **BLEService**: A wrapper for `CBService`.
23+
- **BLECharacteristic**: A wrapper for `CBCharacteristic`.
24+
- **BLEPeripheralManager**: A wrapper for `CBPeripheralManager` to act as a Bluetooth peripheral.
25+
1626
# How to use
1727

18-
As simple as creating a CBCentralManager and let the reactive magic of Combine do the rest:
28+
### Central Manager (Scanning and Connecting)
29+
30+
As simple as creating a `CBCentralManager` and letting the reactive magic of Combine do the rest:
1931

2032
```swift
2133
import CoreBluetooth
2234
import Combine
2335
import BLECombineKit
2436

25-
...
26-
2737
let centralManager = BLECombineKit.buildCentralManager(with: CBCentralManager())
2838

2939
let serviceUUID = CBUUID(string: "0x00FF")
3040
let characteristicUUID = CBUUID(string: "0xFF01")
31-
// Connect to the first peripheral that matches the given service UUID and observe a specific
32-
// characteristic in that service.
41+
3342
centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil)
3443
.first()
3544
.flatMap { $0.peripheral.connect(with: nil) }
@@ -45,37 +54,75 @@ centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil)
4554
.store(in: &disposables)
4655
```
4756

48-
And with Swift Concurrency, it would look like this:
49-
50-
```swift
51-
import CoreBluetooth
52-
import Combine
53-
import BLECombineKit
57+
### Swift Concurrency
5458

55-
...
59+
With Swift Concurrency, you can use `AsyncThrowingStream` and `async/await`:
5660

57-
let centralManager = BLECombineKit.buildCentralManager(with: CBCentralManager())
58-
59-
let serviceUUID = CBUUID(string: "0x00FF")
60-
let characteristicUUID = CBUUID(string: "0xFF01")
61-
// Connect to the first peripheral that matches the given service UUID and observe a specific
62-
// characteristic in that service.
63-
let stream = centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil)
64-
.first()
65-
.flatMap { $0.peripheral.connect(with: nil) }
66-
.flatMap { $0.discoverServices(serviceUUIDs: [serviceUUID]) }
67-
.flatMap { $0.discoverCharacteristics(characteristicUUIDs: nil) }
68-
.filter { $0.value.uuid == characteristicUUID }
69-
.flatMap { $0.observeValueUpdateAndSetNotification() }
70-
.values
61+
```swift
62+
let stream = centralManager.scanForPeripheralsStream(withServices: [serviceUUID], options: nil)
7163

7264
Task {
73-
for try await value in stream {
74-
print("Value received \(value)")
75-
}
65+
do {
66+
// Connect to all the peripherals found matching the service UUID.
67+
// This is just an example so you can also just wait for the first peripheral
68+
// found instead of using a for loop.
69+
for try await peripheral in stream {
70+
let connected = try await centralManager.connectAsync(peripheral: peripheral)
71+
let services = try await connected.discoverServicesAsync(serviceUUIDs: [serviceUUID])
72+
73+
for service in services {
74+
let characteristics = try await service.discoverCharacteristicsAsync(characteristicUUIDs: nil)
75+
// ... interact with characteristics
76+
}
77+
}
78+
} catch {
79+
print("Error: \(error)")
80+
}
7681
}
7782
```
7883

84+
### Writing Data
85+
86+
You can write data to characteristics using either Combine or Async/Await:
87+
88+
```swift
89+
// Combine
90+
characteristic.writeValue(someData, type: .withResponse)
91+
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
92+
.store(in: &disposables)
93+
94+
// Async/Await
95+
try await characteristic.writeValueAsync(someData, type: .withResponse)
96+
```
97+
98+
### Peripheral Manager (Advertising)
99+
100+
Act as a peripheral and advertise services:
101+
102+
```swift
103+
let peripheralManager = BLECombineKit.buildPeripheralManager()
104+
105+
let advertisementData: [String: Any] = [
106+
CBAdvertisementDataServiceUUIDsKey: [serviceUUID],
107+
CBAdvertisementDataLocalNameKey: "MyPeripheral"
108+
]
109+
110+
peripheralManager.startAdvertising(advertisementData)
111+
.sink(receiveCompletion: { _ in }, receiveValue: { result in
112+
print("Advertising status: \(result)")
113+
})
114+
.store(in: &disposables)
115+
```
116+
117+
# Error Handling
118+
119+
BLECombineKit provides a structured `BLEError` enum that categorizes errors from different parts of the stack:
120+
121+
- `.managerState`: Issues with Bluetooth state (powered off, unauthorized, etc.)
122+
- `.peripheral`: Issues during connection, discovery, or communication.
123+
- `.data`: Data conversion failures.
124+
- `.writeFailed`: Explicit write operation failures.
125+
79126
# Installation
80127

81128
## Swift Package Manager

Sources/BLECombineKit/Central/BLECentralManager+Async.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import Foundation
1313
@available(iOS 15, macOS 12.0, *)
1414
extension BLECentralManager {
1515

16+
/// Scans for peripherals given a set of service identifiers and options, returning an async stream of results.
17+
/// - Parameters:
18+
/// - services: Optional list of service UUIDs to scan for.
19+
/// - options: Optional scanning options.
20+
/// - Returns: An `AsyncThrowingStream` emitting discovered peripherals.
1621
public func scanForPeripheralsStream(
1722
withServices services: [CBUUID]?,
1823
options: [String: Any]?
@@ -23,6 +28,11 @@ extension BLECentralManager {
2328
return scanPublisher.asyncThrowingStream
2429
}
2530

31+
/// Connects to a peripheral asynchronously.
32+
/// - Parameters:
33+
/// - peripheral: The peripheral to connect to.
34+
/// - options: Optional connection options.
35+
/// - Returns: The connected peripheral.
2636
public func connectAsync(
2737
peripheral: BLEPeripheral,
2838
options: [String: Any]?

Sources/BLECombineKit/Central/BLEScanResult.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88

99
import Foundation
1010

11+
/// Represents the result of a peripheral scan.
1112
public struct BLEScanResult {
13+
/// The discovered peripheral.
1214
public let peripheral: BLEPeripheral
15+
/// The advertisement data associated with the scan result.
1316
public let advertisementData: [String: Any]
17+
/// The RSSI (Received Signal Strength Indicator) of the peripheral at the time of discovery.
1418
public let rssi: NSNumber
1519

1620
public init(peripheral: BLEPeripheral, advertisementData: [String: Any], rssi: NSNumber) {

Sources/BLECombineKit/Characteristic/BLECharacteristic+Async.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import Foundation
1212

1313
@available(iOS 15, macOS 12.0, *)
1414
extension BLECharacteristic {
15+
/// Reads the value for the characteristic asynchronously.
16+
/// - Returns: The read data.
1517
public func readValueAsync() async throws -> BLEData {
1618
var iterator = readValue().values.makeAsyncIterator()
1719
guard let value = try await iterator.next() else {
@@ -20,10 +22,14 @@ extension BLECharacteristic {
2022
return value
2123
}
2224

25+
/// Returns an async stream for observing value updates of the characteristic.
26+
/// - Returns: An `AsyncThrowingStream` emitting data updates.
2327
public func observeValueStream() -> AsyncThrowingStream<BLEData, Error> {
2428
return observeValue().asyncThrowingStream
2529
}
2630

31+
/// Sets notifications and returns an async stream for observing value updates of the characteristic.
32+
/// - Returns: An `AsyncThrowingStream` emitting data updates.
2733
public func observeValueUpdateAndSetNotificationStream() -> AsyncThrowingStream<BLEData, Error> {
2834
return observeValueUpdateAndSetNotification().asyncThrowingStream
2935
}

Sources/BLECombineKit/Characteristic/BLECharacteristic.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import Combine
1010
@preconcurrency import CoreBluetooth
1111

12+
/// A wrapper around `CBCharacteristic` that provides Combine-based APIs.
1213
public struct BLECharacteristic: Sendable {
14+
/// The underlying CoreBluetooth characteristic.
1315
public let value: CBCharacteristic
1416
private let peripheral: BLEPeripheral
1517

@@ -18,22 +20,35 @@ public struct BLECharacteristic: Sendable {
1820
self.peripheral = peripheral
1921
}
2022

23+
/// Reads the value of the characteristic.
24+
/// - Returns: A Publisher that emits the value or an error.
2125
public func readValue() -> AnyPublisher<BLEData, BLEError> {
2226
peripheral.readValue(for: value)
2327
}
2428

29+
/// Observes value updates for the characteristic.
30+
/// - Returns: A Publisher that emits the value whenever it updates.
2531
public func observeValue() -> AnyPublisher<BLEData, BLEError> {
2632
peripheral.observeValue(for: value)
2733
}
2834

35+
/// Observes value updates and sets the notification/indication status.
36+
/// - Returns: A Publisher that emits the value whenever it updates.
2937
public func observeValueUpdateAndSetNotification() -> AnyPublisher<BLEData, BLEError> {
3038
peripheral.observeValueUpdateAndSetNotification(for: value)
3139
}
3240

41+
/// Sets the notification/indication status for the characteristic.
42+
/// - Parameter enabled: Whether to enable or disable notifications.
3343
public func setNotifyValue(_ enabled: Bool) {
3444
peripheral.setNotifyValue(enabled, for: value)
3545
}
3646

47+
/// Writes a value to the characteristic.
48+
/// - Parameters:
49+
/// - data: The data to write.
50+
/// - type: The type of write (with or without response).
51+
/// - Returns: A Publisher that completes on success or fails with an error.
3752
public func writeValue(
3853
_ data: Data,
3954
type: CBCharacteristicWriteType

Sources/BLECombineKit/Data/BLEData.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,53 @@
88

99
import Foundation
1010

11+
/// A wrapper around `Data` that provides convenient conversion methods for common types.
1112
public struct BLEData: Sendable {
13+
/// The underlying raw data.
1214
public let value: Data
1315

1416
public init(value: Data) {
1517
self.value = value
1618
}
1719

20+
/// Converts the data to a 32-bit floating point value.
1821
public var floatValue: Float32? {
1922
self.to(type: Float32.self)
2023
}
2124

25+
/// Converts the data to a 32-bit integer value.
2226
public var intValue: Int32? {
2327
self.to(type: Int32.self)
2428
}
2529

30+
/// Converts the data to an unsigned 32-bit integer value.
2631
public var uintValue: UInt32? {
2732
self.to(type: UInt32.self)
2833
}
2934

35+
/// Converts the data to a 16-bit integer value.
3036
public var int16Value: Int16? {
3137
self.to(type: Int16.self)
3238
}
3339

40+
/// Converts the data to an unsigned 16-bit integer value.
3441
public var uint16Value: UInt16? {
3542
self.to(type: UInt16.self)
3643
}
3744

45+
/// Converts the data to an 8-bit integer value.
3846
public var int8Value: Int8? {
3947
self.to(type: Int8.self)
4048
}
4149

50+
/// Converts the data to an unsigned 8-bit integer value.
4251
public var uint8Value: UInt8? {
4352
self.to(type: UInt8.self)
4453
}
4554

55+
/// Converts the raw data to a specified numeric type.
56+
/// - Parameter type: The type to convert to.
57+
/// - Returns: The converted value, or nil if the data size is insufficient.
4658
public func to<T>(type: T.Type) -> T? where T: ExpressibleByIntegerLiteral {
4759
var genericValue: T = 0
4860
guard value.count >= MemoryLayout.size(ofValue: genericValue) else { return nil }

Sources/BLECombineKit/Error/BLEError.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ extension CBATTError: @retroactive Hashable, @retroactive Identifiable {
2121
public var id: Self { self }
2222
}
2323

24+
/// Represents errors that can occur within the BLECombineKit framework.
2425
public enum BLEError: Error, CustomStringConvertible {
2526

27+
/// Errors related to the underlying CoreBluetooth framework.
2628
public enum CoreBluetoothError: Error, Hashable, Identifiable, CustomStringConvertible {
2729
case base(code: CBError.Code, description: String), ATT(
2830
code: CBATTError.Code,
@@ -62,6 +64,7 @@ public enum BLEError: Error, CustomStringConvertible {
6264
}
6365
}
6466

67+
/// Errors related to the Bluetooth manager's state.
6568
public enum ManagerStateError: Error, Hashable, Identifiable, CustomStringConvertible {
6669
public var id: Self { self }
6770
case unknown
@@ -81,6 +84,7 @@ public enum BLEError: Error, CustomStringConvertible {
8184
}
8285
}
8386

87+
/// Errors related to peripheral operations.
8488
public enum PeripheralError: Error, Hashable, Identifiable, CustomStringConvertible {
8589
public var id: Self { self }
8690
case invalid
@@ -112,6 +116,7 @@ public enum BLEError: Error, CustomStringConvertible {
112116
}
113117
}
114118

119+
/// Errors related to data conversion or validation.
115120
public enum DataError: Error, Hashable, Identifiable, CustomStringConvertible {
116121
public var id: Self { self }
117122
case invalid
@@ -127,30 +132,34 @@ public enum BLEError: Error, CustomStringConvertible {
127132

128133
public var id: Self { self }
129134

135+
/// Error emitted when advertising is already in progress.
130136
case advertisingInProgress
131137

138+
/// Error emitted when advertising fails to start.
132139
case advertisingStartFailed(Error)
133140

141+
/// Error emitted when adding a service fails.
134142
case addingServiceFailed(CBMutableService, Error)
135143

144+
/// Error emitted when publishing an L2CAP channel fails.
136145
case publishingL2CAPChannelFailed(CBL2CAPPSM, Error)
137146

138147
/// Generic error for handling `unknown` cases.
139148
case unknown
140149

141-
/// Error emitted when publisher turns out to be `nil`.
150+
/// Error emitted when the underlying manager or peripheral is deallocated.
142151
case deallocated
143152

144-
// ManagerState
153+
/// Error related to the manager state.
145154
case managerState(ManagerStateError)
146155

147-
// Peripheral
156+
/// Error related to a peripheral.
148157
case peripheral(PeripheralError)
149158

150-
// Data
159+
/// Error related to data operations.
151160
case data(DataError)
152161

153-
// Write
162+
/// Error emitted when a write operation fails.
154163
case writeFailed(CoreBluetoothError)
155164

156165
public var description: String {

Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManagerDelegate.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import Foundation
1414
///
1515
/// See original source on [GitHub](https://github.com/Polidea/RxBluetoothKit/blob/2a95bce60fb569df57d7bec41d215fe58f56e1d4/Source/CBPeripheralManagerDelegateWrapper.swift).
1616
///
17-
final class BLEPeripheralManagerDelegate: NSObject, CBPeripheralManagerDelegate, @unchecked Sendable {
17+
final class BLEPeripheralManagerDelegate: NSObject, CBPeripheralManagerDelegate, @unchecked Sendable
18+
{
1819

1920
let didUpdateState = PassthroughSubject<CBManagerState, Never>()
2021
let isReady = PassthroughSubject<Void, Never>()

0 commit comments

Comments
 (0)