Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build direct Bluetooth Low Energy workflows with Core Bluetooth. Use when implementing BLE central or peripheral GATT communication, scanning or connecting with CBCentralManager, discovering services and characteristics, reading/writing/subscribing with CBPeripheral, publishing local services with CBPeripheralManager, handling Bluetooth authorization, background BLE modes, state restoration, write flow control, or CBUUID-based workflows. For privacy-preserving accessory setup/picker flows, use a
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-14 | ✗→✓ | ▲ Improved | 580% | 0% |
| case-20 | ✓→✗ | ▼ Worse | 554% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 817% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 399% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 222% | 0% |
Scan for, connect to, and exchange data with Bluetooth Low Energy (BLE) devices. Covers the central role (scanning and connecting to peripherals), the peripheral role (advertising services), background modes, and state restoration. Use accessorysetupkit for privacy-preserving accessory discovery and setup; use this skill for direct Core Bluetooth GATT communication.
| Key | Purpose | |---|---| | NSBluetoothAlwaysUsageDescription | Required. Explains why the app uses Bluetooth | | UIBackgroundModes with bluetooth-central | Background scanning and connecting | | UIBackgroundModes with bluetooth-peripheral | Background advertising |
Core Bluetooth has no explicit permission request API. Add NSBluetoothAlwaysUsageDescription, create the manager when the app is ready for Bluetooth access, then check manager.authorization and manager.state. Treat .denied and .restricted as terminal until the user changes Settings; wait for .poweredOn before scanning, connecting, advertising, or publishing services.
Always wait for the poweredOn state before scanning.
swiftimport CoreBluetooth final class BluetoothManager: NSObject, CBCentralManagerDelegate { private var centralManager: CBCentralManager! private var discoveredPeripheral: CBPeripheral? override init() { super.init() centralManager = CBCentralManager(delegate: self, queue: nil) } func centralManagerDidUpdateState(_ central: CBCentralManager) { guard central.state == .poweredOn else { return } startScanning() } }
Scan for specific service UUIDs to save power. Pass nil to discover all peripherals (not recommended in production).
swiftlet heartRateServiceUUID = CBUUID(string: "180D") func startScanning() { centralManager.scanForPeripherals( withServices: [heartRateServiceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false] ) } func centralManager( _ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber ) { guard RSSI.intValue > -70 else { return } // Filter weak signals // IMPORTANT: Retain the peripheral -- it will be deallocated otherwise discoveredPeripheral = peripheral centralManager.stopScan() centralManager.connect(peripheral, options: nil) }
swiftfunc centralManager( _ central: CBCentralManager, didConnect peripheral: CBPeripheral ) { peripheral.delegate = self peripheral.discoverServices([heartRateServiceUUID]) } func centralManager( _ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, timestamp: CFAbsoluteTime, isReconnecting: Bool, error: Error? ) { if isReconnecting { // System is automatically reconnecting return } // Handle disconnection -- optionally reconnect discoveredPeripheral = nil }
Implement CBPeripheralDelegate to walk the service/characteristic tree.
swiftextension BluetoothManager: CBPeripheralDelegate { func peripheral( _ peripheral: CBPeripheral, didDiscoverServices error: Error? ) { guard let services = peripheral.services else { return } for service in services { peripheral.discoverCharacteristics(nil, for: service) } } func peripheral( _ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error? ) { guard let characteristics = service.characteristics else { return } for characteristic in characteristics { if characteristic.properties.contains(.notify) { peripheral.setNotifyValue(true, for: characteristic) } if characteristic.properties.contains(.read) { peripheral.readValue(for: characteristic) } } } }
swiftfunc peripheral( _ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error? ) { guard let data = characteristic.value else { return } switch characteristic.uuid { case CBUUID(string: "2A37"): if let heartRate = parseHeartRate(data) { print("Heart rate: \(heartRate) bpm") } case CBUUID(string: "2A19"): let batteryLevel = data.first.map { Int($0) } ?? 0 print("Battery: \(batteryLevel)%") default: break } } private func parseHeartRate(_ data: Data) -> Int? { guard data.count >= 2 else { return nil } let flags = data[0] let is16Bit = (flags & 0x01) != 0 if is16Bit { guard data.count >= 3 else { return nil } return Int(data[1]) | (Int(data[2]) << 8) } else { return Int(data[1]) } }
swiftfunc writeValue(_ data: Data, to characteristic: CBCharacteristic, on peripheral: CBPeripheral, preferResponse: Bool = true) { let type: CBCharacteristicWriteType if preferResponse, characteristic.properties.contains(.write) { type = .withResponse } else if characteristic.properties.contains(.writeWithoutResponse), peripheral.canSendWriteWithoutResponse { type = .withoutResponse } else if characteristic.properties.contains(.write) { type = .withResponse } else { return } guard data.count <= peripheral.maximumWriteValueLength(for: type) else { return } peripheral.writeValue(data, for: characteristic, type: type) } // Confirmation callback for .withResponse writes. func peripheral( _ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error? ) { if let error { print("Write failed: \(error.localizedDescription)") } } // Resume queued .withoutResponse writes here. func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {}
swift// Subscribe peripheral.setNotifyValue(true, for: characteristic) // Unsubscribe peripheral.setNotifyValue(false, for: characteristic) // Confirmation func peripheral( _ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error? ) { if characteristic.isNotifying { print("Now receiving notifications for \(characteristic.uuid)") } }
Publish services from the local device using CBPeripheralManager.
swiftfinal class BLEPeripheralManager: NSObject, CBPeripheralManagerDelegate { private var peripheralManager: CBPeripheralManager! private let serviceUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABC") private let charUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABD") override init() { super.init() peripheralManager = CBPeripheralManager(delegate: self, queue: nil) } func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { guard peripheral.state == .poweredOn else { return } setupService() } private func setupService() { let characteristic = CBMutableCharacteristic( type: charUUID, properties: [.read, .notify], value: nil, permissions: [.readable] ) let service = CBMutableService(type: serviceUUID, primary: true) service.characteristics = [characteristic] peripheralManager.add(service) } func peripheralManager( _ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error? ) { guard error == nil else { return } peripheralManager.startAdvertising([ CBAdvertisementDataServiceUUIDsKey: [serviceUUID], CBAdvertisementDataLocalNameKey: "MyDevice" ]) } }
Add bluetooth-central to UIBackgroundModes. In the background:
nil scans are foreground-onlyCBCentralManagerScanOptionAllowDuplicatesKey, have no effectAdd bluetooth-peripheral to UIBackgroundModes. In the background:
State restoration allows the system to re-create your central or peripheral manager after your app is terminated and relaunched for a BLE event.
swift// 1. Create with a restoration identifier centralManager = CBCentralManager( delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: "myCentral"] ) // 2. Implement the restoration delegate method func centralManager( _ central: CBCentralManager, willRestoreState dict: [String: Any] ) { if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { for peripheral in peripherals { // Re-assign delegate and retain peripheral.delegate = self discoveredPeripheral = peripheral } } let restoredServices = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] let restoredOptions = dict[CBCentralManagerRestoredStateScanOptionsKey] as? [String: Any] // Resume scanning with restoredServices/restoredOptions if still needed. }
swiftperipheralManager = CBPeripheralManager( delegate: self, queue: nil, options: [CBPeripheralManagerOptionRestoreIdentifierKey: "myPeripheral"] ) func peripheralManager( _ peripheral: CBPeripheralManager, willRestoreState dict: [String: Any] ) { let services = dict[CBPeripheralManagerRestoredStateServicesKey] as? [CBMutableService] let advertisement = dict[CBPeripheralManagerRestoredStateAdvertisementDataKey] as? [String: Any] // Reconnect app state to restored services/advertisement as needed. }
| Mistake | Fix | |---|---| | Scan/connect before .poweredOn | Start BLE work from centralManagerDidUpdateState. | | Discovered peripheral is not retained | Hold a strong reference through connection and discovery. | | Production scan passes nil services | Filter by the service UUIDs the feature needs. | | Service discovery begins before didConnect | Advance only from delegate callbacks and handle failure/disconnect paths. | | Writes ignore characteristic properties or payload limits | Select the supported write type, respect maximumWriteValueLength, and gate .withoutResponse on canSendWriteWithoutResponse. |
NSBluetoothAlwaysUsageDescription added to Info.plistcentralManagerDidUpdateState returning .poweredOnnil) in productionCBPeripheralDelegate set before calling discoverServicesmaximumWriteValueLength(for:).withoutResponse writes honor canSendWriteWithoutResponsebluetooth-central or bluetooth-peripheral) added if neededwillRestoreState delegate method implemented when using state restoration.withResponse vs .withoutResponse)Other measured skills in the registry, with their headline benchmark lift.