Skip to content

Commit

Permalink
Added documentation for CBPeripheralManager
Browse files Browse the repository at this point in the history
  • Loading branch information
pouljohn1 committed May 22, 2018
1 parent 3c1e396 commit bf31f9e
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 19 deletions.
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions RxBluetoothKit.xcodeproj/project.pbxproj
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down
6 changes: 5 additions & 1 deletion Source/CentralManager.swift
Expand Up @@ -11,14 +11,18 @@ 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)
/// .flatMap { centralManager.scanForPeripherals(nil) }
/// ```
/// 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 {

Expand Down
161 changes: 161 additions & 0 deletions Source/PeripheralManager.swift
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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<StartAdvertisingResult> {
let observable: Observable<StartAdvertisingResult> = Observable.create { [weak self] observer in
guard let strongSelf = self else {
Expand Down Expand Up @@ -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<CBService> {
let observable = delegateWrapper
.didAddService
Expand All @@ -116,54 +181,138 @@ 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<CBATTRequest> {
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,
onSubscribedCentrals centrals: [CBCentral]?) -> Bool {
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<Void> {
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)
}

// 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<CBL2CAPPSM> {
let observable: Observable<CBL2CAPPSM> = Observable.create { [weak self] observer in
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions Templates/Mock.swifttemplate
Expand Up @@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions Tests/Autogenerated/Mock.generated.swift
Expand Up @@ -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!
Expand Down

0 comments on commit bf31f9e

Please sign in to comment.