$npx -y skills add dpearson2699/swift-ios-skills --skill core-bluetoothBuild 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 l
| 1 | # Core Bluetooth |
| 2 | |
| 3 | Scan for, connect to, and exchange data with Bluetooth Low Energy (BLE) devices. |
| 4 | Covers the central role (scanning and connecting to peripherals), the peripheral |
| 5 | role (advertising services), background modes, and state restoration. |
| 6 | Use `accessorysetupkit` for privacy-preserving accessory discovery and setup; |
| 7 | use this skill for direct Core Bluetooth GATT communication. |
| 8 | |
| 9 | ## Contents |
| 10 | |
| 11 | - [Setup](#setup) |
| 12 | - [Central Role: Scanning](#central-role-scanning) |
| 13 | - [Central Role: Connecting](#central-role-connecting) |
| 14 | - [Discovering Services and Characteristics](#discovering-services-and-characteristics) |
| 15 | - [Reading, Writing, and Notifications](#reading-writing-and-notifications) |
| 16 | - [Peripheral Role: Advertising](#peripheral-role-advertising) |
| 17 | - [Background BLE](#background-ble) |
| 18 | - [State Restoration](#state-restoration) |
| 19 | - [Common Mistakes](#common-mistakes) |
| 20 | - [Review Checklist](#review-checklist) |
| 21 | - [References](#references) |
| 22 | |
| 23 | ## Setup |
| 24 | |
| 25 | ### Info.plist Keys |
| 26 | |
| 27 | | Key | Purpose | |
| 28 | |---|---| |
| 29 | | `NSBluetoothAlwaysUsageDescription` | Required. Explains why the app uses Bluetooth | |
| 30 | | `UIBackgroundModes` with `bluetooth-central` | Background scanning and connecting | |
| 31 | | `UIBackgroundModes` with `bluetooth-peripheral` | Background advertising | |
| 32 | |
| 33 | ### Bluetooth Authorization |
| 34 | |
| 35 | Core Bluetooth has no explicit permission request API. Add |
| 36 | `NSBluetoothAlwaysUsageDescription`, create the manager when the app is ready for |
| 37 | Bluetooth access, then check `manager.authorization` and `manager.state`. |
| 38 | Treat `.denied` and `.restricted` as terminal until the user changes Settings; |
| 39 | wait for `.poweredOn` before scanning, connecting, advertising, or publishing |
| 40 | services. |
| 41 | |
| 42 | ## Central Role: Scanning |
| 43 | |
| 44 | ### Creating the Central Manager |
| 45 | |
| 46 | Always wait for the `poweredOn` state before scanning. |
| 47 | |
| 48 | ```swift |
| 49 | import CoreBluetooth |
| 50 | |
| 51 | final class BluetoothManager: NSObject, CBCentralManagerDelegate { |
| 52 | private var centralManager: CBCentralManager! |
| 53 | private var discoveredPeripheral: CBPeripheral? |
| 54 | |
| 55 | override init() { |
| 56 | super.init() |
| 57 | centralManager = CBCentralManager(delegate: self, queue: nil) |
| 58 | } |
| 59 | |
| 60 | func centralManagerDidUpdateState(_ central: CBCentralManager) { |
| 61 | guard central.state == .poweredOn else { return } |
| 62 | startScanning() |
| 63 | } |
| 64 | } |
| 65 | ``` |
| 66 | |
| 67 | ### Scanning for Peripherals |
| 68 | |
| 69 | Scan for specific service UUIDs to save power. Pass `nil` to discover all |
| 70 | peripherals (not recommended in production). |
| 71 | |
| 72 | ```swift |
| 73 | let heartRateServiceUUID = CBUUID(string: "180D") |
| 74 | |
| 75 | func startScanning() { |
| 76 | centralManager.scanForPeripherals( |
| 77 | withServices: [heartRateServiceUUID], |
| 78 | options: [CBCentralManagerScanOptionAllowDuplicatesKey: false] |
| 79 | ) |
| 80 | } |
| 81 | |
| 82 | func centralManager( |
| 83 | _ central: CBCentralManager, |
| 84 | didDiscover peripheral: CBPeripheral, |
| 85 | advertisementData: [String: Any], |
| 86 | rssi RSSI: NSNumber |
| 87 | ) { |
| 88 | guard RSSI.intValue > -70 else { return } // Filter weak signals |
| 89 | |
| 90 | // IMPORTANT: Retain the peripheral -- it will be deallocated otherwise |
| 91 | discoveredPeripheral = peripheral |
| 92 | centralManager.stopScan() |
| 93 | centralManager.connect(peripheral, options: nil) |
| 94 | } |
| 95 | ``` |
| 96 | |
| 97 | ## Central Role: Connecting |
| 98 | |
| 99 | ```swift |
| 100 | func centralManager( |
| 101 | _ central: CBCentralManager, |
| 102 | didConnect peripheral: CBPeripheral |
| 103 | ) { |
| 104 | peripheral.delegate = self |
| 105 | peripheral.discoverServices([heartRateServiceUUID]) |
| 106 | } |
| 107 | |
| 108 | func centralManager( |
| 109 | _ central: CBCentralManager, |
| 110 | didDisconnectPeripheral peripheral: CBPeripheral, |
| 111 | timestamp: CFAbsoluteTime, |
| 112 | isReconnecting: Bool, |
| 113 | error: Error? |
| 114 | ) { |
| 115 | if isReconnecting { |
| 116 | // System is automatically reconnecting |
| 117 | return |
| 118 | } |
| 119 | // Handle disconnection -- optionally reconnect |
| 120 | discoveredPeripheral = nil |
| 121 | } |
| 122 | ``` |
| 123 | |
| 124 | ## Discovering Services and Characteristics |
| 125 | |
| 126 | Implement `CBPeripheralDelegate` to walk the service/characteristic tree. |
| 127 | |
| 128 | ```swift |
| 129 | extension BluetoothManager: CBPeripheralDelegate { |
| 130 | func peripheral( |
| 131 | _ peripheral: CBPeripheral, |
| 132 | didDiscoverServices error: Error? |
| 133 | ) { |
| 134 | guard let services = peripheral.services else { return } |
| 135 | for service in services { |
| 136 | peripheral.discoverCharacteristics(nil, for: service) |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | func peripheral( |
| 141 | _ peripheral: CBPeripheral, |
| 142 | didDiscoverCharacteristicsFor service: CBService, |
| 143 | error: Error? |
| 144 | ) { |
| 145 | guard let characteristics = service.characteristics else { return |