Skip to content

Commit

Permalink
Refactor publishers to Deferred where side effects are present (#49)
Browse files Browse the repository at this point in the history
* Updated the methods with side effects to rely on a Deferred Publisher

* Made the CBPeripheral to conform to CBPeripheralWrapper
  • Loading branch information
Henryforce authored Jul 24, 2024
1 parent e394c3d commit bff416f
Show file tree
Hide file tree
Showing 23 changed files with 478 additions and 484 deletions.
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ import BLECombineKit
let centralManager = BLECombineKit.buildCentralManager(with: CBCentralManager())

let serviceUUID = CBUUID(string: "0x00FF")
// Connect to the first peripheral that matches the given service UUID and observe all the
// characteristics in that service.
let characteristicUUID = CBUUID(string: "0xFF01")
// Connect to the first peripheral that matches the given service UUID and observe a specific
// characteristic in that service.
centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil)
.first()
.flatMap { $0.peripheral.connect(with: nil) }
.flatMap { $0.discoverServices(serviceUUIDs: [serviceUUID]) }
.flatMap { $0.discoverCharacteristics(characteristicUUIDs: nil) }
.flatMap { $0.observeValue() }
.filter { $0.value.uuid == characteristicUUID }
.flatMap { $0.observeValueUpdateAndSetNotification() }
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { data in
Expand All @@ -55,14 +57,16 @@ import BLECombineKit
let centralManager = BLECombineKit.buildCentralManager(with: CBCentralManager())

let serviceUUID = CBUUID(string: "0x00FF")
// Connect to the first peripheral that matches the given service UUID and observe all the
// characteristics in that service.
let characteristicUUID = CBUUID(string: "0xFF01")
// Connect to the first peripheral that matches the given service UUID and observe a specific
// characteristic in that service.
let stream = centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil)
.first()
.flatMap { $0.peripheral.connect(with: nil) }
.flatMap { $0.discoverServices(serviceUUIDs: [serviceUUID]) }
.flatMap { $0.discoverCharacteristics(characteristicUUIDs: nil) }
.flatMap { $0.observeValue() }
.filter { $0.value.uuid == characteristicUUID }
.flatMap { $0.observeValueUpdateAndSetNotification() }
.values

Task {
Expand Down
2 changes: 1 addition & 1 deletion Sources/BLECombineKit/Central/BLECentralManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public protocol BLECentralManager: AnyObject {
func cancelPeripheralConnection(_ peripheral: BLEPeripheral) -> AnyPublisher<Never, Never>

/// Register for any connection events.
#if !os(macOS)
#if os(iOS) || os(tvOS) || os(watchOS)
func registerForConnectionEvents(options: [CBConnectionEventMatchingOption: Any]?)
#endif

Expand Down
19 changes: 8 additions & 11 deletions Sources/BLECombineKit/Central/BLECentralManagerDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,31 @@ final class BLECentralManagerDelegate: NSObject, CBCentralManagerDelegate {
let didConnectPeripheral = PassthroughSubject<CBPeripheralWrapper, Never>()
let didDisconnectPeripheral = PassthroughSubject<CBPeripheralWrapper, Never>()
let didFailToConnect = PassthroughSubject<CBPeripheralWrapper, Never>()
let didDiscoverAdvertisementData = PassthroughSubject<DidDiscoverAdvertisementDataResult, Never>()
let didDiscoverAdvertisementData = PassthroughSubject<
DidDiscoverAdvertisementDataResult, BLEError
>()
let didUpdateState = PassthroughSubject<ManagerState, Never>()
let willRestoreState = PassthroughSubject<[String: Any], Never>()
let didUpdateANCSAuthorization = PassthroughSubject<CBPeripheralWrapper, Never>()

public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral)
didConnectPeripheral.send(peripheralWrapper)
didConnectPeripheral.send(peripheral)
}

public func centralManager(
_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?
) {
let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral)
didDisconnectPeripheral.send(peripheralWrapper)
didDisconnectPeripheral.send(peripheral)
}

public func centralManager(
_ central: CBCentralManager,
didFailToConnect peripheral: CBPeripheral,
error: Error?
) {
let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral)
didFailToConnect.send(peripheralWrapper)
didFailToConnect.send(peripheral)
}

public func centralManager(
Expand All @@ -50,8 +49,7 @@ final class BLECentralManagerDelegate: NSObject, CBCentralManagerDelegate {
advertisementData: [String: Any],
rssi RSSI: NSNumber
) {
let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral)
let result = (peripheral: peripheralWrapper, advertisementData: advertisementData, rssi: RSSI)
let result = (peripheral: peripheral, advertisementData: advertisementData, rssi: RSSI)
didDiscoverAdvertisementData.send(result)
}

Expand All @@ -72,8 +70,7 @@ final class BLECentralManagerDelegate: NSObject, CBCentralManagerDelegate {
_ central: CBCentralManager,
didUpdateANCSAuthorizationFor peripheral: CBPeripheral
) {
let peripheralWrapper = StandardCBPeripheralWrapper(peripheral: peripheral)
didUpdateANCSAuthorization.send(peripheralWrapper)
didUpdateANCSAuthorization.send(peripheral)
}
#endif

Expand Down
8 changes: 4 additions & 4 deletions Sources/BLECombineKit/Central/CBCentralManagerWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,13 @@ final class StandardCBCentralManagerWrapper: CBCentralManagerWrapper {
func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBPeripheralWrapper] {
wrappedManager
.retrievePeripherals(withIdentifiers: identifiers)
.map { StandardCBPeripheralWrapper(peripheral: $0) }
}

func retrieveConnectedPeripherals(
withServices serviceUUIDs: [CBUUID]
) -> [CBPeripheralWrapper] {
wrappedManager
.retrieveConnectedPeripherals(withServices: serviceUUIDs)
.map { StandardCBPeripheralWrapper(peripheral: $0) }
}

func scanForPeripherals(withServices serviceUUIDs: [CBUUID]?, options: [String: Any]?) {
Expand All @@ -66,11 +64,13 @@ final class StandardCBCentralManagerWrapper: CBCentralManagerWrapper {
}

func connect(_ wrappedPeripheral: CBPeripheralWrapper, options: [String: Any]?) {
wrappedManager.connect(wrappedPeripheral.peripheral, options: options)
guard let manager else { return }
wrappedPeripheral.connect(manager: manager)
}

func cancelPeripheralConnection(_ wrappedPeripheral: CBPeripheralWrapper) {
wrappedManager.cancelPeripheralConnection(wrappedPeripheral.peripheral)
guard let manager else { return }
wrappedPeripheral.cancelConnection(manager: manager)
}

#if !os(macOS)
Expand Down
140 changes: 74 additions & 66 deletions Sources/BLECombineKit/Central/StandardBLECentralManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class StandardBLECentralManager: BLECentralManager {
var stateSubject = CurrentValueSubject<ManagerState, Never>(ManagerState.unknown)
let delegate: BLECentralManagerDelegate

private var cancellables = [AnyCancellable]()
private var cancellables = Set<AnyCancellable>()

var isScanning: Bool {
associatedCentralManager.isScanning
Expand Down Expand Up @@ -49,48 +49,7 @@ final class StandardBLECentralManager: BLECentralManager {
self.init(centralManager: centralManagerWrapper, managerDelegate: BLECentralManagerDelegate())
}

func observeUpdateState() {
delegate
.didUpdateState
.sink { self.stateSubject.send($0) }
.store(in: &cancellables)
}

func observeDidConnectPeripheral() {
delegate
.didConnectPeripheral
.sink { [weak self] result in
guard let self = self else { return }
self.peripheralProvider
.provide(for: result, centralManager: self)
.connectionState.send(true)
}.store(in: &cancellables)
}

func observeDidFailToConnectPeripheral() {
delegate
.didFailToConnect
.ignoreFailure()
.sink { [weak self] result in
guard let self = self else { return }
self.peripheralProvider.provide(for: result, centralManager: self).connectionState.send(
false
)
}.store(in: &cancellables)
}

func observeDidDisconnectPeripheral() {
delegate
.didDisconnectPeripheral
.sink { [weak self] result in
guard let self = self else { return }
self.peripheralProvider.provide(for: result, centralManager: self).connectionState.send(
false
)
}.store(in: &cancellables)
}

public func retrievePeripherals(
func retrievePeripherals(
withIdentifiers identifiers: [UUID]
) -> AnyPublisher<BLEPeripheral, BLEError> {
let retrievedPeripherals = associatedCentralManager.retrievePeripherals(
Expand All @@ -99,7 +58,7 @@ final class StandardBLECentralManager: BLECentralManager {
return observePeripherals(from: retrievedPeripherals)
}

public func retrieveConnectedPeripherals(
func retrieveConnectedPeripherals(
withServices serviceUUIDs: [CBUUID]
) -> AnyPublisher<BLEPeripheral, BLEError> {
let retrievedPeripherals = associatedCentralManager.retrieveConnectedPeripherals(
Expand All @@ -108,36 +67,42 @@ final class StandardBLECentralManager: BLECentralManager {
return observePeripherals(from: retrievedPeripherals)
}

public func scanForPeripherals(
func scanForPeripherals(
withServices services: [CBUUID]?,
options: [String: Any]?
) -> AnyPublisher<BLEScanResult, BLEError> {
associatedCentralManager.scanForPeripherals(withServices: services, options: options)

return self.delegate
let stream = delegate
.didDiscoverAdvertisementData
.tryMap { [weak self] peripheral, advertisementData, rssi in
guard let self else { throw BLEError.deallocated }
.eraseToAnyPublisher() // Erase needed to silence flatMap(maxPublishers) availability.
.flatMap { [weak self] result -> AnyPublisher<BLEScanResult, BLEError> in
guard let self else { return Fail(error: BLEError.deallocated).eraseToAnyPublisher() }
let peripheral = self.peripheralProvider.provide(
for: peripheral,
for: result.peripheral,
centralManager: self
)

return BLEScanResult(
peripheral: peripheral,
advertisementData: advertisementData,
rssi: rssi
)
return Just(
BLEScanResult(
peripheral: peripheral,
advertisementData: result.advertisementData,
rssi: result.rssi
)
).setFailureType(to: BLEError.self).eraseToAnyPublisher()
}
.mapError { $0 as? BLEError ?? BLEError.unknown }
.eraseToAnyPublisher()

return Deferred<AnyPublisher<BLEScanResult, BLEError>> { [associatedCentralManager] in
defer {
associatedCentralManager.scanForPeripherals(withServices: services, options: options)
}
return stream
}.eraseToAnyPublisher()
}

public func stopScan() {
func stopScan() {
associatedCentralManager.stopScan()
}

public func connect(
func connect(
peripheral: BLEPeripheral,
options: [String: Any]?
) -> AnyPublisher<BLEPeripheral, BLEError> {
Expand All @@ -160,31 +125,32 @@ final class StandardBLECentralManager: BLECentralManager {
.eraseToAnyPublisher()
}

public func cancelPeripheralConnection(
func cancelPeripheralConnection(
_ peripheral: BLEPeripheral
) -> AnyPublisher<Never, Never> {
let associatedPeripheral = peripheral.associatedPeripheral
associatedCentralManager.cancelPeripheralConnection(associatedPeripheral)

return delegate.didDisconnectPeripheral
return delegate
.didDisconnectPeripheral
.filter { $0.identifier == associatedPeripheral.identifier }
.first()
.ignoreOutput()
.ignoreFailure()
.eraseToAnyPublisher()
}

#if !os(macOS)
public func registerForConnectionEvents(options: [CBConnectionEventMatchingOption: Any]?) {
#if os(iOS) || os(tvOS) || os(watchOS)
func registerForConnectionEvents(options: [CBConnectionEventMatchingOption: Any]?) {
associatedCentralManager.registerForConnectionEvents(options: options)
}
#endif

public func observeWillRestoreState() -> AnyPublisher<[String: Any], Never> {
func observeWillRestoreState() -> AnyPublisher<[String: Any], Never> {
delegate.willRestoreState.eraseToAnyPublisher()
}

public func observeDidUpdateANCSAuthorization() -> AnyPublisher<BLEPeripheral, Never> {
func observeDidUpdateANCSAuthorization() -> AnyPublisher<BLEPeripheral, Never> {
delegate.didUpdateANCSAuthorization
.compactMap { [weak self] peripheral in
guard let self = self else { return nil }
Expand All @@ -201,6 +167,48 @@ final class StandardBLECentralManager: BLECentralManager {
observeDidDisconnectPeripheral()
}

private func observeUpdateState() {
let stateSubject = self.stateSubject
return delegate
.didUpdateState
.sink { stateSubject.send($0) }
.store(in: &cancellables)
}

private func observeDidConnectPeripheral() {
delegate
.didConnectPeripheral
.sink { [weak self] result in
guard let self = self else { return }
self.peripheralProvider
.provide(for: result, centralManager: self)
.connectionState.send(true)
}.store(in: &cancellables)
}

private func observeDidFailToConnectPeripheral() {
delegate
.didFailToConnect
.ignoreFailure()
.sink { [weak self] result in
guard let self = self else { return }
self.peripheralProvider.provide(for: result, centralManager: self).connectionState.send(
false
)
}.store(in: &cancellables)
}

private func observeDidDisconnectPeripheral() {
delegate
.didDisconnectPeripheral
.sink { [weak self] result in
guard let self = self else { return }
self.peripheralProvider.provide(for: result, centralManager: self).connectionState.send(
false
)
}.store(in: &cancellables)
}

private func observePeripherals(
from retrievedPeripherals: [CBPeripheralWrapper]
) -> AnyPublisher<BLEPeripheral, BLEError> {
Expand Down
3 changes: 1 addition & 2 deletions Sources/BLECombineKit/Characteristic/BLECharacteristic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@

import Combine
import CoreBluetooth
import Foundation

public struct BLECharacteristic: BLEPeripheralResult {
public struct BLECharacteristic {
public let value: CBCharacteristic
private let peripheral: BLEPeripheral

Expand Down
6 changes: 2 additions & 4 deletions Sources/BLECombineKit/Data/BLEData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@

import Foundation

public struct BLEData: BLEPeripheralResult {
public struct BLEData {
public let value: Data
public let peripheral: BLEPeripheral

public init(value: Data, peripheral: BLEPeripheral) {
public init(value: Data) {
self.value = value
self.peripheral = peripheral
}

public var floatValue: Float32? {
Expand Down
Loading

0 comments on commit bff416f

Please sign in to comment.