Skip to content

Commit

Permalink
Merge pull request #7 from teufelaudio/manager-proxy-delegation
Browse files Browse the repository at this point in the history
Adds ways to proxy delegation of CentralManager as well a peripheral.
  • Loading branch information
LukasLiebl authored Jun 24, 2024
2 parents 0f5aef4 + 1198ba4 commit d97b424
Show file tree
Hide file tree
Showing 197 changed files with 1,000 additions and 23,242 deletions.
4 changes: 2 additions & 2 deletions .sourcery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ templates:
output:
Sources/CombineBluetoothMocks
args:
imports: ["Combine", "CombineBluetooth", "CoreBluetooth"]
testable-imports: []
autoMockableImports: ["Combine", "CombineBluetooth", "CoreBluetooth"]
autoMockableTestableImports: []
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
code:
@mint bootstrap
@mint run sourcery --config .sourcery.yml
1 change: 1 addition & 0 deletions Mintfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
krzysztofzablocki/Sourcery@2.2.4
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,6 @@ parameters, these parameters are stored in an array that can be asserted later.
for tests this is probably the behaviour you want, so you control the race conditions in your tests and avoid them
in runtime.

For more information, please check the auto-generated mocks file source code.
For more information, please check the auto-generated mocks file source code.

To generate mocks you'd need to install [Mint](https://github.com/yonaskolb/Mint) and execute `make code`, which bootstraps Mint as well as executes Sourcery to generate mocks.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import CoreBluetooth

extension CBCentralManager {
public var combine: CentralManager {
public var combine: any CentralManager {
// Disable the skip of central manager delegate observation as we know the central manager is an instance of CBCentralManager.
CoreBluetoothCentralManager(centralManager: self)
}
}
48 changes: 27 additions & 21 deletions Sources/CombineBluetooth/Internal/CoreBluetoothCentralManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,51 @@ import Combine
import CoreBluetooth

class CoreBluetoothCentralManager: NSObject {
// MARK: - Properties
private var kvoDelegate: AnyCancellable?
private let centralManager: CBCentralManager
// This is required to avoid returning different instances of CoreBluetoothPeripheral every time we discover a device previously discovered.
// There should not be different instances because CoreBluetoothPeripheral holds the delegate for the CBPeripheral, which is expected to be
// unique.

private var cachedPeripherals: [UUID: CoreBluetoothPeripheral] = [:]
private let cachedPeripheralsAccess = NSRecursiveLock()

init(centralManager: CBCentralManager) {
self.centralManager = centralManager
super.init()
restoreDelegation()
}


private var _state: CurrentValueSubject<CBManagerState, BluetoothError> = .init(.unknown)
private var _stateRestoration: PassthroughSubject<StateRestorationEvent, BluetoothError> = .init()
private var _scanPublisher: PassthroughSubject<AdvertisingPeripheral, BluetoothError> = .init()

private var _didConnectPeripheral: PassthroughSubject<CBPeripheral, Never> = .init()
private var _didFailToConnectPeripheral: PassthroughSubject<(peripheral: CBPeripheral, error: Error?), Never> = .init()
private var _didDisconnectPeripheral: PassthroughSubject<(peripheral: CBPeripheral, error: Error?), Never> = .init()

// MARK: Init
required public init(centralManager: CBCentralManager) {
self.centralManager = centralManager
super.init()
restoreDelegation()
}

func restoreDelegation() {
// MARK: Private funcs
private func restoreDelegation() {
_state = .init(centralManager.state)
_stateRestoration = .init()
_scanPublisher = .init()
centralManager.delegate = self
kvoDelegate = centralManager.publisher(for: \.delegate).sink { [weak self] value in
guard let self = self else { return }
guard value !== self else { return }

self._state.send(completion: .failure(.noLongerDelegate))
self._stateRestoration.send(completion: .failure(.noLongerDelegate))
self._scanPublisher.send(completion: .failure(.noLongerDelegate))
self.kvoDelegate = nil
}

kvoDelegate = centralManager
.publisher(for: \.delegate)
.sink { [weak self] value in
guard let self = self else { return }
guard value !== self else { return }

self._state.send(completion: .failure(.noLongerDelegate))
self._stateRestoration.send(completion: .failure(.noLongerDelegate))
self._scanPublisher.send(completion: .failure(.noLongerDelegate))
self.kvoDelegate = nil
}
}
}

// MARK: - CentralManager implementation

extension CoreBluetoothCentralManager: CentralManager {
private func peripheral(for cbPeripheral: CBPeripheral) -> CoreBluetoothPeripheral {
cachedPeripheralsAccess.lock()
Expand Down Expand Up @@ -160,7 +166,7 @@ extension CoreBluetoothCentralManager: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
_state.send(central.state)
}

func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
_stateRestoration.send(.willRestoreState(dict))
}
Expand Down
10 changes: 5 additions & 5 deletions Sources/CombineBluetooth/Internal/CoreBluetoothPeripheral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ class CoreBluetoothPeripheral: NSObject, Identifiable {
CoreBluetoothPeripheral(peripheral: peripheral)
}

weak var proxyDelegate: CBPeripheralDelegate?

private init(peripheral: CBPeripheral) {
self.peripheral = peripheral
super.init()
self.peripheral.delegate = self
}

var proxyDelegate: CBPeripheralDelegate?
}

extension CoreBluetoothPeripheral: CBPeripheralDelegate {
func peripheralDidUpdateName(_ peripheral: CBPeripheral) { }

func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) { }
func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
proxyDelegate?.peripheralDidUpdateName?(peripheral)
}

func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
didReadRSSI.send(error.map(Result.failure) ?? .success(RSSI))
Expand Down
5 changes: 5 additions & 0 deletions Sources/CombineBluetooth/Models/BluetoothPeripheral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ public protocol BluetoothPeripheral: BluetoothPeer {
var services: [BluetoothService]? { get }
var canSendWriteWithoutResponse: Bool { get }
var isReadyAgainForWriteWithoutResponse: AnyPublisher<Void, Never> { get }

/// `CBPeripheralDelegate` which gets referenced weakly and peripheral's delegate calls
/// get proxied to. Be aware that the `CBPeripheral` within the delegate methods get passed by reference
/// so in case you access their delegate you might overwrite it, thus cancelling this proxyDelegate.
var proxyDelegate: CBPeripheralDelegate? { get set }

func readRSSI() -> AnyPublisher<NSNumber, BluetoothError>
func discoverServices(_ serviceUUIDs: [CBUUID]?) -> AnyPublisher<BluetoothService, BluetoothError>
func discoverIncludedServices(_ includedServiceUUIDs: [CBUUID]?, for service: BluetoothService) -> AnyPublisher<BluetoothService, BluetoothError>
Expand Down
1 change: 1 addition & 0 deletions Sources/CombineBluetooth/Models/CentralManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public protocol CentralManager: BluetoothManager {
func connect(_ peripheral: BluetoothPeripheral) -> AnyPublisher<BluetoothPeripheral, BluetoothError>
func connect(_ peripheral: BluetoothPeripheral, options: [String : Any]) -> AnyPublisher<BluetoothPeripheral, BluetoothError>
func peripheral(for uuid: UUID) -> BluetoothPeripheral?

// TODO: Nice to have, but complicate to handle single delegate (subscribing again with different options should end prior observations, which can
// be a bit unexpected).
// - (void)registerForConnectionEventsWithOptions:(nullable NSDictionary<CBConnectionEventMatchingOption, id> *)options NS_AVAILABLE_IOS(13_0);
Expand Down
Loading

0 comments on commit d97b424

Please sign in to comment.