diff --git a/Source/Central/BLECentralManager.swift b/Source/Central/BLECentralManager.swift index 5178ef7..75e439f 100644 --- a/Source/Central/BLECentralManager.swift +++ b/Source/Central/BLECentralManager.swift @@ -37,6 +37,8 @@ final class StandardBLECentralManager: BLECentralManager { private var cancellables = [AnyCancellable]() + private var connectedPeripherals: [UUID: BLETrackedPeripheral] = [:] + var isScanning: Bool { centralManager.isScanning } @@ -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) } @@ -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) } @@ -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 { + 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 { - let retrievedPeripherals = centralManager.retrievePeripherals(withIdentifiers: identifiers) - return observePeripherals(from: retrievedPeripherals) + return waitUntilPoweredOn().flatMap { wrapper -> AnyPublisher in + let retrievedPeripherals = wrapper.retrievePeripherals(withIdentifiers: identifiers) + return self.observePeripherals(from: retrievedPeripherals) + }.eraseToAnyPublisher() } public func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBUUID]) -> AnyPublisher { - let retrievedPeripherals = centralManager.retrieveConnectedPeripherals(withServices: serviceUUIDs) - return observePeripherals(from: retrievedPeripherals) + return waitUntilPoweredOn().flatMap { wrapper -> AnyPublisher in + let retrievedPeripherals = wrapper.retrieveConnectedPeripherals(withServices: serviceUUIDs) + return self.observePeripherals(from: retrievedPeripherals) + }.eraseToAnyPublisher() } public func scanForPeripherals(withServices services: [CBUUID]?, diff --git a/Source/Central/CBCentralManagerWrapper.swift b/Source/Central/CBCentralManagerWrapper.swift index 632af3e..680cab3 100644 --- a/Source/Central/CBCentralManagerWrapper.swift +++ b/Source/Central/CBCentralManagerWrapper.swift @@ -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] @@ -32,6 +33,10 @@ final class StandardCBCentralManagerWrapper: CBCentralManagerWrapper { wrappedManager.isScanning } + var state: CBManagerState { + wrappedManager.state + } + let wrappedManager: CBCentralManager init(with manager: CBCentralManager) { diff --git a/Tests/BLECentralManagerTests.swift b/Tests/BLECentralManagerTests.swift index e2bef95..c994e54 100644 --- a/Tests/BLECentralManagerTests.swift +++ b/Tests/BLECentralManagerTests.swift @@ -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: []) @@ -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: []) @@ -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() { @@ -219,7 +280,7 @@ class BLECentralManagerTests: XCTestCase { // Then wait(for: [observeDidUpdateANCSAuthorizationExpectation], timeout: 0.005) - XCTAssertEqual(peripheralProvider.buildBLEPeripheralWasCalledCount, 1) + XCTAssertEqual(peripheralProvider.provideBLEPeripheralWasCalledCount, 1) XCTAssertNotNil(observedPeripheral) } diff --git a/Tests/Mocks/BLEPeripheralMocks.swift b/Tests/Mocks/BLEPeripheralMocks.swift index da05c91..d932916 100644 --- a/Tests/Mocks/BLEPeripheralMocks.swift +++ b/Tests/Mocks/BLEPeripheralMocks.swift @@ -11,7 +11,7 @@ import CoreBluetooth import Combine @testable import BLECombineKit -final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral { +final class MockBLEPeripheral: BLETrackedPeripheral { let connectionState = CurrentValueSubject(false) var peripheral: CBPeripheralWrapper @@ -19,6 +19,10 @@ final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral { self.peripheral = MockCBPeripheralWrapper() } + init(peripheral: MockCBPeripheralWrapper) { + self.peripheral = peripheral + } + public func observeConnectionState() -> AnyPublisher { return Just.init(true).eraseToAnyPublisher() } @@ -26,8 +30,8 @@ final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral { var connectWasCalled = false func connect(with options: [String : Any]?) -> AnyPublisher { 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() } @@ -35,6 +39,7 @@ final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral { var disconnectWasCalled = false func disconnect() -> AnyPublisher { disconnectWasCalled = true + self.connectionState.send(false) return Empty(completeImmediately: true).eraseToAnyPublisher() } diff --git a/Tests/Mocks/MockBLEPeripheralProvider.swift b/Tests/Mocks/MockBLEPeripheralProvider.swift index 4d2b0b0..fa73e36 100644 --- a/Tests/Mocks/MockBLEPeripheralProvider.swift +++ b/Tests/Mocks/MockBLEPeripheralProvider.swift @@ -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) } } @@ -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) } } diff --git a/Tests/Mocks/MockCBCentralManagerWrapper.swift b/Tests/Mocks/MockCBCentralManagerWrapper.swift index 8ee1dfa..8648e0c 100644 --- a/Tests/Mocks/MockCBCentralManagerWrapper.swift +++ b/Tests/Mocks/MockCBCentralManagerWrapper.swift @@ -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