core-bluetooth
dpearson2699/swift-ios-skills
Build Bluetooth Low Energy central and peripheral workflows with Core Bluetooth scanning, connecting, GATT communication, and background modes.
What is core-bluetooth?
Core Bluetooth enables direct BLE communication for iOS apps. Use it to scan and connect to peripherals (central role), discover and read/write GATT services and characteristics, subscribe to notifications, advertise local services (peripheral role), and handle background BLE modes and state restoration. Targets Swift 6.3 / iOS 26+.
- Scan for and connect to BLE peripherals with CBCentralManager
- Discover services and characteristics on remote devices
- Read, write, and subscribe to characteristic notifications
- Publish local GATT services with CBPeripheralManager
- Handle Bluetooth authorization and power state
- Support background BLE scanning and advertising with state restoration
How to install core-bluetooth
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill core-bluetooth- NSBluetoothAlwaysUsageDescription in Info.plist
- UIBackgroundModes with bluetooth-central or bluetooth-peripheral if background operation needed
- Swift 6.3 / iOS 26+
How to use core-bluetooth
- 1.Create a CBCentralManager delegate and wait for poweredOn state before scanning
- 2.Scan for specific service UUIDs to save power using scanForPeripherals
- 3.Implement CBPeripheralDelegate to handle discovered services and characteristics
- 4.Discover services on connected peripherals, then discover characteristics within each service
- 5.Read characteristic values via readValue or subscribe to notifications with setNotifyValue
- 6.Write values using writeValue with appropriate write type based on characteristic properties
- 7.For peripheral role, create CBPeripheralManager and publish services with addService
Use cases
- Heart rate monitor or fitness tracker integration
- Smart home accessory control and data collection
- Wearable device communication and sensor data streaming
- Local device-to-device data exchange without internet
- Background health monitoring with BLE notifications
- iOS app developers building BLE integrations
- Wearable and health app developers
- Smart home and IoT application teams
- Developers needing direct GATT communication after accessorysetupkit setup
core-bluetooth FAQ
You must retain the CBPeripheral object. Store it as a property (e.g., discoveredPeripheral) or it will be garbage collected and the connection will fail.
Use accessorysetupkit for privacy-preserving accessory discovery and setup flows. After setup completes, return to Core Bluetooth for direct GATT communication.
Core Bluetooth has no explicit permission request API. Add NSBluetoothAlwaysUsageDescription to Info.plist, create the manager when ready, check manager.authorization and manager.state, and wait for .poweredOn before any BLE operations.
Write with response waits for confirmation from the peripheral (slower, reliable). Write without response sends immediately without waiting (faster, best-effort). Check characteristic properties and use canSendWriteWithoutResponse before choosing.
Add UIBackgroundModes with bluetooth-central (for scanning/connecting) or bluetooth-peripheral (for advertising) to Info.plist. Implement state restoration to reconnect after app termination.
Full instructions (SKILL.md)
Source of truth, from dpearson2699/swift-ios-skills.
name: core-bluetooth description: "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 accessorysetupkit first and return here for post-setup GATT communication."
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
Related skills
More from dpearson2699/swift-ios-skills and the wider catalog.

core-data
Build, review, or improve Core Data persistence in apps that have not adopted SwiftData. Use when working with NSManagedObject subclasses, NSFetchedResultsController for list-driven UI, NSBatchInsertRequest / NSBatchDeleteRequest / NSBatchUpdateRequest for bulk operations, NSPersistentHistoryChangeRequest for persistent history tracking and multi-target sync, NSStagedMigrationManager for staged schema migrations (iOS 17+), NSCompositeAttributeDescription for composite attributes (iOS 17+), or when integrating Core Data threading with Swift Concurrency. For Core Data + SwiftData coexistence or migration, see the swiftdata skill instead.

core-motion
Access iOS/watchOS device sensors: accelerometer, gyroscope, magnetometer, pedometer, activity recognition, altitude, and motion data.

core-nfc
Read and write NFC tags on iPhone using CoreNFC framework.

coreml
Load, configure, and run Core ML models in iOS apps with on-device inference.

cryptokit
Apple CryptoKit for Swift cryptographic primitives: hashing, encryption, signing, and key exchange.

cryptotokenkit
Access security tokens and smart cards via CryptoTokenKit for token drivers, NFC sessions, and certificate-based authentication.