
Core Bluetooth
- 2.6k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
core-bluetooth is a Swift iOS skill for BLE central and peripheral GATT communication with Core Bluetooth managers.
About
Core Bluetooth covers scanning, connecting, and exchanging data over Bluetooth Low Energy in Swift 6.3 on iOS 26+. Central role workflows use CBCentralManager to scan for service UUIDs, connect to CBPeripheral devices, discover services and characteristics, and read, write, or subscribe to notifications. Peripheral role workflows publish local GATT services with CBPeripheralManager advertising. Setup requires NSBluetoothAlwaysUsageDescription and optional UIBackgroundModes for bluetooth-central or bluetooth-peripheral. Authorization has no explicit prompt API; apps check manager.authorization and wait for poweredOn before scanning or advertising. Background BLE and state restoration patterns preserve connections across app suspends. Write flow control, CBUUID workflows, and common mistakes like scanning with nil service filters in production are documented. Scope directs privacy-preserving accessory setup to accessorysetupkit first, returning here for post-setup GATT communication. Review checklists verify Info.plist keys, authorization handling, and background mode entitlements before App Review.
- Central role scan, connect, discover, read, write, and notify patterns.
- Peripheral role advertising and local GATT service publishing.
- NSBluetoothAlwaysUsageDescription and background mode setup.
- Authorization via manager.authorization without explicit permission API.
- State restoration and background BLE connection preservation.
Core Bluetooth by the numbers
- 2,646 all-time installs (skills.sh)
- +117 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #72 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
core-bluetooth capabilities & compatibility
- Capabilities
- central manager scanning and connection lifecycl · service and characteristic discovery with read w · peripheral manager advertising and publishing · background ble and state restoration patterns · authorization and poweredon state gating
- Use cases
- api development · frontend
- Platforms
- macOS
- Runs
- Runs locally
- Pricing
- Free
What core-bluetooth says it does
Core Bluetooth has no explicit permission request API.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill core-bluetoothAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.6k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I scan, connect, and exchange BLE characteristic data with correct authorization and background modes?
Implement BLE central and peripheral GATT workflows with CBCentralManager, CBPeripheral, and background modes in Swift.
Who is it for?
iOS developers implementing heart rate monitors, sensors, or custom BLE peripherals in Swift.
Skip if: Skip for initial privacy-preserving accessory picker flows owned by accessorysetupkit.
When should I use this skill?
User implements CBCentralManager scanning, CBPeripheral characteristics, or BLE background modes.
What you get
Working central or peripheral BLE flows with poweredOn gating, GATT discovery, and background restoration.
- CBCentralManager implementation
- Characteristic subscription handlers
- Heart-rate BPM parser
By the numbers
- Reference eval uses GATT service UUID 180D and characteristic 2A37
- Skill includes eval-driven safety checks for scan, connect, subscribe, and write flows
Files
Core Bluetooth
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. Targets Swift 6.3 / iOS 26+. Use accessorysetupkit for privacy-preserving accessory discovery and setup; use this skill for direct Core Bluetooth GATT communication.
Contents
- Setup
- Central Role: Scanning
- Central Role: Connecting
- Discovering Services and Characteristics
- Reading, Writing, and Notifications
- Peripheral Role: Advertising
- Background BLE
- State Restoration
- Common Mistakes
- Review Checklist
- References
Setup
Info.plist Keys
| Key | Purpose |
|---|---|
NSBluetoothAlwaysUsageDescription | Required. Explains why the app uses Bluetooth |
UIBackgroundModes with bluetooth-central | Background scanning and connecting |
UIBackgroundModes with bluetooth-peripheral | Background advertising |
Bluetooth Authorization
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.
Central Role: Scanning
Creating the Central Manager
Always wait for the poweredOn state before scanning.
import 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()
}
}Scanning for Peripherals
Scan for specific service UUIDs to save power. Pass nil to discover all peripherals (not recommended in production).
let 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)
}Central Role: Connecting
func 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
}Discovering Services and Characteristics
Implement CBPeripheralDelegate to walk the service/characteristic tree.
extension 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)
}
}
}
}Reading, Writing, and Notifications
Reading a Value
func 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])
}
}Writing a Value
func 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) {}Subscribing to Notifications
// 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)")
}
}Peripheral Role: Advertising
Publish services from the local device using CBPeripheralManager.
final 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"
])
}
}Background BLE
Background Central Mode
Add bluetooth-central to UIBackgroundModes. In the background:
- Scanning must specify one or more service UUIDs;
nilscans are foreground-only - Scan options, including
CBCentralManagerScanOptionAllowDuplicatesKey, have no effect
Background Peripheral Mode
Add bluetooth-peripheral to UIBackgroundModes. In the background:
- Without this mode, published service contents are disabled while suspended
- The local name is not advertised
- Service UUIDs move to the overflow area and require explicit service scans
State Restoration
State restoration allows the system to re-create your central or peripheral manager after your app is terminated and relaunched for a BLE event.
Central Manager State Restoration
// 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.
}Peripheral Manager State Restoration
peripheralManager = 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.
}Common Mistakes
DON'T: Scan or connect before poweredOn
// WRONG: Scanning immediately -- manager may not be ready
let manager = CBCentralManager(delegate: self, queue: nil)
manager.scanForPeripherals(withServices: nil) // May silently fail
// CORRECT: Wait for poweredOn in the delegate
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
central.scanForPeripherals(withServices: [serviceUUID])
}
}DON'T: Lose the peripheral reference
Core Bluetooth does not retain discovered peripherals. If you don't hold a strong reference, the peripheral is deallocated and the connection fails silently.
// WRONG: No strong reference kept
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral, ...) {
central.connect(peripheral) // peripheral may be deallocated
}
// CORRECT: Retain the peripheral
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral, ...) {
self.discoveredPeripheral = peripheral // Strong reference
central.connect(peripheral)
}DON'T: Scan for nil services in production
// WRONG: Discovers every BLE device in range -- drains battery
centralManager.scanForPeripherals(withServices: nil)
// CORRECT: Specify the service UUIDs you need
centralManager.scanForPeripherals(withServices: [targetServiceUUID])DON'T: Assume connection order or timing
// WRONG: Assuming immediate connection
centralManager.connect(peripheral)
discoverServicesNow() // Peripheral not connected yet
// CORRECT: Discover services in the didConnect callback
func centralManager(_ central: CBCentralManager,
didConnect peripheral: CBPeripheral) {
peripheral.delegate = self
peripheral.discoverServices([serviceUUID])
}DON'T: Write without checking properties and flow control
// WRONG: May fail, report an error, or provide no confirmation
peripheral.writeValue(data, for: characteristic, type: .withResponse)
// CORRECT: Check properties, length, and .withoutResponse flow control
if characteristic.properties.contains(.write),
data.count <= peripheral.maximumWriteValueLength(for: .withResponse) {
peripheral.writeValue(data, for: characteristic, type: .withResponse)
} else if characteristic.properties.contains(.writeWithoutResponse),
peripheral.canSendWriteWithoutResponse,
data.count <= peripheral.maximumWriteValueLength(for: .withoutResponse) {
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
}Review Checklist
- [ ]
NSBluetoothAlwaysUsageDescriptionadded to Info.plist - [ ] All BLE operations gated on
centralManagerDidUpdateStatereturning.poweredOn - [ ] Discovered peripherals retained with a strong reference
- [ ] Scanning uses specific service UUIDs (not
nil) in production - [ ]
CBPeripheralDelegateset before callingdiscoverServices - [ ] Characteristic properties checked before read/write/notify
- [ ] Write payloads stay within
maximumWriteValueLength(for:) - [ ]
.withoutResponsewrites honorcanSendWriteWithoutResponse - [ ] Background mode (
bluetooth-centralorbluetooth-peripheral) added if needed - [ ] State restoration identifier set if app needs relaunch-on-BLE-event support
- [ ]
willRestoreStatedelegate method implemented when using state restoration - [ ] Scanning stopped after discovering the target peripheral
- [ ] Disconnection handled with optional automatic reconnect logic
- [ ] Write type matches characteristic properties (
.withResponsevs.withoutResponse)
References
- Extended patterns (reconnection strategies, data parsing, SwiftUI integration): references/ble-patterns.md
- Core Bluetooth framework
- CBCentralManager
- CBPeripheral
- CBPeripheralManager
- CBService
- CBCharacteristic
- CBUUID
- CBCentralManagerDelegate
- CBPeripheralDelegate
- NSBluetoothAlwaysUsageDescription
- CBManagerAuthorization
- scanForPeripherals(withServices:options:))
- startAdvertising(_:))
- writeValue(_:for:type:))
- maximumWriteValueLength(for:))
- canSendWriteWithoutResponse
- Configuring background execution modes
{
"skill_name": "core-bluetooth",
"evals": [
{
"id": 1,
"prompt": "I'm building an iOS heart-rate monitor screen that scans for service 180D, connects, subscribes to 2A37, parses the BPM, and sends a small command to a writable characteristic. Show the Core Bluetooth manager code and the safety checks I should include.",
"expected_output": "Guidance creates a CBCentralManager, waits for poweredOn, checks Bluetooth authorization, scans with service UUID 180D, retains the discovered peripheral, sets the peripheral delegate before discovery, subscribes to notify characteristics, parses heart-rate data with byte-count guards, and chooses write types using characteristic properties plus maximumWriteValueLength and canSendWriteWithoutResponse.",
"files": [],
"assertions": [
"The output checks Bluetooth authorization and gates BLE operations on the manager reaching poweredOn.",
"The output scans for service UUID 180D rather than using a nil production scan.",
"The output retains the discovered CBPeripheral and sets its delegate before service discovery.",
"The output parses heart-rate measurement data with explicit guards for the flags byte and 16-bit BPM payload length.",
"The output checks characteristic write properties, maximumWriteValueLength(for:), and canSendWriteWithoutResponse before using writeValue."
]
},
{
"id": 2,
"prompt": "Review this BLE background plan: scan for all peripherals with nil services while in the background, rely on allowDuplicates for live RSSI, advertise a local name from the phone, skip bluetooth-peripheral because the service was added before suspension, and restore only the peripheral identifier after relaunch.",
"expected_output": "Review flags each background/state-restoration issue. It explains that background central scans require explicit service UUIDs and scan options have no effect; background peripheral advertising omits the local name and puts service UUIDs in overflow; published service contents are disabled without bluetooth-peripheral; and restoration should use Core Bluetooth restoration identifiers plus restored state dictionaries for peripherals, scan services/options, services, and advertisement data.",
"files": [],
"assertions": [
"The output rejects nil-service background scanning and requires explicit service UUIDs for bluetooth-central background scans.",
"The output states scan options such as allowDuplicates have no effect while scanning in the background.",
"The output states background peripheral advertising does not include the local name and service UUIDs are discoverable only through explicit service scans.",
"The output states published service contents are disabled in the background without bluetooth-peripheral mode.",
"The output recommends Core Bluetooth restoration identifiers and restored state dictionary keys instead of restoring only an app-stored peripheral identifier."
]
},
{
"id": 3,
"prompt": "We need a privacy-preserving first-run picker for a BLE thermostat, then after the user picks it we need to read and write GATT characteristics. Should this be handled entirely by Core Bluetooth, entirely by AccessorySetupKit, or split? Give the implementation boundary.",
"expected_output": "The answer splits responsibility: AccessorySetupKit owns the privacy-preserving picker/discovery/authorization step, and Core Bluetooth owns post-setup GATT communication using the selected accessory identifier. It avoids broadening Core Bluetooth into picker setup and avoids using AccessorySetupKit for characteristic read/write logic.",
"files": [],
"assertions": [
"The output routes the first-run privacy-preserving accessory picker to AccessorySetupKit.",
"The output routes post-selection GATT reads, writes, notifications, services, and characteristics to Core Bluetooth.",
"The output does not recommend broad direct Core Bluetooth scanning for the initial privacy-preserving picker use case.",
"The output does not expand AccessorySetupKit into characteristic read/write implementation.",
"The output describes the handoff from selected accessory identity to Core Bluetooth communication."
]
}
]
}
Core Bluetooth Extended Patterns
Overflow reference for the core-bluetooth skill. Contains advanced patterns that exceed the main skill file's scope.
Contents
- SwiftUI BLE Integration
- Reconnection Strategies
- Data Parsing Helpers
- Write Flow Control
- Multiple Peripheral Management
- L2CAP Channels
- Peripheral Role: Responding to Requests
SwiftUI BLE Integration
Observable Bluetooth Manager
import CoreBluetooth
import SwiftUI
@Observable
@MainActor
final class BLEViewModel: NSObject {
private var centralManager: CBCentralManager!
private var connectedPeripheral: CBPeripheral?
var isBluetoothOn = false
var isScanning = false
var isConnected = false
var discoveredDevices: [DiscoveredDevice] = []
var heartRate: Int = 0
struct DiscoveredDevice: Identifiable {
let id: UUID
let name: String
let rssi: Int
let peripheral: CBPeripheral
}
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
}
func startScan() {
guard isBluetoothOn else { return }
discoveredDevices.removeAll()
isScanning = true
centralManager.scanForPeripherals(
withServices: [CBUUID(string: "180D")],
options: nil
)
}
func stopScan() {
centralManager.stopScan()
isScanning = false
}
func connect(to device: DiscoveredDevice) {
stopScan()
connectedPeripheral = device.peripheral
centralManager.connect(device.peripheral)
}
func disconnect() {
guard let peripheral = connectedPeripheral else { return }
centralManager.cancelPeripheralConnection(peripheral)
}
}
extension BLEViewModel: CBCentralManagerDelegate {
nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) {
Task { @MainActor in
isBluetoothOn = central.state == .poweredOn
}
}
nonisolated func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String: Any],
rssi RSSI: NSNumber
) {
Task { @MainActor in
let name = peripheral.name ?? "Unknown"
let device = DiscoveredDevice(
id: peripheral.identifier,
name: name,
rssi: RSSI.intValue,
peripheral: peripheral
)
if !discoveredDevices.contains(where: { $0.id == device.id }) {
discoveredDevices.append(device)
}
}
}
nonisolated func centralManager(
_ central: CBCentralManager,
didConnect peripheral: CBPeripheral
) {
Task { @MainActor in
isConnected = true
peripheral.delegate = self
peripheral.discoverServices([CBUUID(string: "180D")])
}
}
nonisolated func centralManager(
_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
timestamp: CFAbsoluteTime,
isReconnecting: Bool,
error: Error?
) {
Task { @MainActor in
isConnected = false
connectedPeripheral = nil
}
}
}
extension BLEViewModel: CBPeripheralDelegate {
nonisolated func peripheral(
_ peripheral: CBPeripheral,
didDiscoverServices error: Error?
) {
guard let services = peripheral.services else { return }
for service in services {
peripheral.discoverCharacteristics(
[CBUUID(string: "2A37")],
for: service
)
}
}
nonisolated func peripheral(
_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?
) {
guard let characteristics = service.characteristics else { return }
for char in characteristics where char.properties.contains(.notify) {
peripheral.setNotifyValue(true, for: char)
}
}
nonisolated func peripheral(
_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?
) {
guard let data = characteristic.value, data.count >= 2 else { return }
let flags = data[0]
let bpm: Int
if (flags & 0x01) != 0 {
guard data.count >= 3 else { return }
bpm = Int(data[1]) | (Int(data[2]) << 8)
} else {
bpm = Int(data[1])
}
Task { @MainActor in
heartRate = bpm
}
}
}SwiftUI View
struct HeartRateView: View {
@State private var viewModel = BLEViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isConnected {
VStack {
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundStyle(.red)
Text("\(viewModel.heartRate) BPM")
.font(.largeTitle.monospacedDigit())
Button("Disconnect") { viewModel.disconnect() }
}
} else {
List(viewModel.discoveredDevices) { device in
Button {
viewModel.connect(to: device)
} label: {
HStack {
Text(device.name)
Spacer()
Text("\(device.rssi) dBm")
.foregroundStyle(.secondary)
}
}
}
}
}
.navigationTitle("Heart Rate Monitor")
.toolbar {
if !viewModel.isConnected {
Button(viewModel.isScanning ? "Stop" : "Scan") {
viewModel.isScanning ? viewModel.stopScan() : viewModel.startScan()
}
}
}
}
}
}Reconnection Strategies
Auto-Reconnect on Disconnect
func centralManager(
_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
timestamp: CFAbsoluteTime,
isReconnecting: Bool,
error: Error?
) {
if !isReconnecting {
// Attempt to reconnect
central.connect(peripheral, options: nil)
}
}Reconnecting to Known Peripherals
Store the peripheral's UUID and reconnect at next launch using retrievePeripherals(withIdentifiers:).
func reconnectToKnownDevice(uuid: UUID) {
let peripherals = centralManager.retrievePeripherals(
withIdentifiers: [uuid]
)
if let peripheral = peripherals.first {
connectedPeripheral = peripheral
centralManager.connect(peripheral, options: nil)
}
}Retrieving Already-Connected Peripherals
If another app has connected to the peripheral, you can retrieve it by service.
func findConnectedHeartRateMonitors() -> [CBPeripheral] {
centralManager.retrieveConnectedPeripherals(
withServices: [CBUUID(string: "180D")]
)
}Data Parsing Helpers
Generic Data Reader
extension Data {
func readUInt8(at offset: Int) -> UInt8? {
guard offset < count else { return nil }
return self[offset]
}
func readUInt16LE(at offset: Int) -> UInt16? {
guard offset + 1 < count else { return nil }
return UInt16(self[offset]) | (UInt16(self[offset + 1]) << 8)
}
func readUInt32LE(at offset: Int) -> UInt32? {
guard offset + 3 < count else { return nil }
return UInt32(self[offset])
| (UInt32(self[offset + 1]) << 8)
| (UInt32(self[offset + 2]) << 16)
| (UInt32(self[offset + 3]) << 24)
}
}Battery Level Parser
func parseBatteryLevel(_ data: Data) -> Int? {
data.readUInt8(at: 0).map { Int($0) }
}Write Flow Control
Use .withResponse for commands that need delivery confirmation. Reserve .withoutResponse for streaming or fire-and-forget payloads, and pause when the peripheral cannot accept more unacknowledged writes.
func writeChunks(_ chunks: [Data],
to characteristic: CBCharacteristic,
on peripheral: CBPeripheral) {
for chunk in chunks {
guard chunk.count <= peripheral.maximumWriteValueLength(for: .withoutResponse),
peripheral.canSendWriteWithoutResponse else { return }
peripheral.writeValue(chunk, for: characteristic, type: .withoutResponse)
}
}
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
// Continue draining queued chunks here.
}Multiple Peripheral Management
Managing Several Connections
@MainActor
final class MultiDeviceManager: NSObject {
private var centralManager: CBCentralManager!
private var connectedPeripherals: [UUID: CBPeripheral] = [:]
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String: Any],
rssi RSSI: NSNumber
) {
let id = peripheral.identifier
guard connectedPeripherals[id] == nil else { return }
connectedPeripherals[id] = peripheral
central.connect(peripheral)
}
func centralManager(
_ central: CBCentralManager,
didConnect peripheral: CBPeripheral
) {
peripheral.delegate = self
peripheral.discoverServices(nil)
}
func disconnectAll() {
for peripheral in connectedPeripherals.values {
centralManager.cancelPeripheralConnection(peripheral)
}
connectedPeripherals.removeAll()
}
}L2CAP Channels
Use L2CAP channels for higher-throughput, stream-oriented data transfer.
Central Side
func peripheral(
_ peripheral: CBPeripheral,
didOpen channel: CBL2CAPChannel?,
error: Error?
) {
guard let channel else { return }
let inputStream = channel.inputStream
let outputStream = channel.outputStream
inputStream.delegate = self
outputStream.delegate = self
inputStream.schedule(in: .main, forMode: .default)
outputStream.schedule(in: .main, forMode: .default)
inputStream.open()
outputStream.open()
}
// Open a channel to a known PSM
peripheral.openL2CAPChannel(CBL2CAPPSM(0x0025))Peripheral Side
// Publish a channel listener
peripheralManager.publishL2CAPChannel(withEncryption: true)
func peripheralManager(
_ peripheral: CBPeripheralManager,
didPublishL2CAPChannel PSM: CBL2CAPPSM,
error: Error?
) {
// Share the PSM with centrals via a characteristic value
print("Published L2CAP channel on PSM: \(PSM)")
}Peripheral Role: Responding to Requests
When acting as a peripheral, respond to read and write requests from connected centrals.
extension BLEPeripheralManager: CBPeripheralManagerDelegate {
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveRead request: CBATTRequest
) {
if request.characteristic.uuid == charUUID {
let value = currentSensorData()
request.value = value.subdata(
in: request.offset..<value.count
)
peripheral.respond(to: request, withResult: .success)
} else {
peripheral.respond(to: request, withResult: .attributeNotFound)
}
}
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveWrite requests: [CBATTRequest]
) {
for request in requests {
if let value = request.value {
handleIncomingData(value)
}
}
// Respond to the first request -- Core Bluetooth sends the
// response for all requests in the batch
if let first = requests.first {
peripheral.respond(to: first, withResult: .success)
}
}
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didSubscribeTo characteristic: CBCharacteristic
) {
// Central subscribed to notifications -- start sending updates
startSendingUpdates()
}
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didUnsubscribeFrom characteristic: CBCharacteristic
) {
stopSendingUpdates()
}
}Related skills
How it compares
Pick core-bluetooth for native iOS CBCentralManager and GATT workflows; use cross-platform BLE libraries when the same code must also target Android.
FAQ
Is there a Bluetooth permission prompt API?
No. Add NSBluetoothAlwaysUsageDescription and check manager.authorization and state instead.
Should I scan with nil service UUIDs in production?
No. Scan for specific service UUIDs to save power; nil discovers all peripherals.
When should I use accessorysetupkit instead?
Use accessorysetupkit for privacy-preserving accessory discovery and setup before GATT communication here.
Is Core Bluetooth safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.