Skip to content

Instantly share code, notes, and snippets.

@fdenisnascimento
Created June 9, 2022 17:04
Show Gist options
  • Save fdenisnascimento/8b9d9d606532faf25e813d7beb0943b7 to your computer and use it in GitHub Desktop.
Save fdenisnascimento/8b9d9d606532faf25e813d7beb0943b7 to your computer and use it in GitHub Desktop.
ComposableArchitecture wrapper for CoreBluetooth
import Foundation
import CoreBluetooth
public struct Characteristic: Equatable {
let rawValue: CBCharacteristic?
public let identifier: CBUUID
public let value: Data?
public let isNotifying: Bool
public let descriptors: [Descriptor]?
init(from characteristic: CBCharacteristic) {
rawValue = characteristic
identifier = characteristic.uuid
value = characteristic.value
isNotifying = characteristic.isNotifying
descriptors = characteristic.descriptors?.map(Descriptor.init)
}
init(
identifier: CBUUID,
value: Data?,
isNotifying: Bool,
descriptors: [Descriptor]?
) {
rawValue = nil
self.identifier = identifier
self.value = value
self.isNotifying = isNotifying
self.descriptors = descriptors
}
}
import Foundation
import CoreBluetooth
public struct Descriptor: Equatable {
public enum DescriptorType: Equatable {
case characteristicExtendedProperties(NSNumber?)
case characteristicUserDescription(String?)
case clientCharacteristicConfiguration(NSNumber?)
case serverCharacteristicConfiguration(NSNumber?)
case characteristicFormat(Data?)
case characteristicAggregateFormat(Data?)
}
let rawValue: CBDescriptor?
public let identifier: CBUUID
public let value: DescriptorType?
init(from descriptor: CBDescriptor) {
rawValue = descriptor
identifier = descriptor.uuid
switch descriptor.uuid.uuidString {
case CBUUIDCharacteristicExtendedPropertiesString:
value = .characteristicExtendedProperties(descriptor.value as? NSNumber)
case CBUUIDCharacteristicUserDescriptionString:
value = .characteristicUserDescription(descriptor.value as? String)
case CBUUIDClientCharacteristicConfigurationString:
value = .clientCharacteristicConfiguration(descriptor.value as? NSNumber)
case CBUUIDServerCharacteristicConfigurationString:
value = .serverCharacteristicConfiguration(descriptor.value as? NSNumber)
case CBUUIDCharacteristicFormatString:
value = .characteristicFormat(descriptor.value as? Data)
case CBUUIDCharacteristicAggregateFormatString:
value = .characteristicAggregateFormat(descriptor.value as? Data)
default:
value = nil
}
}
init(identifier: CBUUID, value: DescriptorType?) {
rawValue = nil
self.identifier = identifier
self.value = value
}
}
import Foundation
import CoreBluetooth
import Combine
import ComposableArchitecture
private var dependencies: [AnyHashable: Dependencies] = [:]
private struct Dependencies {
let manager: CBCentralManager
let delegate: BluetoothManager.Delegate
let subscriber: Effect<BluetoothManager.Action, Never>.Subscriber
}
private func couldNotFindBluetoothManager(id: Any) {
assertionFailure(
"""
A Bluetooth manager could not be found with the id \(id). This is considered a programmer error. \
You should not invoke methods on a Bluetooth manager before it has been created or after it \
has been destroyed. Refactor your code to make sure there is a Bluetooth manager created by the \
time you invoke this endpoint.
"""
)
}
private func couldNotFindRawPeripheralValue() {
assertionFailure(
"""
The supplied peripheral did not have a raw value. This is considered a programmer error. \
You should use the .live static function to initialize a peripheral.
"""
)
}
extension BluetoothManager {
public static let live = BluetoothManager(
create: { id, queue, options in
Effect.run { subscriber in
let delegate = Delegate(subscriber)
let manager = CBCentralManager(delegate: delegate, queue: queue, options: options?.toDictionary())
dependencies[id] = Dependencies(manager: manager, delegate: delegate, subscriber: subscriber)
return AnyCancellable {
dependencies[id] = nil
}
}
},
destroy: { id in
.fireAndForget {
dependencies[id]?.subscriber.send(completion: .finished)
dependencies[id] = nil
}
},
connect: { id, peripheral, options in
guard let rawPeripheral = peripheral.rawValue else {
couldNotFindRawPeripheralValue()
return .none
}
return .fireAndForget { dependencies[id]?.manager.connect(rawPeripheral, options: options?.toDictionary()) }
},
cancelConnection: { id, peripheral in
guard let rawPeripheral = peripheral.rawValue else {
couldNotFindRawPeripheralValue()
return .none
}
return .fireAndForget { dependencies[id]?.manager.cancelPeripheralConnection(rawPeripheral) }
},
retrieveConnectedPeripherals: { id, uuids in
guard let dependency = dependencies[id] else {
couldNotFindBluetoothManager(id: id)
return []
}
return dependency
.manager
.retrieveConnectedPeripherals(withServices: uuids)
.map { Peripheral.live(from: $0, subscriber: dependency.subscriber) }
},
retrievePeripherals: { id, uuids in
guard let dependency = dependencies[id] else {
couldNotFindBluetoothManager(id: id)
return []
}
return dependency
.manager
.retrieveConnectedPeripherals(withServices: uuids.map(CBUUID.init))
.map { Peripheral.live(from: $0, subscriber: dependency.subscriber) }
},
scanForPeripherals: { id, services, options in
.fireAndForget { dependencies[id]?.manager.scanForPeripherals(withServices: services, options: options?.toDictionary()) }
},
stopScan: { id in
.fireAndForget { dependencies[id]?.manager.stopScan() }
},
isScanning: { id in
dependencies[id]?.manager.isScanning ?? false
},
registerForConnectionEvents: { id, options in
.fireAndForget { dependencies[id]?.manager.registerForConnectionEvents(options: options?.toDictionary()) }
},
supports: CBCentralManager.supports
)
class Delegate: NSObject, CBCentralManagerDelegate {
let subscriber: Effect<BluetoothManager.Action, Never>.Subscriber
init(_ subscriber: Effect<BluetoothManager.Action, Never>.Subscriber) {
self.subscriber = subscriber
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
subscriber.send(.didConnect(.live(from: peripheral, subscriber: subscriber)))
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
subscriber.send(.didDisconnect(.live(from: peripheral, subscriber: subscriber), error as? CBError))
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
subscriber.send(.didFailToConnect(.live(from: peripheral, subscriber: subscriber), error as? CBError))
}
func centralManager(_ central: CBCentralManager, connectionEventDidOccur event: CBConnectionEvent, for peripheral: CBPeripheral) {
subscriber.send(.connectionEventDidOccur(event, .live(from: peripheral, subscriber: subscriber)))
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
subscriber.send(.didDiscover(.live(from: peripheral, subscriber: subscriber), .init(from: advertisementData), RSSI))
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
subscriber.send(.didUpdateState(central.state))
}
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) {
subscriber.send(.willRestore(.init(from: dict, subscriber: subscriber)))
}
func centralManager(_ central: CBCentralManager, didUpdateANCSAuthorizationFor peripheral: CBPeripheral) {
subscriber.send(.didUpdateANCSAuthorization(.live(from: peripheral, subscriber: subscriber)))
}
}
}
import Foundation
import CoreBluetooth
import ComposableArchitecture
public func _unimplemented(_ function: StaticString, file: StaticString = #file, line: UInt = #line) -> Never {
fatalError(
"""
`\(function)` was called but is not implemented. Be sure to provide an implementation for this endpoint when creating the mock.
""",
file: file,
line: line
)
}
extension BluetoothManager {
public static func mock(
create: @escaping (AnyHashable, DispatchQueue?, InitializationOptions?) -> Effect<Action, Never> = { _, _, _ in
_unimplemented("create")
},
destroy: @escaping (AnyHashable) -> Effect<Never, Never> = { _ in
_unimplemented("destroy")
},
connect: @escaping (AnyHashable, Peripheral, ConnectionOptions?) -> Effect<Never, Never> = { _, _, _ in
_unimplemented("connect")
},
cancelConnection: @escaping (AnyHashable, Peripheral) -> Effect<Never, Never> = { _, _ in
_unimplemented("cancelConnection")
},
retrieveConnectedPeripherals: @escaping (AnyHashable, [CBUUID]) -> [Peripheral] = { _, _ in
_unimplemented("retrieveConnectedPeripherals")
},
retrievePeripherals: @escaping (AnyHashable, [UUID]) -> [Peripheral] = { _, _ in
_unimplemented("retrievePeripherals")
},
scanForPeripherals: @escaping (AnyHashable, [CBUUID]?, ScanOptions?) -> Effect<Never, Never> = { _, _, _ in
_unimplemented("scanForPeripherals")
},
stopScan: @escaping (AnyHashable) -> Effect<Never, Never> = { _ in
_unimplemented("stopScan")
},
isScanning: @escaping (AnyHashable) -> Bool = { _ in
_unimplemented("isScanning")
},
registerForConnectionEvents: @escaping (AnyHashable, ConnectionEventOptions?) -> Effect<Never, Never> = { _, _ in
_unimplemented("registerForConnectionEvents")
},
supports: @escaping (CBCentralManager.Feature) -> Bool = { _ in
_unimplemented("supports")
}
) -> Self {
Self(
create: create,
destroy: destroy,
connect: connect,
cancelConnection: cancelConnection,
retrieveConnectedPeripherals: retrieveConnectedPeripherals,
retrievePeripherals: retrievePeripherals,
scanForPeripherals: scanForPeripherals,
stopScan: stopScan,
isScanning: isScanning,
registerForConnectionEvents: registerForConnectionEvents,
supports: supports
)
}
}
import Foundation
import CoreBluetooth
import ComposableArchitecture
public struct BluetoothManager {
var create: (AnyHashable, DispatchQueue?, InitializationOptions?) -> Effect<Action, Never>
var destroy: (AnyHashable) -> Effect<Never, Never>
var connect: (AnyHashable, Peripheral, ConnectionOptions?) -> Effect<Never, Never>
var cancelConnection: (AnyHashable, Peripheral) -> Effect<Never, Never>
var retrieveConnectedPeripherals: (AnyHashable, [CBUUID]) -> [Peripheral]
var retrievePeripherals: (AnyHashable, [UUID]) -> [Peripheral]
var scanForPeripherals: (AnyHashable, [CBUUID]?, ScanOptions?) -> Effect<Never, Never>
var stopScan: (AnyHashable) -> Effect<Never, Never>
var isScanning: (AnyHashable) -> Bool
var supports: (CBCentralManager.Feature) -> Bool
var registerForConnectionEvents: (AnyHashable, ConnectionEventOptions?) -> Effect<Never, Never>
public init(
create: @escaping (AnyHashable, DispatchQueue?, InitializationOptions?) -> Effect<Action, Never>,
destroy: @escaping (AnyHashable) -> Effect<Never, Never>,
connect: @escaping (AnyHashable, Peripheral, ConnectionOptions?) -> Effect<Never, Never>,
cancelConnection: @escaping (AnyHashable, Peripheral) -> Effect<Never, Never>,
retrieveConnectedPeripherals: @escaping (AnyHashable, [CBUUID]) -> [Peripheral],
retrievePeripherals: @escaping (AnyHashable, [UUID]) -> [Peripheral],
scanForPeripherals: @escaping (AnyHashable, [CBUUID]?, ScanOptions?) -> Effect<Never, Never>,
stopScan: @escaping (AnyHashable) -> Effect<Never, Never>,
isScanning: @escaping (AnyHashable) -> Bool,
registerForConnectionEvents: @escaping (AnyHashable, ConnectionEventOptions?) -> Effect<Never, Never>,
supports: @escaping (CBCentralManager.Feature) -> Bool
) {
self.create = create
self.destroy = destroy
self.connect = connect
self.cancelConnection = cancelConnection
self.retrieveConnectedPeripherals = retrieveConnectedPeripherals
self.retrievePeripherals = retrievePeripherals
self.scanForPeripherals = scanForPeripherals
self.stopScan = stopScan
self.isScanning = isScanning
self.supports = supports
self.registerForConnectionEvents = registerForConnectionEvents
}
}
extension BluetoothManager {
public func create(id: AnyHashable, queue: DispatchQueue? = nil, options: InitializationOptions? = nil) -> Effect<Action, Never> {
create(id, queue, options)
}
public func destroy(id: AnyHashable) -> Effect<Never, Never> {
destroy(id)
}
public func connect(id: AnyHashable, to peripheral: Peripheral, options: ConnectionOptions? = nil) -> Effect<Never, Never> {
connect(id, peripheral, options)
}
public func cancelConnection(id: AnyHashable, with peripheral: Peripheral) -> Effect<Never, Never> {
cancelConnection(id, peripheral)
}
public func retrieveConnectedPeripherals(id: AnyHashable, services: [CBUUID]) -> [Peripheral] {
retrieveConnectedPeripherals(id, services)
}
public func retrievePeripherals(id: AnyHashable, identifiers: [UUID]) -> [Peripheral] {
retrievePeripherals(id, identifiers)
}
public func scanForPeripherals(id: AnyHashable, services: [CBUUID]? = nil, options: ScanOptions? = nil) -> Effect<Never, Never> {
scanForPeripherals(id, services, options)
}
public func stopScan(id: AnyHashable) -> Effect<Never, Never> {
stopScan(id)
}
public func isScanning(id: AnyHashable) -> Bool {
isScanning(id)
}
public func supports(_ feature: CBCentralManager.Feature) -> Bool {
supports(feature)
}
public func registerForConnectionEvents(id: AnyHashable, options: ConnectionEventOptions? = nil) -> Effect<Never, Never> {
registerForConnectionEvents(id, options)
}
}
extension BluetoothManager {
public struct InitializationOptions {
let showPowerAlert: Bool?
let restoreIdentifier: String?
public init(showPowerAlert: Bool? = nil, restoreIdentifier: String? = nil) {
self.showPowerAlert = showPowerAlert
self.restoreIdentifier = restoreIdentifier
}
func toDictionary() -> [String: Any] {
var dictionary = [String: Any]()
if let showPowerAlert = showPowerAlert {
dictionary[CBCentralManagerOptionShowPowerAlertKey] = NSNumber(booleanLiteral: showPowerAlert)
}
if let restoreIdentifier = restoreIdentifier {
dictionary[CBCentralManagerOptionRestoreIdentifierKey] = restoreIdentifier as NSString
}
return dictionary
}
}
public struct ConnectionOptions {
let notifyOnConnection: Bool?
let notifyOnDisconnection: Bool?
let notifyOnNotification: Bool?
let enableTransportBridging: Bool?
let requiredANCS: Bool?
let startDelay: NSNumber?
public init(
notifyOnConnection: Bool? = nil,
notifyOnDisconnection: Bool? = nil,
notifyOnNotification: Bool? = nil,
enableTransportBridging: Bool? = nil,
requiredANCS: Bool? = nil,
startDelay: NSNumber? = nil
) {
self.notifyOnConnection = notifyOnConnection
self.notifyOnDisconnection = notifyOnDisconnection
self.notifyOnNotification = notifyOnNotification
self.enableTransportBridging = enableTransportBridging
self.requiredANCS = requiredANCS
self.startDelay = startDelay
}
func toDictionary() -> [String: Any] {
var dictionary = [String: Any]()
if let notifyOnConnection = notifyOnConnection {
dictionary[CBConnectPeripheralOptionNotifyOnConnectionKey] = NSNumber(booleanLiteral: notifyOnConnection)
}
if let notifyOnDisconnection = notifyOnDisconnection {
dictionary[CBConnectPeripheralOptionNotifyOnDisconnectionKey] = NSNumber(booleanLiteral: notifyOnDisconnection)
}
if let notifyOnNotification = notifyOnNotification {
dictionary[CBConnectPeripheralOptionNotifyOnNotificationKey] = NSNumber(booleanLiteral: notifyOnNotification)
}
if let enableTransportBridging = enableTransportBridging {
dictionary[CBConnectPeripheralOptionEnableTransportBridgingKey] = NSNumber(booleanLiteral: enableTransportBridging)
}
if let requiredANCS = requiredANCS {
dictionary[CBConnectPeripheralOptionRequiresANCS] = NSNumber(booleanLiteral: requiredANCS)
}
if let startDelay = startDelay {
dictionary[CBConnectPeripheralOptionStartDelayKey] = startDelay
}
return dictionary
}
}
public struct ScanOptions: Equatable {
let allowDuplicates: Bool?
let solicitedServiceUUIDs: [CBUUID]?
public init(allowDuplicates: Bool? = nil, solicitedServiceUUIDs: [CBUUID]? = nil) {
self.allowDuplicates = allowDuplicates
self.solicitedServiceUUIDs = solicitedServiceUUIDs
}
init(from dictionary: [String: Any]?) {
allowDuplicates = (dictionary?[CBCentralManagerScanOptionAllowDuplicatesKey] as? NSNumber)?.boolValue
solicitedServiceUUIDs = dictionary?[CBCentralManagerScanOptionSolicitedServiceUUIDsKey] as? [CBUUID]
}
func toDictionary() -> [String: Any] {
var dictionary = [String: Any]()
if let allowDuplicates = allowDuplicates {
dictionary[CBCentralManagerScanOptionAllowDuplicatesKey] = NSNumber(booleanLiteral: allowDuplicates)
}
if let solicitedServiceUUIDs = solicitedServiceUUIDs {
dictionary[CBCentralManagerScanOptionSolicitedServiceUUIDsKey] = solicitedServiceUUIDs as NSArray
}
return dictionary
}
}
public struct ConnectionEventOptions {
let peripheralUUIDs: [UUID]?
let serviceUUIDs: [CBUUID]?
public init(peripheralUUIDs: [UUID]? = nil, serviceUUIDs: [CBUUID]? = nil) {
self.peripheralUUIDs = peripheralUUIDs
self.serviceUUIDs = serviceUUIDs
}
func toDictionary() -> [CBConnectionEventMatchingOption : Any] {
var dictionary = [CBConnectionEventMatchingOption: Any]()
if let peripheralUUIDs = peripheralUUIDs {
dictionary[.peripheralUUIDs] = peripheralUUIDs as NSArray
}
if let serviceUUIDs = serviceUUIDs {
dictionary[.serviceUUIDs] = serviceUUIDs as NSArray
}
return dictionary
}
}
}
extension BluetoothManager {
public enum Action: Equatable {
case didConnect(Peripheral)
case didDisconnect(Peripheral, CBError?)
case didFailToConnect(Peripheral, CBError?)
case connectionEventDidOccur(CBConnectionEvent, Peripheral)
case didDiscover(Peripheral, AdvertismentData, NSNumber)
case willRestore(RestorationOptions)
case didUpdateANCSAuthorization(Peripheral)
case didUpdateState(CBManagerState)
case peripheral(Peripheral, Peripheral.Action)
}
}
extension BluetoothManager.Action {
public struct AdvertismentData: Equatable {
public let localName: String?
public let manufacturerData: Data?
public let serviceData: [CBUUID: Data]?
public let serviceUUIDs: [CBUUID]?
public let overflowServiceUUIDs: [CBUUID]?
public let solicitedServiceUUIDs: [CBUUID]?
public let txPowerLevel: NSNumber?
public let isConnectable: Bool?
init(from dictionary: [String: Any]) {
localName = dictionary[CBAdvertisementDataLocalNameKey] as? String
manufacturerData = dictionary[CBAdvertisementDataManufacturerDataKey] as? Data
txPowerLevel = dictionary[CBAdvertisementDataTxPowerLevelKey] as? NSNumber
isConnectable = (dictionary[CBAdvertisementDataIsConnectable] as? NSNumber)?.boolValue
serviceData = dictionary[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data]
serviceUUIDs = dictionary[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]
overflowServiceUUIDs = dictionary[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID]
solicitedServiceUUIDs = dictionary[CBAdvertisementDataSolicitedServiceUUIDsKey] as? [CBUUID]
}
}
public struct RestorationOptions: Equatable {
public let peripherals: [Peripheral]?
public let scannedServices: [CBUUID]?
public let scanOptions: BluetoothManager.ScanOptions?
init(from dictionary: [String: Any], subscriber: Effect<BluetoothManager.Action, Never>.Subscriber) {
peripherals = (dictionary[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral])?.map { peripheral in
Peripheral.live(from: peripheral, subscriber: subscriber)
}
scannedServices = dictionary[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID]
scanOptions = .init(from: dictionary[CBCentralManagerRestoredStateScanOptionsKey] as? [String: Any])
}
}
}
import CoreBluetooth
import ComposableArchitecture
import Combine
private func couldNotFindRawServiceValue() {
assertionFailure(
"""
The supplied service did not have a raw value. This is considered a programmer error. \
You should use the Service.init(from:) initializer.
"""
)
}
private func couldNotFindRawCharacteristicValue() {
assertionFailure(
"""
The supplied characteristic did not have a raw value. This is considered a programmer error. \
You should use the Characteristic.init(from:) initializer.
"""
)
}
private func couldNotFindRawDescriptorValue() {
assertionFailure(
"""
The supplied descriptor did not have a raw value. This is considered a programmer error. \
You should use the Descriptor.init(from:) initializer.
"""
)
}
extension Peripheral {
public static func live(from peripheral: CBPeripheral, subscriber: Effect<BluetoothManager.Action, Never>.Subscriber) -> Self {
Self(
rawValue: peripheral,
delegate: Delegate(subscriber),
identifier: { peripheral.identifier },
name: { peripheral.name },
services: { peripheral.services?.map(Service.init) },
discoverServices: { ids in
.fireAndForget { peripheral.discoverServices(ids) }
},
discoverIncludedServices: { ids, service in
guard let rawService = service.rawValue else {
couldNotFindRawServiceValue()
return .none
}
return .fireAndForget { peripheral.discoverIncludedServices(ids, for: rawService) }
},
discoverCharacteristics: { ids, service in
guard let rawService = service.rawValue else {
couldNotFindRawServiceValue()
return .none
}
return .fireAndForget { peripheral.discoverCharacteristics(ids, for: rawService) }
},
discoverDescriptors: { characteristic in
guard let rawCharacteristic = characteristic.rawValue else {
couldNotFindRawCharacteristicValue()
return .none
}
return .fireAndForget { peripheral.discoverDescriptors(for: rawCharacteristic) }
},
readCharacteristicValue: { characteristic in
guard let rawCharacteristic = characteristic.rawValue else {
couldNotFindRawCharacteristicValue()
return .none
}
return .fireAndForget { peripheral.readValue(for: rawCharacteristic) }
},
readDescriptorValue: { descriptor in
guard let rawDescriptor = descriptor.rawValue else {
couldNotFindRawDescriptorValue()
return .none
}
return .fireAndForget { peripheral.readValue(for: rawDescriptor) }
},
writeCharacteristicValue: { data, characteristic, writeType in
guard let rawCharacteristic = characteristic.rawValue else {
couldNotFindRawCharacteristicValue()
return .none
}
return .fireAndForget { peripheral.writeValue(data, for: rawCharacteristic, type: writeType) }
},
writeDescriptorValue: { data, descriptor in
guard let rawDescriptor = descriptor.rawValue else {
couldNotFindRawDescriptorValue()
return .none
}
return .fireAndForget { peripheral.writeValue(data, for: rawDescriptor) }
},
maximumWriteValueLength: peripheral.maximumWriteValueLength,
setNotifyValue: { value, characteristic in
guard let rawCharacteristic = characteristic.rawValue else {
couldNotFindRawCharacteristicValue()
return .none
}
return .fireAndForget { peripheral.setNotifyValue(value, for: rawCharacteristic) }
},
state: { peripheral.state },
canSendWriteWithoutResponse: { peripheral.canSendWriteWithoutResponse },
readRSSI: {
.fireAndForget { peripheral.readRSSI() }
},
openL2CAPChannel: { psm in
.fireAndForget { peripheral.openL2CAPChannel(psm) }
},
ancsAuthorized: { peripheral.ancsAuthorized }
)
}
class Delegate: NSObject, CBPeripheralDelegate {
let subscriber: Effect<BluetoothManager.Action, Never>.Subscriber
init(_ subscriber: Effect<BluetoothManager.Action, Never>.Subscriber) {
self.subscriber = subscriber
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didDiscoverServices(error as? CBError)
)
)
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverIncludedServicesFor service: CBService, error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didDiscoverIncludedServices(Service(from: service), error as? CBError)
)
)
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didDiscoverCharacteristics(Service(from: service), error as? CBError)
)
)
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didDiscoverDescriptors(Characteristic(from: characteristic), error as? CBError)
)
)
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didUpdateCharacteristicValue(Characteristic(from: characteristic), error as? CBError)
)
)
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didUpdateDescriptorValue(Descriptor(from: descriptor), error as? CBError)
)
)
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didWriteCharacteristicValue(Characteristic(from: characteristic), error as? CBError)
)
)
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didWriteDescriptorValue(Descriptor(from: descriptor), error as? CBError)
)
)
}
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.isReadyToSendWriteWithoutResponse
)
)
}
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didUpdateNotificationState(Characteristic(from: characteristic), error as? CBError)
)
)
}
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didReadRSSI(RSSI, error as? CBError)
)
)
}
func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didUpdateName
)
)
}
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didModifyServices(invalidatedServices.map(Service.init))
)
)
}
func peripheral(_ peripheral: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error?) {
subscriber.send(
.peripheral(
.live(from: peripheral, subscriber: subscriber),
.didOpenL2CAPChannel(channel, error as? CBError)
)
)
}
}
}
import CoreBluetooth
import ComposableArchitecture
extension Peripheral {
public static func mock(
identifier: @escaping () -> UUID = {
_unimplemented("identifier")
},
name: @escaping () -> String? = {
_unimplemented("name")
},
services: @escaping () -> [Service]? = {
_unimplemented("services")
},
discoverServices: @escaping ([CBUUID]?) -> Effect<Never, Never> = { _ in
_unimplemented("discoverServices")
},
discoverIncludedServices: @escaping ([CBUUID]?, Service) -> Effect<Never, Never> = { _, _ in
_unimplemented("discoverIncludedServices")
},
discoverCharacteristics: @escaping ([CBUUID]?, Service) -> Effect<Never, Never> = { _, _ in
_unimplemented("discoverCharacteristics")
},
discoverDescriptors: @escaping (Characteristic) -> Effect<Never, Never> = { _ in
_unimplemented("discoverDescriptors")
},
readCharacteristicValue: @escaping (Characteristic) -> Effect<Never, Never> = { _ in
_unimplemented("readCharacteristicValue")
},
readDescriptorValue: @escaping (Descriptor) -> Effect<Never, Never> = { _ in
_unimplemented("readDescriptorValue")
},
writeCharacteristicValue: @escaping (Data, Characteristic, CBCharacteristicWriteType) -> Effect<Never, Never> = { _, _, _ in
_unimplemented("writeCharacteristicValue")
},
writeDescriptorValue: @escaping (Data, Descriptor) -> Effect<Never, Never> = { _, _ in
_unimplemented("writeDescriptorValue")
},
maximumWriteValueLength: @escaping (CBCharacteristicWriteType) -> Int = { _ in
_unimplemented("maximumWriteValueLength")
},
setNotifyValue: @escaping (Bool, Characteristic) -> Effect<Never, Never> = { _, _ in
_unimplemented("setNotifyValue")
},
state: @escaping () -> CBPeripheralState = {
_unimplemented("state")
},
canSendWriteWithoutResponse: @escaping () -> Bool = {
_unimplemented("canSendWriteWithoutResponse")
},
readRSSI: @escaping () -> Effect<Never, Never> = {
_unimplemented("readRSSI")
},
openL2CAPChannel: @escaping (CBL2CAPPSM) -> Effect<Never, Never> = { _ in
_unimplemented("openL2CAPChannel")
},
ancsAuthorized: @escaping () -> Bool = {
_unimplemented("ancsAuthorized")
}
) -> Self {
Self(
rawValue: nil,
delegate: nil,
identifier: identifier,
name: name,
services: services,
discoverServices: discoverServices,
discoverIncludedServices: discoverIncludedServices,
discoverCharacteristics: discoverCharacteristics,
discoverDescriptors: discoverDescriptors,
readCharacteristicValue: readCharacteristicValue,
readDescriptorValue: readDescriptorValue,
writeCharacteristicValue: writeCharacteristicValue,
writeDescriptorValue: writeDescriptorValue,
maximumWriteValueLength: maximumWriteValueLength,
setNotifyValue: setNotifyValue,
state: state,
canSendWriteWithoutResponse: canSendWriteWithoutResponse,
readRSSI: readRSSI,
openL2CAPChannel: openL2CAPChannel,
ancsAuthorized: ancsAuthorized
)
}
}
import CoreBluetooth
import ComposableArchitecture
public struct Peripheral {
public var rawValue: CBPeripheral?
var delegate: CBPeripheralDelegate?
public var identifier: () -> UUID
public var name: () -> String?
public var services: () -> [Service]?
public var state: () -> CBPeripheralState
public var canSendWriteWithoutResponse: () -> Bool
public var readRSSI: () -> Effect<Never, Never>
public var ancsAuthorized: () -> Bool
var discoverServices: ([CBUUID]?) -> Effect<Never, Never>
var discoverIncludedServices: ([CBUUID]?, Service) -> Effect<Never, Never>
var discoverCharacteristics: ([CBUUID]?, Service) -> Effect<Never, Never>
var discoverDescriptors: (Characteristic) -> Effect<Never, Never>
var readCharacteristicValue: (Characteristic) -> Effect<Never, Never>
var readDescriptorValue: (Descriptor) -> Effect<Never, Never>
var writeCharacteristicValue: (Data, Characteristic, CBCharacteristicWriteType) -> Effect<Never, Never>
var writeDescriptorValue: (Data, Descriptor) -> Effect<Never, Never>
var maximumWriteValueLength: (CBCharacteristicWriteType) -> Int
var setNotifyValue: (Bool, Characteristic) -> Effect<Never, Never>
var openL2CAPChannel: (CBL2CAPPSM) -> Effect<Never, Never>
public init(
rawValue: CBPeripheral?,
delegate: CBPeripheralDelegate?,
identifier: @escaping () -> UUID,
name: @escaping () -> String?,
services: @escaping () -> [Service]?,
discoverServices: @escaping ([CBUUID]?) -> Effect<Never, Never>,
discoverIncludedServices: @escaping ([CBUUID]?, Service) -> Effect<Never, Never>,
discoverCharacteristics: @escaping ([CBUUID]?, Service) -> Effect<Never, Never>,
discoverDescriptors: @escaping (Characteristic) -> Effect<Never, Never>,
readCharacteristicValue: @escaping (Characteristic) -> Effect<Never, Never>,
readDescriptorValue: @escaping (Descriptor) -> Effect<Never, Never>,
writeCharacteristicValue: @escaping (Data, Characteristic, CBCharacteristicWriteType) -> Effect<Never, Never>,
writeDescriptorValue: @escaping (Data, Descriptor) -> Effect<Never, Never>,
maximumWriteValueLength: @escaping (CBCharacteristicWriteType) -> Int,
setNotifyValue: @escaping (Bool, Characteristic) -> Effect<Never, Never>,
state: @escaping () -> CBPeripheralState,
canSendWriteWithoutResponse: @escaping () -> Bool,
readRSSI: @escaping () -> Effect<Never, Never>,
openL2CAPChannel: @escaping (CBL2CAPPSM) -> Effect<Never, Never>,
ancsAuthorized: @escaping () -> Bool
) {
self.rawValue = rawValue
self.delegate = delegate
self.rawValue?.delegate = delegate
self.identifier = identifier
self.name = name
self.services = services
self.discoverServices = discoverServices
self.discoverIncludedServices = discoverIncludedServices
self.discoverCharacteristics = discoverCharacteristics
self.discoverDescriptors = discoverDescriptors
self.readCharacteristicValue = readCharacteristicValue
self.readDescriptorValue = readDescriptorValue
self.writeCharacteristicValue = writeCharacteristicValue
self.writeDescriptorValue = writeDescriptorValue
self.maximumWriteValueLength = maximumWriteValueLength
self.setNotifyValue = setNotifyValue
self.state = state
self.canSendWriteWithoutResponse = canSendWriteWithoutResponse
self.readRSSI = readRSSI
self.openL2CAPChannel = openL2CAPChannel
self.ancsAuthorized = ancsAuthorized
}
}
extension Peripheral: Equatable {
public static func == (lhs: Peripheral, rhs: Peripheral) -> Bool {
return lhs.rawValue == rhs.rawValue
}
}
extension Peripheral {
public func discoverServices(_ uuids: [CBUUID]? = nil) -> Effect<Never, Never> {
discoverServices(uuids)
}
public func discoverIncludedServices(_ uuids: [CBUUID]? = nil, for service: Service) -> Effect<Never, Never> {
discoverIncludedServices(uuids, service)
}
public func discoverCharacteristics(_ uuids: [CBUUID]? = nil, for service: Service) -> Effect<Never, Never> {
discoverCharacteristics(uuids, service)
}
public func discoverDescriptors(for characteristic: Characteristic) -> Effect<Never, Never> {
discoverDescriptors(characteristic)
}
public func readValue(for characteristic: Characteristic) -> Effect<Never, Never> {
readCharacteristicValue(characteristic)
}
public func readValue(for descriptor: Descriptor) -> Effect<Never, Never> {
readDescriptorValue(descriptor)
}
public func writeValue(_ data: Data, for characteristic: Characteristic, type: CBCharacteristicWriteType) -> Effect<Never, Never> {
writeCharacteristicValue(data, characteristic, type)
}
public func writeValue(_ data: Data, for descriptor: Descriptor) -> Effect<Never, Never> {
writeDescriptorValue(data, descriptor)
}
public func maximumWriteValueLength(for writeType: CBCharacteristicWriteType) -> Int {
maximumWriteValueLength(writeType)
}
public func setNotifyValue(_ enabled: Bool, for characteristic: Characteristic) -> Effect<Never, Never> {
setNotifyValue(enabled, characteristic)
}
public func openL2CAPChannel(_ psm: CBL2CAPPSM) -> Effect<Never, Never> {
openL2CAPChannel(psm)
}
}
extension Peripheral {
public enum Action: Equatable {
case didDiscoverServices(CBError?)
case didDiscoverIncludedServices(Service, CBError?)
case didDiscoverCharacteristics(Service, CBError?)
case didDiscoverDescriptors(Characteristic, CBError?)
case didUpdateCharacteristicValue(Characteristic, CBError?)
case didUpdateDescriptorValue(Descriptor, CBError?)
case didWriteCharacteristicValue(Characteristic, CBError?)
case didWriteDescriptorValue(Descriptor, CBError?)
case isReadyToSendWriteWithoutResponse
case didUpdateNotificationState(Characteristic, CBError?)
case didReadRSSI(NSNumber, CBError?)
case didUpdateName
case didModifyServices([Service])
case didOpenL2CAPChannel(CBL2CAPChannel?, CBError?)
}
}
import Foundation
import CoreBluetooth
public struct Service: Equatable {
let rawValue: CBService?
public let identifier: CBUUID
public let isPrimary: Bool
public let characteristics: [Characteristic]?
public let includedServices: [Service]?
init(from service: CBService) {
rawValue = service
identifier = service.uuid
isPrimary = service.isPrimary
characteristics = service.characteristics?.map(Characteristic.init)
includedServices = service.includedServices?.map(Service.init)
}
init(
identifier: CBUUID,
isPrimary: Bool,
characteristics: [Characteristic]?,
includedServices: [Service]?
) {
rawValue = nil
self.identifier = identifier
self.isPrimary = isPrimary
self.characteristics = characteristics
self.includedServices = includedServices
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment