Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions Source/Central/BLECentralManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ final class StandardBLECentralManager: BLECentralManager {

private var cancellables = [AnyCancellable]()

private var connectedPeripherals: [UUID: BLETrackedPeripheral] = [:]

var isScanning: Bool {
centralManager.isScanning
}
Expand Down Expand Up @@ -66,10 +68,16 @@ final class StandardBLECentralManager: BLECentralManager {
self.init(centralManager: centralManagerWrapper, managerDelegate: BLECentralManagerDelegate())
}

func observeUpdateState() {
private func observeUpdateState() {
delegate
.didUpdateState
.sink { self.stateSubject.send($0) }
.sink { state in
self.stateSubject.send(state)
for p in self.connectedPeripherals.values {
p.connectionState.send(false)
}
self.connectedPeripherals.removeAll()
}
.store(in: &cancellables)
}

Expand All @@ -78,7 +86,9 @@ final class StandardBLECentralManager: BLECentralManager {
.didConnectPeripheral
.sink { [weak self] result in
guard let self = self else { return }
self.peripheralProvider.provide(for: result, centralManager: self).connectionState.send(true)
let p = self.peripheralProvider.provide(for: result, centralManager: self)
p.connectionState.send(true)
self.connectedPeripherals[p.peripheral.identifier] = p
}.store(in: &cancellables)
}

Expand All @@ -87,18 +97,37 @@ final class StandardBLECentralManager: BLECentralManager {
.didDisconnectPeripheral
.sink { [weak self] result in
guard let self = self else { return }
self.peripheralProvider.provide(for: result, centralManager: self).connectionState.send(false)
let p = self.peripheralProvider.provide(for: result, centralManager: self)
p.connectionState.send(false)
self.connectedPeripherals[p.peripheral.identifier] = nil
}.store(in: &cancellables)
}

private func waitUntilPoweredOn() -> AnyPublisher<CBCentralManagerWrapper, BLEError> {
if self.stateSubject.value == .poweredOn {
return Just(centralManager).setFailureType(to: BLEError.self).eraseToAnyPublisher()
} else {
return self.stateSubject
.filter({ $0 == .poweredOn })
.first()
.map { _ in self.centralManager }
.setFailureType(to: BLEError.self)
.eraseToAnyPublisher()
}
}

public func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> AnyPublisher<BLEPeripheral, BLEError> {
let retrievedPeripherals = centralManager.retrievePeripherals(withIdentifiers: identifiers)
return observePeripherals(from: retrievedPeripherals)
return waitUntilPoweredOn().flatMap { wrapper -> AnyPublisher<BLEPeripheral, BLEError> in
let retrievedPeripherals = wrapper.retrievePeripherals(withIdentifiers: identifiers)
return self.observePeripherals(from: retrievedPeripherals)
}.eraseToAnyPublisher()
}

public func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBUUID]) -> AnyPublisher<BLEPeripheral, BLEError> {
let retrievedPeripherals = centralManager.retrieveConnectedPeripherals(withServices: serviceUUIDs)
return observePeripherals(from: retrievedPeripherals)
return waitUntilPoweredOn().flatMap { wrapper -> AnyPublisher<BLEPeripheral, BLEError> in
let retrievedPeripherals = wrapper.retrieveConnectedPeripherals(withServices: serviceUUIDs)
return self.observePeripherals(from: retrievedPeripherals)
}.eraseToAnyPublisher()
}

public func scanForPeripherals(withServices services: [CBUUID]?,
Expand Down
5 changes: 5 additions & 0 deletions Source/Central/CBCentralManagerWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import CoreBluetooth
public protocol CBCentralManagerWrapper {
var manager: CBCentralManager? { get }
var isScanning: Bool { get }
var state: CBManagerState { get }

func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBPeripheralWrapper]
func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBUUID]) -> [CBPeripheralWrapper]
Expand All @@ -32,6 +33,10 @@ final class StandardCBCentralManagerWrapper: CBCentralManagerWrapper {
wrappedManager.isScanning
}

var state: CBManagerState {
wrappedManager.state
}

let wrappedManager: CBCentralManager

init(with manager: CBCentralManager) {
Expand Down
71 changes: 66 additions & 5 deletions Tests/BLECentralManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,14 @@ class BLECentralManagerTests: XCTestCase {
XCTAssertEqual(centralManagerWrapper.registerForConnectionEventsWasCalledCount, 1)
}

// test retrieving peripherals when state is already powered on
func testRetrievePeripheralsReturns() throws {
// Given
var retrievedPeripheral: BLEPeripheral?
let peripheralExpectation = expectation(description: "PeripheralExpectation")
delegate.didUpdateState.send(.poweredOn)
let expectedPeripheral = MockCBPeripheralWrapper()
centralManagerWrapper.mockRetrievedPeripherals = [expectedPeripheral]

// When
sut.retrievePeripherals(withIdentifiers: [])
Expand All @@ -152,15 +156,72 @@ class BLECentralManagerTests: XCTestCase {

// Then
wait(for: [peripheralExpectation], timeout: 0.005)
XCTAssertNil(retrievedPeripheral) // BLEPeripheralBuilder is returning nil, so no peripherals returned
XCTAssertEqual(retrievedPeripheral?.peripheral.identifier, expectedPeripheral.identifier)
XCTAssertEqual(centralManagerWrapper.retrievePeripheralsWasCalledCount, 1)
XCTAssertEqual(peripheralProvider.buildBLEPeripheralWasCalledCount, 0)
XCTAssertEqual(peripheralProvider.provideBLEPeripheralWasCalledCount, 1)
}

// test retrieving peripherals when state becomes powered on
func testRetrievePeripheralsReturnsOnStateUpdate() throws {
// Given
var retrievedPeripheral: BLEPeripheral?
let peripheralExpectation = expectation(description: "PeripheralExpectation")
let expectedPeripheral = MockCBPeripheralWrapper()
centralManagerWrapper.mockRetrievedPeripherals = [expectedPeripheral]

// When
sut.retrievePeripherals(withIdentifiers: [])
.sink(receiveCompletion: { completion in
guard case .finished = completion else { return }
peripheralExpectation.fulfill()
}, receiveValue: { peripheral in
retrievedPeripheral = peripheral
}).store(in: &cancellables)
delegate.didUpdateState.send(.poweredOn)

// Then
wait(for: [peripheralExpectation], timeout: 0.005)
XCTAssertEqual(retrievedPeripheral?.peripheral.identifier, expectedPeripheral.identifier)
XCTAssertEqual(centralManagerWrapper.retrievePeripheralsWasCalledCount, 1)
XCTAssertEqual(peripheralProvider.provideBLEPeripheralWasCalledCount, 1)
}

func testConnectToRetrievedPeripheral() throws {
// Given
let peripheralExpectation = expectation(description: "PeripheralExpectation")
delegate.didUpdateState.send(.poweredOn)
let expectedPeripheral = MockCBPeripheralWrapper()
centralManagerWrapper.mockRetrievedPeripherals = [expectedPeripheral]

// When
sut.retrievePeripherals(withIdentifiers: [])
.flatMap({ peripheral in
peripheral.connect(with: nil)
})
.catch({ _ in Empty(completeImmediately: false) })
.flatMap({ peripheral in
peripheral.observeConnectionState()
})
.sink(receiveCompletion: { completion in
}, receiveValue: { state in
if state {
peripheralExpectation.fulfill()
}
}).store(in: &cancellables)

// Then
wait(for: [peripheralExpectation], timeout: 0.005)
XCTAssertEqual(centralManagerWrapper.retrievePeripheralsWasCalledCount, 1)
XCTAssertEqual(peripheralProvider.provideBLEPeripheralWasCalledCount, 1)
}

func testRetrieveConnectedPeripheralsReturns() throws {
// Given
var retrievedPeripheral: BLEPeripheral?
let peripheralExpectation = expectation(description: "PeripheralExpectation")
delegate.didUpdateState.send(.poweredOn)
let expectedPeripheral = MockCBPeripheralWrapper()
centralManagerWrapper.mockRetrievedConnectedPeripherals = [expectedPeripheral]

// When
sut.retrieveConnectedPeripherals(withServices: [])
Expand All @@ -173,9 +234,9 @@ class BLECentralManagerTests: XCTestCase {

// Then
wait(for: [peripheralExpectation], timeout: 0.005)
XCTAssertNil(retrievedPeripheral) // BLEPeripheralBuilder is returning nil, so no peripherals returned
XCTAssertEqual(retrievedPeripheral?.peripheral.identifier, expectedPeripheral.identifier)
XCTAssertEqual(centralManagerWrapper.retrieveConnectedPeripheralsWasCalledCount, 1)
XCTAssertEqual(peripheralProvider.buildBLEPeripheralWasCalledCount, 0)
XCTAssertEqual(peripheralProvider.provideBLEPeripheralWasCalledCount, 1)
}

func testWillRestoreStateReturnsWhenDelegateUpdates() {
Expand Down Expand Up @@ -219,7 +280,7 @@ class BLECentralManagerTests: XCTestCase {

// Then
wait(for: [observeDidUpdateANCSAuthorizationExpectation], timeout: 0.005)
XCTAssertEqual(peripheralProvider.buildBLEPeripheralWasCalledCount, 1)
XCTAssertEqual(peripheralProvider.provideBLEPeripheralWasCalledCount, 1)
XCTAssertNotNil(observedPeripheral)
}

Expand Down
11 changes: 8 additions & 3 deletions Tests/Mocks/BLEPeripheralMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,35 @@ import CoreBluetooth
import Combine
@testable import BLECombineKit

final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral {
final class MockBLEPeripheral: BLETrackedPeripheral {
let connectionState = CurrentValueSubject<Bool, Never>(false)
var peripheral: CBPeripheralWrapper

init() {
self.peripheral = MockCBPeripheralWrapper()
}

init(peripheral: MockCBPeripheralWrapper) {
self.peripheral = peripheral
}

public func observeConnectionState() -> AnyPublisher<Bool, Never> {
return Just.init(true).eraseToAnyPublisher()
}

var connectWasCalled = false
func connect(with options: [String : Any]?) -> AnyPublisher<BLEPeripheral, BLEError> {
connectWasCalled = true
let blePeripheral = StandardBLEPeripheral(peripheral: peripheral, centralManager: nil)
return Just.init(blePeripheral)
self.connectionState.send(true)
return Just.init(self)
.setFailureType(to: BLEError.self)
.eraseToAnyPublisher()
}

var disconnectWasCalled = false
func disconnect() -> AnyPublisher<Never, BLEError> {
disconnectWasCalled = true
self.connectionState.send(false)
return Empty(completeImmediately: true).eraseToAnyPublisher()
}

Expand Down
10 changes: 5 additions & 5 deletions Tests/Mocks/MockBLEPeripheralProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import Foundation
@testable import BLECombineKit

final class MockBLEPeripheralProvider: BLEPeripheralProvider {
var buildBLEPeripheralWasCalledCount = 0
var provideBLEPeripheralWasCalledCount = 0
var blePeripheral: BLETrackedPeripheral?

func provide(
for peripheral: CBPeripheralWrapper,
centralManager: BLECentralManager
) -> BLETrackedPeripheral {
buildBLEPeripheralWasCalledCount += 1
return blePeripheral ?? MockBLEPeripheral()
provideBLEPeripheralWasCalledCount += 1
return blePeripheral ?? MockBLEPeripheral(peripheral: peripheral as! MockCBPeripheralWrapper)
}
}

Expand All @@ -31,8 +31,8 @@ final class MockArrayBLEPeripheralBuilder: BLEPeripheralProvider {
for peripheral: CBPeripheralWrapper,
centralManager: BLECentralManager
) -> BLETrackedPeripheral {
let peripheral = blePeripherals.element(at: buildBLEPeripheralWasCalledCount)
let p = blePeripherals.element(at: buildBLEPeripheralWasCalledCount)
buildBLEPeripheralWasCalledCount += 1
return peripheral ?? MockBLEPeripheral()
return p ?? MockBLEPeripheral(peripheral: peripheral as! MockCBPeripheralWrapper)
}
}
10 changes: 6 additions & 4 deletions Tests/Mocks/MockCBCentralManagerWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@ final class MockCBCentralManagerWrapper: CBCentralManagerWrapper {

var isScanning: Bool = false

var mockRetrieviePeripherals: [CBPeripheralWrapper] = .init()
var state: CBManagerState = .unknown

var mockRetrievedPeripherals: [CBPeripheralWrapper] = .init()
var retrievePeripheralsWasCalledCount = 0
func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBPeripheralWrapper] {
retrievePeripheralsWasCalledCount += 1
return mockRetrieviePeripherals
return mockRetrievedPeripherals
}

var mockRetrieveConnectedPeripherals: [CBPeripheralWrapper] = .init()
var mockRetrievedConnectedPeripherals: [CBPeripheralWrapper] = .init()
var retrieveConnectedPeripheralsWasCalledCount = 0
func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBUUID]) -> [CBPeripheralWrapper] {
retrieveConnectedPeripheralsWasCalledCount += 1
return mockRetrieveConnectedPeripherals
return mockRetrievedConnectedPeripherals
}

var scanForPeripheralsWasCalledCount = 0
Expand Down