From bf31f9e816bf0f9a6508e8e58921dfdf5ea6abd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Janeczek?= Date: Tue, 22 May 2018 13:15:11 +0200 Subject: [PATCH] Added documentation for `CBPeripheralManager` --- README.md | 4 +- RxBluetoothKit.xcodeproj/project.pbxproj | 4 - Source/CentralManager.swift | 6 +- Source/PeripheralManager.swift | 161 ++++++++++++++++++ Templates/Mock.swifttemplate | 9 +- Tests/Autogenerated/Mock.generated.swift | 8 +- .../_CentralManager.generated.swift | 6 +- .../_PeripheralManager.generated.swift | 161 ++++++++++++++++++ ...ripheralManagerTest+StartAdvertising.swift | 8 +- 9 files changed, 348 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 28d8b342..52719b85 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,14 @@ Follow [Polidea's Blog](https://www.polidea.com/blog/RxBluetoothKit_The_most_sim ## Features - [x] CBCentralManger RxSwift support +- [x] CBPeripheralManger RxSwift support - [x] CBPeripheral RxSwift support - [x] Scan sharing - [x] Scan queueing - [x] Bluetooth error bubbling ## Sample -In Example folder you can find application we've provided to you. It's a great place to dig in, once you want to see everything in action. App provides most of the common usages of RxBluetoothKit. +In ExampleApp folder you can find application we've provided to you. It's a great place to dig in, once you want to see everything in action. App provides most of the common usages of RxBluetoothKit. ## Installation @@ -60,6 +61,7 @@ Library is built on top of Apple's CoreBluetooth. It has multiple components, that should be familiar to you: - CentralManager +- PeripheralManager - ScannedPeripheral - Peripheral - Service diff --git a/RxBluetoothKit.xcodeproj/project.pbxproj b/RxBluetoothKit.xcodeproj/project.pbxproj index 80f46303..0fef85e4 100644 --- a/RxBluetoothKit.xcodeproj/project.pbxproj +++ b/RxBluetoothKit.xcodeproj/project.pbxproj @@ -95,7 +95,6 @@ 1D49C69C201B6568000610F8 /* CentralManagerTest+ObserveConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D49C699201B6568000610F8 /* CentralManagerTest+ObserveConnect.swift */; }; 1D4CC1A120B4012400C14A9D /* PeripheralManagerTest+AddService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4CC1A020B4012400C14A9D /* PeripheralManagerTest+AddService.swift */; }; 1D4CC1A220B4012400C14A9D /* PeripheralManagerTest+AddService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4CC1A020B4012400C14A9D /* PeripheralManagerTest+AddService.swift */; }; - 1D4CC1A320B4012400C14A9D /* PeripheralManagerTest+AddService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4CC1A020B4012400C14A9D /* PeripheralManagerTest+AddService.swift */; }; 1D4CC1A520B40F7D00C14A9D /* PeripheralManagerTest+Observables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4CC1A420B40F7D00C14A9D /* PeripheralManagerTest+Observables.swift */; }; 1D4CC1A620B40F7D00C14A9D /* PeripheralManagerTest+Observables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4CC1A420B40F7D00C14A9D /* PeripheralManagerTest+Observables.swift */; }; 1D4CC1A720B40F7D00C14A9D /* PeripheralManagerTest+Observables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4CC1A420B40F7D00C14A9D /* PeripheralManagerTest+Observables.swift */; }; @@ -126,7 +125,6 @@ 1D73E84320A32CC7009167AA /* _PeripheralManager.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D73E83C20A32C98009167AA /* _PeripheralManager.generated.swift */; }; 1D73E84620A32D0B009167AA /* PeripheralManagerTest+StartAdvertising.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D73E84520A32D0B009167AA /* PeripheralManagerTest+StartAdvertising.swift */; }; 1D73E84720A32D0B009167AA /* PeripheralManagerTest+StartAdvertising.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D73E84520A32D0B009167AA /* PeripheralManagerTest+StartAdvertising.swift */; }; - 1D73E84820A32D0B009167AA /* PeripheralManagerTest+StartAdvertising.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D73E84520A32D0B009167AA /* PeripheralManagerTest+StartAdvertising.swift */; }; 1D73E84A20A32D26009167AA /* BasePeripheralManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D73E84920A32D26009167AA /* BasePeripheralManagerTest.swift */; }; 1D73E84B20A32D26009167AA /* BasePeripheralManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D73E84920A32D26009167AA /* BasePeripheralManagerTest.swift */; }; 1D73E84C20A32D26009167AA /* BasePeripheralManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D73E84920A32D26009167AA /* BasePeripheralManagerTest.swift */; }; @@ -1268,7 +1266,6 @@ 4CB5044820457A2400031AA7 /* PeripheralTest+DescriptorsDiscover.swift in Sources */, 4CB5044D204582D700031AA7 /* PeripheralTest+DescriptorsOperation.swift in Sources */, 1D490B681FFF8E1200D3F871 /* _Descriptor.generated.swift in Sources */, - 1D4CC1A320B4012400C14A9D /* PeripheralManagerTest+AddService.swift in Sources */, 4C2E13FD20401EC300143D4D /* PeripheralTest+Service.swift in Sources */, 4C6A2CE1203DBABD0085EE1E /* UUIDIdentifiableTest.swift in Sources */, 1D73E84C20A32D26009167AA /* BasePeripheralManagerTest.swift in Sources */, @@ -1288,7 +1285,6 @@ 1D490B631FFF8E1200D3F871 /* _BluetoothError.generated.swift in Sources */, 1DCA1EF72028680D0090D14E /* CharacteristicNotificationManagerTest.swift in Sources */, 1D490B781FFF8F5500D3F871 /* _RestoredState.generated.swift in Sources */, - 1D73E84820A32D0B009167AA /* PeripheralManagerTest+StartAdvertising.swift in Sources */, 1D8116CA1FFE0F1700147BF5 /* Mock.generated.swift in Sources */, 4C2E13F920401D6000143D4D /* BasePeripheralTest.swift in Sources */, 1D73E84320A32CC7009167AA /* _PeripheralManager.generated.swift in Sources */, diff --git a/Source/CentralManager.swift b/Source/CentralManager.swift index 384efc6a..43e55e18 100644 --- a/Source/CentralManager.swift +++ b/Source/CentralManager.swift @@ -11,7 +11,7 @@ public typealias DisconnectionReason = Error /// public `CentralManager`'s functions you should make sure that Bluetooth is turned on and powered on. It can be done /// by calling and observing returned value of `observeState()` and then chaining it with `scanForPeripherals(_:options:)`: /// ``` -/// centralManager.observeState +/// let disposable = centralManager.observeState /// .startWith(centralManager.state) /// .filter { $0 == .poweredOn } /// .take(1) @@ -19,6 +19,10 @@ public typealias DisconnectionReason = Error /// ``` /// As a result you will receive `ScannedPeripheral` which contains `Peripheral` object, `AdvertisementData` and /// peripheral's RSSI registered during discovery. You can then `establishConnection(_:options:)` and do other operations. +/// You can also simply stop scanning with just disposing it: +/// ``` +/// disposable.dispose() +/// ``` /// - seealso: `Peripheral` public class CentralManager: ManagerType { diff --git a/Source/PeripheralManager.swift b/Source/PeripheralManager.swift index 27cee416..4f3f2311 100644 --- a/Source/PeripheralManager.swift +++ b/Source/PeripheralManager.swift @@ -2,8 +2,26 @@ import Foundation import CoreBluetooth import RxSwift +/// PeripheralManager is a class implementing ReactiveX API which wraps all Core Bluetooth Peripheral's functions allowing to +/// advertising, publishing L2CAP channels and more. +/// You can start using this class by add services and starting advertising. +/// Before calling any public `PeripheralManager`'s functions you should make sure that Bluetooth is turned on and powered on. It can be done +/// by calling and observing returned value of `observeState()` and then chaining it with `add(_:)` with `startAdvertising(_:)`: +/// ``` +/// let disposable = centralManager.observeState +/// .startWith(centralManager.state) +/// .filter { $0 == .poweredOn } +/// .take(1) +/// .flatMap { centralManager.add(myService) } +/// .flatMap { centralManager.startAdvertising(myAdvertisementData) } +/// ``` +/// As a result your peripheral will start advertising. To stop advertising simply dispose it: +/// ``` +/// disposable.dispose() +/// ``` class PeripheralManager: ManagerType { + /// Implementation of CBPeripheralManager public let manager: CBPeripheralManager let delegateWrapper: CBPeripheralManagerDelegateWrapper @@ -42,6 +60,12 @@ class PeripheralManager: ManagerType { self.init(peripheralManager: peripheralManager, delegateWrapper: delegateWrapper) } + /// Returns the app’s authorization status for sharing data while in the background state. + /// Wrapper of `CBPeripheralManager.authorizationStatus()` method. + static var authorizationStatus: CBPeripheralManagerAuthorizationStatus { + return CBPeripheralManager.authorizationStatus() + } + // MARK: State public var state: BluetoothState { @@ -54,6 +78,32 @@ class PeripheralManager: ManagerType { // MARK: Advertising + /// Starts peripheral advertising on subscription. It create inifinite observable + /// which emits only one next value, of enum type `StartAdvertisingResult`, just + /// after advertising start succeeds. + /// For more info of what specific `StartAdvertisingResult` enum cases means please + /// refer to ``StartAdvertisingResult` documentation. + /// + /// There can be only one ongoing advertising (CoreBluetooth limit). + /// It will return `advertisingInProgress` error if this method will be called when + /// it is already advertising. + /// + /// Advertising is automatically stopped just after disposing of the subscription. + /// + /// It can return `BluetoothError.advertisingStartFailed` error when start advertisement failed + /// + /// - parameter advertisementData: Services of peripherals to search for. Nil value will accept all peripherals. + /// - returns: Infinite observable which emit `StartAdvertisingResult` when advertisement started. + /// + /// Observable can ends with following errors: + /// * `BluetoothError.advertisingInProgress` + /// * `BluetoothError.advertisingStartFailed` + /// * `BluetoothError.destroyed` + /// * `BluetoothError.bluetoothUnsupported` + /// * `BluetoothError.bluetoothUnauthorized` + /// * `BluetoothError.bluetoothPoweredOff` + /// * `BluetoothError.bluetoothInUnknownState` + /// * `BluetoothError.bluetoothResetting` public func startAdvertising(_ advertisementData: [String: Any]?) -> Observable { let observable: Observable = Observable.create { [weak self] observer in guard let strongSelf = self else { @@ -99,6 +149,21 @@ class PeripheralManager: ManagerType { // MARK: Services + /// Function that triggers `CBPeripheralManager.add(_:)` method and waits for + /// delegate `CBPeripheralManagerDelegate.peripheralManager(_:didAdd:error:)` result. + /// If it will receive non nil error in result than `Observable` will emit `BluetoothError.addingServiceFailed` error. + /// Add method is called after subscription to `Observable` is made. + /// - Parameter service: `Characteristic` to read value from + /// - Returns: `Single` which emits `next` with given characteristic when value is ready to read. + /// + /// Observable can ends with following errors: + /// * `BluetoothError.addingServiceFailed` + /// * `BluetoothError.destroyed` + /// * `BluetoothError.bluetoothUnsupported` + /// * `BluetoothError.bluetoothUnauthorized` + /// * `BluetoothError.bluetoothPoweredOff` + /// * `BluetoothError.bluetoothInUnknownState` + /// * `BluetoothError.bluetoothResetting` public func add(_ service: CBMutableService) -> Single { let observable = delegateWrapper .didAddService @@ -116,30 +181,58 @@ class PeripheralManager: ManagerType { }.asSingle() } + /// Wrapper for `CBPeripheralManager.remove(_:)` method public func remove(_ service: CBMutableService) { manager.remove(service) } + /// Wrapper for `CBPeripheralManager.removeAllServices()` method public func removeAllServices() { manager.removeAllServices() } // MARK: Read & Write + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:didReceiveRead:)` results + /// - returns: Observable that emits `next` event whenever didReceiveRead occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `BluetoothError.destroyed` + /// * `BluetoothError.bluetoothUnsupported` + /// * `BluetoothError.bluetoothUnauthorized` + /// * `BluetoothError.bluetoothPoweredOff` + /// * `BluetoothError.bluetoothInUnknownState` + /// * `BluetoothError.bluetoothResetting` public func observeDidReceiveRead() -> Observable { return ensure(.poweredOn, observable: delegateWrapper.didReceiveRead) } + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:didReceiveWrite:)` results + /// - returns: Observable that emits `next` event whenever didReceiveWrite occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `BluetoothError.destroyed` + /// * `BluetoothError.bluetoothUnsupported` + /// * `BluetoothError.bluetoothUnauthorized` + /// * `BluetoothError.bluetoothPoweredOff` + /// * `BluetoothError.bluetoothInUnknownState` + /// * `BluetoothError.bluetoothResetting` public func observeDidReceiveWrite() -> Observable<[CBATTRequest]> { return ensure(.poweredOn, observable: delegateWrapper.didReceiveWrite) } + /// Wrapper for `CBPeripheralManager.respond(to:withResult:)` method public func respond(to request: CBATTRequest, withResult result: CBATTError.Code) { manager.respond(to: request, withResult: result) } // MARK: Updating value + /// Wrapper for `CBPeripheralManager.updateValue(_:for:onSubscribedCentrals:)` method public func updateValue( _ value: Data, for characteristic: CBMutableCharacteristic, @@ -147,16 +240,52 @@ class PeripheralManager: ManagerType { return manager.updateValue(value, for: characteristic, onSubscribedCentrals: centrals) } + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManagerIsReady(toUpdateSubscribers:)` results + /// - returns: Observable that emits `next` event whenever isReadyToUpdateSubscribers occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `BluetoothError.destroyed` + /// * `BluetoothError.bluetoothUnsupported` + /// * `BluetoothError.bluetoothUnauthorized` + /// * `BluetoothError.bluetoothPoweredOff` + /// * `BluetoothError.bluetoothInUnknownState` + /// * `BluetoothError.bluetoothResetting` public func observeIsReadyToUpdateSubscribers() -> Observable { return ensure(.poweredOn, observable: delegateWrapper.isReady) } // MARK: Subscribing + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:central:didSubscribeTo:)` results + /// - returns: Observable that emits `next` event whenever didSubscribeTo occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `BluetoothError.destroyed` + /// * `BluetoothError.bluetoothUnsupported` + /// * `BluetoothError.bluetoothUnauthorized` + /// * `BluetoothError.bluetoothPoweredOff` + /// * `BluetoothError.bluetoothInUnknownState` + /// * `BluetoothError.bluetoothResetting` public func observeOnSubscribe() -> Observable<(CBCentral, CBCharacteristic)> { return ensure(.poweredOn, observable: delegateWrapper.didSubscribeTo) } + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:central:didUnsubscribeFrom:)` results + /// - returns: Observable that emits `next` event whenever didUnsubscribeFrom occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `BluetoothError.destroyed` + /// * `BluetoothError.bluetoothUnsupported` + /// * `BluetoothError.bluetoothUnauthorized` + /// * `BluetoothError.bluetoothPoweredOff` + /// * `BluetoothError.bluetoothInUnknownState` + /// * `BluetoothError.bluetoothResetting` public func observeOnUnsubscribe() -> Observable<(CBCentral, CBCharacteristic)> { return ensure(.poweredOn, observable: delegateWrapper.didUnsubscribeFrom) } @@ -164,6 +293,26 @@ class PeripheralManager: ManagerType { // MARK: L2CAP #if os(iOS) || os(tvOS) || os(watchOS) + + /// Starts publishing L2CAP channel on subscription. It create inifinite observable + /// which emits only one next value, of `CBL2CAPPSM` type, just + /// after L2CAP channel has been published. + /// + /// Channel is automatically unpublished just after disposing of the subscription. + /// + /// It can return `publishingL2CAPChannelFailed` error when publishing channel failed + /// + /// - parameter encryptionRequired: Publishing channel with or without encryption. + /// - returns: Infinite observable which emit `CBL2CAPPSM` when channel published. + /// + /// Observable can ends with following errors: + /// * `BluetoothError.publishingL2CAPChannelFailed` + /// * `BluetoothError.destroyed` + /// * `BluetoothError.bluetoothUnsupported` + /// * `BluetoothError.bluetoothUnauthorized` + /// * `BluetoothError.bluetoothPoweredOff` + /// * `BluetoothError.bluetoothInUnknownState` + /// * `BluetoothError.bluetoothResetting` @available(iOS 11, tvOS 11, watchOS 4, *) public func publishL2CAPChannel(withEncryption encryptionRequired: Bool) -> Observable { let observable: Observable = Observable.create { [weak self] observer in @@ -195,6 +344,18 @@ class PeripheralManager: ManagerType { return self.ensure(.poweredOn, observable: observable) } + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:didOpen:error:)` results + /// - returns: Observable that emits `next` event whenever didOpen occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `BluetoothError.destroyed` + /// * `BluetoothError.bluetoothUnsupported` + /// * `BluetoothError.bluetoothUnauthorized` + /// * `BluetoothError.bluetoothPoweredOff` + /// * `BluetoothError.bluetoothInUnknownState` + /// * `BluetoothError.bluetoothResetting` @available(iOS 11, tvOS 11, watchOS 4, *) public func observeDidOpenL2CAPChannel() -> Observable<(CBL2CAPChannel?, Error?)> { return ensure(.poweredOn, observable: delegateWrapper.didOpenChannel) diff --git a/Templates/Mock.swifttemplate b/Templates/Mock.swifttemplate index 336267b2..edeb3193 100644 --- a/Templates/Mock.swifttemplate +++ b/Templates/Mock.swifttemplate @@ -135,13 +135,14 @@ class <%= typeToMock.name %>Mock: <%= supertypeName %> { let methodReturnsName = "\(formattedName)Returns" let methodReturnName = "\(formattedName)Return" let isReturningType = !method.returnTypeName.isVoid + let isStaticText = method.isClass ? "static " : "" let methodReturnDeclaration = isReturningType ? " -> \(Utils.changeTypeName(method.returnTypeName.name))" : "" -%> - var <%= methodParamsName %>: [(<%= Utils.printMethodParamTypes(method) %>)] = [] + <%= isStaticText %>var <%= methodParamsName %>: [(<%= Utils.printMethodParamTypes(method) %>)] = [] <% if isReturningType { -%> - var <%= methodReturnsName %>: [<%= Utils.changeTypeName(method.returnTypeName.name) %>] = [] - var <%= methodReturnName %>: <%= Utils.changeTypeName(method.returnTypeName.name) %>? + <%= isStaticText %>var <%= methodReturnsName %>: [<%= Utils.changeTypeName(method.returnTypeName.name) %>] = [] + <%= isStaticText %>var <%= methodReturnName %>: <%= Utils.changeTypeName(method.returnTypeName.name) %>? <% } -%> - func <%= Utils.printMethodName(method) %><%= methodReturnDeclaration %> { + <%= isStaticText %>func <%= Utils.printMethodName(method) %><%= methodReturnDeclaration %> { <%= methodParamsName %>.append((<%= method.parameters.reduce("", { "\($0)\($1.name), " }).dropLast(2) %>)) <% if isReturningType { -%> if <%= methodReturnsName %>.isEmpty { diff --git a/Tests/Autogenerated/Mock.generated.swift b/Tests/Autogenerated/Mock.generated.swift index a40dbdf0..36f328bb 100644 --- a/Tests/Autogenerated/Mock.generated.swift +++ b/Tests/Autogenerated/Mock.generated.swift @@ -91,10 +91,10 @@ class CBPeripheralManagerMock: CBManagerMock { init(delegate: CBPeripheralManagerDelegate?, queue: DispatchQueue?) { } - var authorizationStatusParams: [()] = [] - var authorizationStatusReturns: [CBPeripheralManagerAuthorizationStatus] = [] - var authorizationStatusReturn: CBPeripheralManagerAuthorizationStatus? - func authorizationStatus() -> CBPeripheralManagerAuthorizationStatus { + static var authorizationStatusParams: [()] = [] + static var authorizationStatusReturns: [CBPeripheralManagerAuthorizationStatus] = [] + static var authorizationStatusReturn: CBPeripheralManagerAuthorizationStatus? + static func authorizationStatus() -> CBPeripheralManagerAuthorizationStatus { authorizationStatusParams.append(()) if authorizationStatusReturns.isEmpty { return authorizationStatusReturn! diff --git a/Tests/Autogenerated/_CentralManager.generated.swift b/Tests/Autogenerated/_CentralManager.generated.swift index 83dc17cc..1243916d 100644 --- a/Tests/Autogenerated/_CentralManager.generated.swift +++ b/Tests/Autogenerated/_CentralManager.generated.swift @@ -12,7 +12,7 @@ typealias DisconnectionReason = Error /// public `_CentralManager`'s functions you should make sure that Bluetooth is turned on and powered on. It can be done /// by calling and observing returned value of `observeState()` and then chaining it with `scanForPeripherals(_:options:)`: /// ``` -/// centralManager.observeState +/// let disposable = centralManager.observeState /// .startWith(centralManager.state) /// .filter { $0 == .poweredOn } /// .take(1) @@ -20,6 +20,10 @@ typealias DisconnectionReason = Error /// ``` /// As a result you will receive `_ScannedPeripheral` which contains `_Peripheral` object, `AdvertisementData` and /// peripheral's RSSI registered during discovery. You can then `establishConnection(_:options:)` and do other operations. +/// You can also simply stop scanning with just disposing it: +/// ``` +/// disposable.dispose() +/// ``` /// - seealso: `_Peripheral` class _CentralManager: _ManagerType { diff --git a/Tests/Autogenerated/_PeripheralManager.generated.swift b/Tests/Autogenerated/_PeripheralManager.generated.swift index 12f83508..93ddb1a2 100644 --- a/Tests/Autogenerated/_PeripheralManager.generated.swift +++ b/Tests/Autogenerated/_PeripheralManager.generated.swift @@ -3,8 +3,26 @@ import CoreBluetooth @testable import RxBluetoothKit import RxSwift +/// _PeripheralManager is a class implementing ReactiveX API which wraps all Core Bluetooth _Peripheral's functions allowing to +/// advertising, publishing L2CAP channels and more. +/// You can start using this class by add services and starting advertising. +/// Before calling any public `_PeripheralManager`'s functions you should make sure that Bluetooth is turned on and powered on. It can be done +/// by calling and observing returned value of `observeState()` and then chaining it with `add(_:)` with `startAdvertising(_:)`: +/// ``` +/// let disposable = centralManager.observeState +/// .startWith(centralManager.state) +/// .filter { $0 == .poweredOn } +/// .take(1) +/// .flatMap { centralManager.add(myService) } +/// .flatMap { centralManager.startAdvertising(myAdvertisementData) } +/// ``` +/// As a result your peripheral will start advertising. To stop advertising simply dispose it: +/// ``` +/// disposable.dispose() +/// ``` class _PeripheralManager: _ManagerType { + /// Implementation of CBPeripheralManagerMock let manager: CBPeripheralManagerMock let delegateWrapper: CBPeripheralManagerDelegateWrapperMock @@ -43,6 +61,12 @@ class _PeripheralManager: _ManagerType { self.init(peripheralManager: peripheralManager, delegateWrapper: delegateWrapper) } + /// Returns the app’s authorization status for sharing data while in the background state. + /// Wrapper of `CBPeripheralManagerMock.authorizationStatus()` method. + static var authorizationStatus: CBPeripheralManagerAuthorizationStatus { + return CBPeripheralManagerMock.authorizationStatus() + } + // MARK: State var state: BluetoothState { @@ -55,6 +79,32 @@ class _PeripheralManager: _ManagerType { // MARK: Advertising + /// Starts peripheral advertising on subscription. It create inifinite observable + /// which emits only one next value, of enum type `StartAdvertisingResult`, just + /// after advertising start succeeds. + /// For more info of what specific `StartAdvertisingResult` enum cases means please + /// refer to ``StartAdvertisingResult` documentation. + /// + /// There can be only one ongoing advertising (CoreBluetooth limit). + /// It will return `advertisingInProgress` error if this method will be called when + /// it is already advertising. + /// + /// Advertising is automatically stopped just after disposing of the subscription. + /// + /// It can return `_BluetoothError.advertisingStartFailed` error when start advertisement failed + /// + /// - parameter advertisementData: Services of peripherals to search for. Nil value will accept all peripherals. + /// - returns: Infinite observable which emit `StartAdvertisingResult` when advertisement started. + /// + /// Observable can ends with following errors: + /// * `_BluetoothError.advertisingInProgress` + /// * `_BluetoothError.advertisingStartFailed` + /// * `_BluetoothError.destroyed` + /// * `_BluetoothError.bluetoothUnsupported` + /// * `_BluetoothError.bluetoothUnauthorized` + /// * `_BluetoothError.bluetoothPoweredOff` + /// * `_BluetoothError.bluetoothInUnknownState` + /// * `_BluetoothError.bluetoothResetting` func startAdvertising(_ advertisementData: [String: Any]?) -> Observable { let observable: Observable = Observable.create { [weak self] observer in guard let strongSelf = self else { @@ -100,6 +150,21 @@ class _PeripheralManager: _ManagerType { // MARK: Services + /// Function that triggers `CBPeripheralManagerMock.add(_:)` method and waits for + /// delegate `CBPeripheralManagerDelegate.peripheralManager(_:didAdd:error:)` result. + /// If it will receive non nil error in result than `Observable` will emit `_BluetoothError.addingServiceFailed` error. + /// Add method is called after subscription to `Observable` is made. + /// - Parameter service: `_Characteristic` to read value from + /// - Returns: `Single` which emits `next` with given characteristic when value is ready to read. + /// + /// Observable can ends with following errors: + /// * `_BluetoothError.addingServiceFailed` + /// * `_BluetoothError.destroyed` + /// * `_BluetoothError.bluetoothUnsupported` + /// * `_BluetoothError.bluetoothUnauthorized` + /// * `_BluetoothError.bluetoothPoweredOff` + /// * `_BluetoothError.bluetoothInUnknownState` + /// * `_BluetoothError.bluetoothResetting` func add(_ service: CBMutableService) -> Single { let observable = delegateWrapper .didAddService @@ -117,30 +182,58 @@ class _PeripheralManager: _ManagerType { }.asSingle() } + /// Wrapper for `CBPeripheralManagerMock.remove(_:)` method func remove(_ service: CBMutableService) { manager.remove(service) } + /// Wrapper for `CBPeripheralManagerMock.removeAllServices()` method func removeAllServices() { manager.removeAllServices() } // MARK: Read & Write + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:didReceiveRead:)` results + /// - returns: Observable that emits `next` event whenever didReceiveRead occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `_BluetoothError.destroyed` + /// * `_BluetoothError.bluetoothUnsupported` + /// * `_BluetoothError.bluetoothUnauthorized` + /// * `_BluetoothError.bluetoothPoweredOff` + /// * `_BluetoothError.bluetoothInUnknownState` + /// * `_BluetoothError.bluetoothResetting` func observeDidReceiveRead() -> Observable { return ensure(.poweredOn, observable: delegateWrapper.didReceiveRead) } + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:didReceiveWrite:)` results + /// - returns: Observable that emits `next` event whenever didReceiveWrite occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `_BluetoothError.destroyed` + /// * `_BluetoothError.bluetoothUnsupported` + /// * `_BluetoothError.bluetoothUnauthorized` + /// * `_BluetoothError.bluetoothPoweredOff` + /// * `_BluetoothError.bluetoothInUnknownState` + /// * `_BluetoothError.bluetoothResetting` func observeDidReceiveWrite() -> Observable<[CBATTRequestMock]> { return ensure(.poweredOn, observable: delegateWrapper.didReceiveWrite) } + /// Wrapper for `CBPeripheralManagerMock.respond(to:withResult:)` method func respond(to request: CBATTRequestMock, withResult result: CBATTError.Code) { manager.respond(to: request, withResult: result) } // MARK: Updating value + /// Wrapper for `CBPeripheralManagerMock.updateValue(_:for:onSubscribedCentrals:)` method func updateValue( _ value: Data, for characteristic: CBMutableCharacteristic, @@ -148,16 +241,52 @@ class _PeripheralManager: _ManagerType { return manager.updateValue(value, for: characteristic, onSubscribedCentrals: centrals) } + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManagerIsReady(toUpdateSubscribers:)` results + /// - returns: Observable that emits `next` event whenever isReadyToUpdateSubscribers occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `_BluetoothError.destroyed` + /// * `_BluetoothError.bluetoothUnsupported` + /// * `_BluetoothError.bluetoothUnauthorized` + /// * `_BluetoothError.bluetoothPoweredOff` + /// * `_BluetoothError.bluetoothInUnknownState` + /// * `_BluetoothError.bluetoothResetting` func observeIsReadyToUpdateSubscribers() -> Observable { return ensure(.poweredOn, observable: delegateWrapper.isReady) } // MARK: Subscribing + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:central:didSubscribeTo:)` results + /// - returns: Observable that emits `next` event whenever didSubscribeTo occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `_BluetoothError.destroyed` + /// * `_BluetoothError.bluetoothUnsupported` + /// * `_BluetoothError.bluetoothUnauthorized` + /// * `_BluetoothError.bluetoothPoweredOff` + /// * `_BluetoothError.bluetoothInUnknownState` + /// * `_BluetoothError.bluetoothResetting` func observeOnSubscribe() -> Observable<(CBCentralMock, CBCharacteristicMock)> { return ensure(.poweredOn, observable: delegateWrapper.didSubscribeTo) } + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:central:didUnsubscribeFrom:)` results + /// - returns: Observable that emits `next` event whenever didUnsubscribeFrom occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `_BluetoothError.destroyed` + /// * `_BluetoothError.bluetoothUnsupported` + /// * `_BluetoothError.bluetoothUnauthorized` + /// * `_BluetoothError.bluetoothPoweredOff` + /// * `_BluetoothError.bluetoothInUnknownState` + /// * `_BluetoothError.bluetoothResetting` func observeOnUnsubscribe() -> Observable<(CBCentralMock, CBCharacteristicMock)> { return ensure(.poweredOn, observable: delegateWrapper.didUnsubscribeFrom) } @@ -165,6 +294,26 @@ class _PeripheralManager: _ManagerType { // MARK: L2CAP #if os(iOS) || os(tvOS) || os(watchOS) + + /// Starts publishing L2CAP channel on subscription. It create inifinite observable + /// which emits only one next value, of `CBL2CAPPSM` type, just + /// after L2CAP channel has been published. + /// + /// Channel is automatically unpublished just after disposing of the subscription. + /// + /// It can return `publishingL2CAPChannelFailed` error when publishing channel failed + /// + /// - parameter encryptionRequired: Publishing channel with or without encryption. + /// - returns: Infinite observable which emit `CBL2CAPPSM` when channel published. + /// + /// Observable can ends with following errors: + /// * `_BluetoothError.publishingL2CAPChannelFailed` + /// * `_BluetoothError.destroyed` + /// * `_BluetoothError.bluetoothUnsupported` + /// * `_BluetoothError.bluetoothUnauthorized` + /// * `_BluetoothError.bluetoothPoweredOff` + /// * `_BluetoothError.bluetoothInUnknownState` + /// * `_BluetoothError.bluetoothResetting` @available(iOS 11, tvOS 11, watchOS 4, *) func publishL2CAPChannel(withEncryption encryptionRequired: Bool) -> Observable { let observable: Observable = Observable.create { [weak self] observer in @@ -196,6 +345,18 @@ class _PeripheralManager: _ManagerType { return self.ensure(.poweredOn, observable: observable) } + /// Continuous observer for `CBPeripheralManagerDelegate.peripheralManager(_:didOpen:error:)` results + /// - returns: Observable that emits `next` event whenever didOpen occurs. + /// + /// It's **infinite** stream, so `.complete` is never called. + /// + /// Observable can ends with following errors: + /// * `_BluetoothError.destroyed` + /// * `_BluetoothError.bluetoothUnsupported` + /// * `_BluetoothError.bluetoothUnauthorized` + /// * `_BluetoothError.bluetoothPoweredOff` + /// * `_BluetoothError.bluetoothInUnknownState` + /// * `_BluetoothError.bluetoothResetting` @available(iOS 11, tvOS 11, watchOS 4, *) func observeDidOpenL2CAPChannel() -> Observable<(CBL2CAPChannelMock?, Error?)> { return ensure(.poweredOn, observable: delegateWrapper.didOpenChannel) diff --git a/Tests/PeripheralManagerTest+StartAdvertising.swift b/Tests/PeripheralManagerTest+StartAdvertising.swift index 98aea598..ef5a4804 100644 --- a/Tests/PeripheralManagerTest+StartAdvertising.swift +++ b/Tests/PeripheralManagerTest+StartAdvertising.swift @@ -70,7 +70,7 @@ class PeripheralManagerTest_StartAdvertising: BasePeripheralManagerTest { XCTAssertTrue(manager.isAdvertisingOngoing, "should set isAdvertisingOngoing to true") } - func testProperOngoingResult() { + func testProperAttachedToExternalAdvertisingResult() { let observer = setUpStartAdvertising(nil) peripheralManagerMock.state = .poweredOn peripheralManagerMock.isAdvertising = true @@ -78,15 +78,15 @@ class PeripheralManagerTest_StartAdvertising: BasePeripheralManagerTest { testScheduler.advanceTo(subscribeTime) - XCTAssertEqual(observer.events.count, 1, "should get ongoing advertising result") - XCTAssertNotNil(observer.events[0].value.element, "should get ongoing advertising result") + XCTAssertEqual(observer.events.count, 1, "should get attached to external advertising result") + XCTAssertNotNil(observer.events[0].value.element, "should get attached to external advertising result") let ongoing: Bool if case StartAdvertisingResult.attachedToExternalAdvertising(let result) = observer.events[0].value.element! { ongoing = result != nil } else { ongoing = false } - XCTAssertTrue(ongoing, "should get ongoing advertising result") + XCTAssertTrue(ongoing, "should get attached to external advertising result") } func testThrowErrorWhenStartAdvertisingFailed() {