Skip to content

Commit

Permalink
Merge branch 'release/0.1.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
NickKibish committed Oct 1, 2023
2 parents 5a6285e + 0e40419 commit 6f01c77
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 97 deletions.
18 changes: 18 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@
"revision" : "194863075520d244f710fddd66f4f111c21661ef",
"version" : "0.17.0"
}
},
{
"identity" : "swift-docc-plugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-plugin",
"state" : {
"revision" : "26ac5758409154cc448d7ab82389c520fa8a8247",
"version" : "1.3.0"
}
},
{
"identity" : "swift-docc-symbolkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-symbolkit",
"state" : {
"revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
"version" : "1.0.0"
}
}
],
"version" : 2
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/NordicSemiconductor/IOS-CoreBluetooth-Mock.git", from: "0.17.0"),
.package(url: "https://github.com/NickKibish/CoreBluetoothMock-Collection.git", from: "1.0.0")
.package(url: "https://github.com/NickKibish/CoreBluetoothMock-Collection.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
.target(name: "iOS-BLE-Library"),
Expand Down
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,66 @@

# iOS-BLE-Library

![Bluetooth Logo](https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Bluetooth_FM_Color.png/600px-Bluetooth_FM_Color.png)
This library is a wrapper around the CoreBluetooth framework which provides a modern async API based on Combine Framework.

An in-development Bluetooth Low Energy Library by Nordic Semiconductor to interact with the ![CoreBluetooth API](https://developer.apple.com/documentation/corebluetooth), which is not complicated, but requires writing a similar amount of boilerplate around it to interact with it from the app's logic or UI. So, instead of copying / pasting the same code and adapting it for each particular app's use, we're striving to build a modern API that we can use across all of our apps.
# Library Versions

This package contains two versions of the library:
* `iOS-BLE-Library` - the library that uses the native CoreBluetooth API.
* `iOS-BLE-Library-Mock` - the library that uses the [CoreBluetoothMock](https://github.com/NordicSemiconductor/IOS-CoreBluetooth-Mock) API.

# Installation
## Swift Package Manager
The library can be installed using Swift Package Manager.

You can choose between two versions of the library:
![`iOS-BLE-Library`](res/Screenshot-1.png)

Or you can add it as a dependency to your library:
```swift

let package = Package(
/// . . .
dependencies: [
// Set the link to the library and choose the version
.package(url: "https://github.com/NordicSemiconductor/IOS-BLE-Library.git", from: "0.1.3"),
],
targets: [
.target(
name: "MyLib",
dependencies: [
// You can use "native" CoreBluetooth API
.product(name: "iOS-BLE-Library", package: "iOS-BLE-Library")
]
),
.testTarget(
name: "MyLibTests",
dependencies: [
"MyLib",
// Or you can use the CoreBluetoothMock API
.product(name: "iOS-BLE-Library-Mock", package: "iOS-BLE-Library")
]
),
]
)
```

## CocoaPods
The library can be installed using CocoaPods.

Add the following line to your Podfile:
```ruby
pod 'iOS-BLE-Library', '~> 0.1.3'
```

or
```ruby
pod 'iOS-BLE-Library-Mock', '~> 0.1.3'
```

# Use At Your Own Risk (Currently)


As of this writing, we are not recommending this library for external use. It is used by both our Private and Public / Open-Source apps, hence the need to make it public as well. But for now we haven't settled on the API - we're still learning from all of Apple's new technologies such as Actors and Async / Await, therefore, it is likely big changes might be required as we move forward.

# TBD
Expand All @@ -19,4 +73,4 @@ As of this writing, we are not recommending this library for external use. It is
* Create two versions of the library:
* CoreBluetooth API compatible
* [CoreBluetoothMock](https://github.com/NordicSemiconductor/IOS-CoreBluetooth-Mock) API compatible
* Currently the library is using the CoreBluetoothMock API. It can be not compatible with other libraries that relies on the CoreBluetooth API.
* Currently the library is using the CoreBluetoothMock API. It can be not compatible with other libraries that relies on the CoreBluetooth API.
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


Loading

0 comments on commit 6f01c77

Please sign in to comment.