Created
June 9, 2022 17:04
-
-
Save fdenisnascimento/8b9d9d606532faf25e813d7beb0943b7 to your computer and use it in GitHub Desktop.
ComposableArchitecture wrapper for CoreBluetooth
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
) | |
) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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?) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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