Skip to content

Commit

Permalink
Merge branch 'feature/doc_getting_started' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
NickKibish committed Oct 1, 2023
2 parents bf77025 + 8dacb12 commit 0e40419
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 93 deletions.
28 changes: 18 additions & 10 deletions Sources/iOS-BLE-Library/CentralManager/CentralManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ extension CentralManager {
public var localizedDescription: String {
switch self {
case .wrongManager:
return "Incorrect manager instance provided."
return "Incorrect manager instance provided. Delegate must be of type ReactiveCentralManagerDelegate."
case .badState(let state):
return "Bad state: \(state)"
return "Bad state: \(state)."
case .unknownError:
return "An unknown error occurred."
}
Expand Down Expand Up @@ -59,14 +59,18 @@ private class Observer: NSObject {
}
}

/// A custom Central Manager class that extends the functionality of the standard CBCentralManager.
/// This class brings a reactive approach and is based on the Swift Combine framework.
/// A Custom Central Manager class.
///
/// 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.
public class CentralManager {
private let isScanningSubject = CurrentValueSubject<Bool, Never>(false)
private let killSwitchSubject = PassthroughSubject<Void, Never>()
private lazy var observer = Observer(cm: centralManager, publisher: isScanningSubject)

/// The underlying CBCentralManager instance.
public let centralManager: CBCentralManager

/// The reactive delegate for the ``centralManager``.
public let centralManagerDelegate: ReactiveCentralManagerDelegate

/// Initializes a new instance of `CentralManager`.
Expand Down Expand Up @@ -146,7 +150,7 @@ extension CentralManager {
/// Cancels the connection with the specified peripheral.
/// - Parameter peripheral: The peripheral to disconnect from.
/// - Returns: A publisher that emits the disconnected peripheral.
public func cancelPeripheralConnection(_ peripheral: CBPeripheral) -> Publishers.Peripheral
public func cancelPeripheralConnection(_ peripheral: CBPeripheral) -> Publishers.BluetoothPublisher<CBPeripheral, Error>
{
return self.disconnectedPeripheralsChannel
.tryFilter { r in
Expand All @@ -162,14 +166,15 @@ extension CentralManager {
}
.map { $0.0 }
.first()
.peripheral {
self.centralManager.cancelPeripheralConnection(peripheral)
}
.bluetooth {
self.centralManager.cancelPeripheralConnection(peripheral)
}
}
}

// MARK: Retrieving Lists of Peripherals
extension CentralManager {
#warning("check `connect` method")
/// Returns a list of the peripherals connected to the system whose
/// services match a given set of criteria.
///
Expand Down Expand Up @@ -200,14 +205,17 @@ extension CentralManager {

// MARK: Scanning or Stopping Scans of Peripherals
extension CentralManager {
#warning("Question: Should we throw an error if the scan is already running?")
/// Initiates a scan for peripherals with the specified services.
///
/// Calling this method stops an ongoing scan if it is already running and finishes the publisher returned by ``scanForPeripherals(withServices:)``.
///
/// - Parameter services: The services to scan for.
/// - Returns: A publisher that emits scan results or errors.
/// - Returns: A publisher that emits scan results or an error.
public func scanForPeripherals(withServices services: [CBUUID]?)
-> Publishers.BluetoothPublisher<ScanResult, Error>
{
stopScan()
// TODO: Change to BluetoothPublisher
return centralManagerDelegate.stateSubject
.tryFirst { state in
guard let determined = state.ready else { return false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,6 @@ open class ReactiveCentralManagerDelegate: NSObject, CBCentralManagerDelegate {
stateSubject.send(central.state)
}

public func centralManager(
_ central: CBCentralManager, willRestoreState dict: [String: Any]
) {
unimplementedError()
}

// MARK: Monitoring the Central Manager’s Authorization
#if !os(macOS)
public func centralManager(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# ``iOS_BLE_Library/CentralManager``

### Create a Central Manager

Since it's not recommended to override the `CBCentralManager`'s methods, ``CentralManager`` is merely a wrapper around `CBCentralManager` with an instance of it inside.

The new instance of `CBCentralManager` can be created during initialization using ``init(centralManagerDelegate:queue:)``, or an existing instance can be passed using ``init(centralManager:)``.

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.

### Connection

Use ``CentralManager/connect(_:options:)`` to connect to a peripheral.
The returned publisher will emit the connected peripheral or an error if the connection fails.
The publisher will not complete until the peripheral is disconnected.
If the connection fails, or the peripheral is unexpectedly disconnected, the publisher will fail with an error.

> The publisher returned by ``CentralManager/connect(_:options:)`` is a `ConnectablePublisher`. Therefore, you need to call `connect()` or `autoconnect()` to initiate the connection process.
```swift
centralManager.connect(peripheral)
.autoconnect()
.sink { completion in
switch completion {
case .finished:
print("Peripheral disconnected successfully")
case .failure(let error):
print("Error: \(error)")
}
} receiveValue: { peripheral in
print("Peripheral connected: \(peripheral)")
}
.store(in: &cancellables)
```

### Channels

Channels are used to pass through data from the `CBCentralManagerDelegate` methods.
You can consider them as a reactive version of the `CBCentralManagerDelegate` methods.

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.

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.

```swift
centralManager.connectedPeripheralChannel
.sink { peripheral, error in
if let error = error {
print("Error: \(error)")
} else {
print("New peripheral connected: \(peripheral)"
}
}
.store(in: &cancellables)
```

## Topics

### Initializers

- ``init(centralManagerDelegate:queue:)``
- ``init(centralManager:)``

### Instance Properties

- ``centralManager``
- ``centralManagerDelegate``

### Scan

- ``scanForPeripherals(withServices:)``
- ``stopScan()``
- ``retrievePeripherals(withIdentifiers:)``

### Connection

- ``connect(_:options:)``
- ``cancelPeripheralConnection(_:)``
- ``retrieveConnectedPeripherals(withServices:)``

### Channels

- ``stateChannel``
- ``isScanningChannel``
- ``scanResultsChannel``
- ``connectedPeripheralChannel``
- ``disconnectedPeripheralsChannel``
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ``iOS_BLE_Library/ReactiveCentralManagerDelegate``

Implementation of the `CBCentralManagerDelegate`.

`ReactiveCentralManagerDelegate` is a class that implements the `CBCentralManagerDelegate` and is an essential part of the ``CentralManager`` class.

It brings a reactive programming approach, utilizing Combine publishers to seamlessly handle Bluetooth events and data.
This class allows to monitor and respond to events such as peripheral connection, disconnection, and scanning for peripherals.

It has all needed publishers that are used for handling Bluetooth events and data.

## Override

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.

However, it's important to call the `super` implementation of the method, otherwise it will break the `CentralManager` functionality.
19 changes: 19 additions & 0 deletions Sources/iOS-BLE-Library/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ``iOS_BLE_Library``

This library is a wrapper around the CoreBluetooth framework which provides a modern async API based on Combine Framework.

The library has been designed to have a simple API similar to the one provided by the CoreBluetooth framework.
So if you are familiar with the CoreBluetooth framework, you will be able to use this library without any problem.

## Topics

### Central Manager
- ``CentralManager``
- ``ReactiveCentralManagerDelegate``

### Peripheral
- ``Peripheral``
- ``ReactivePeripheralDelegate``

### Essentials
- ``iOS_BLE_Library/Combine/Publishers/BluetoothPublisher``
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
``iOS_BLE_Library/Peripheral``

### Create a Peripheral


57 changes: 57 additions & 0 deletions Sources/iOS-BLE-Library/Peripheral/Peripheral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ public class Peripheral {
case badDelegate
}

/// The underlying CBPeripheral instance.
public let peripheral: CBPeripheral

/// The delegate for handling peripheral events.
public let peripheralDelegate: ReactivePeripheralDelegate

private let stateSubject = CurrentValueSubject<CBPeripheralState, Never>(.disconnected)
Expand All @@ -101,6 +104,11 @@ public class Peripheral {
)

// TODO: Why don't we use default delegate?
/// Initializes a Peripheral instance.
///
/// - Parameters:
/// - peripheral: The CBPeripheral to manage.
/// - delegate: The delegate for handling peripheral events.
public init(peripheral: CBPeripheral, delegate: ReactivePeripheralDelegate) {
self.peripheral = peripheral
self.peripheralDelegate = delegate
Expand All @@ -125,13 +133,18 @@ public class Peripheral {

// MARK: - Channels
extension Peripheral {
/// A publisher for the current state of the peripheral.
public var peripheralStateChannel: AnyPublisher<CBPeripheralState, Never> {
stateSubject.eraseToAnyPublisher()
}
}

extension Peripheral {
// TODO: Extract repeated code
/// Discover services for the peripheral.
///
/// - Parameter serviceUUIDs: An optional array of service UUIDs to filter the discovery results. If nil, all services will be discovered.
/// - Returns: A publisher emitting discovered services or an error.
public func discoverServices(serviceUUIDs: [CBUUID]?)
-> Publishers.BluetoothPublisher<CBService, Error>
{
Expand Down Expand Up @@ -161,6 +174,12 @@ extension Peripheral {
}
}

/// Discover characteristics for a given service.
///
/// - Parameters:
/// - characteristicUUIDs: An optional array of characteristic UUIDs to filter the discovery results. If nil, all characteristics will be discovered.
/// - service: The service for which to discover characteristics.
/// - Returns: A publisher emitting discovered characteristics or an error.
public func discoverCharacteristics(
_ characteristicUUIDs: [CBUUID]?, for service: CBService
) -> Publishers.BluetoothPublisher<CBCharacteristic, Error> {
Expand Down Expand Up @@ -196,6 +215,10 @@ extension Peripheral {
}
}

/// Discover descriptors for a given characteristic.
///
/// - Parameter characteristic: The characteristic for which to discover descriptors.
/// - Returns: A publisher emitting discovered descriptors or an error.
public func discoverDescriptors(for characteristic: CBCharacteristic)
-> Publishers.BluetoothPublisher<CBDescriptor, Error>
{
Expand All @@ -221,6 +244,12 @@ extension Peripheral {

// MARK: - Writing Characteristic and Descriptor Values
extension Peripheral {
/// Write data to a characteristic and wait for a response.
///
/// - Parameters:
/// - data: The data to write.
/// - characteristic: The characteristic to write to.
/// - Returns: A publisher indicating success or an error.
public func writeValueWithResponse(_ data: Data, for characteristic: CBCharacteristic)
-> Publishers.BluetoothPublisher<Void, Error>
{
Expand All @@ -239,21 +268,39 @@ extension Peripheral {
}
}

/// Write data to a characteristic without waiting for a response.
///
/// - Parameters:
/// - data: The data to write.
/// - characteristic: The characteristic to write to.
public func writeValueWithoutResponse(_ data: Data, for characteristic: CBCharacteristic) {
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
}

/// Write data to a descriptor.
///
/// - Parameters:
/// - data: The data to write.
/// - descriptor: The descriptor to write to.
public func writeValue(_ data: Data, for descriptor: CBDescriptor) {
fatalError()
}
}

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

/// Listen for updates to the value of a characteristic.
///
/// - Parameter characteristic: The characteristic to monitor for updates.
/// - Returns: A publisher emitting characteristic values or an error.
public func listenValues(for characteristic: CBCharacteristic) -> AnyPublisher<Data, Error>
{
return peripheralDelegate.updatedCharacteristicValuesSubject
Expand All @@ -268,13 +315,23 @@ extension Peripheral {
.eraseToAnyPublisher()
}

/// Read the value of a descriptor.
///
/// - Parameter descriptor: The descriptor to read from.
/// - Returns: A future emitting the read data or an error.
public func readValue(for descriptor: CBDescriptor) -> Future<Data, Error> {
fatalError()
}
}

// MARK: - Setting Notifications for a Characteristic’s Value
extension Peripheral {
/// Set notification state for a characteristic.
///
/// - Parameters:
/// - isEnabled: Whether notifications should be enabled or disabled.
/// - characteristic: The characteristic for which to set the notification state.
/// - Returns: A publisher indicating success or an error.
public func setNotifyValue(_ isEnabled: Bool, for characteristic: CBCharacteristic)
-> Publishers.BluetoothPublisher<Bool, Error>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ extension Publisher {
}

extension Publishers {

/**
A publisher that is used for most of the Bluetooth operations.

# Overview
This publisher conforms to the `ConnectablePublisher` protocol because most of the Bluetooth operations have to be set up before they can be used.

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.
To learn more about the `ConnectablePublisher` protocol, see [Apple's documentation](https://developer.apple.com/documentation/combine/connectablepublisher).

```swift
let publisher = centralManager.scanForPeripherals(withServices: nil)
.autoconnect()
// chain of publishers
.sink {
// . . .
}
.store(in: &cancellables)
```
*/
public class BluetoothPublisher<Output, Failure: Error>: ConnectablePublisher {

private let inner: BaseConnectable<Output, Failure>
Expand Down
Loading

0 comments on commit 0e40419

Please sign in to comment.