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.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill 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.
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.
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()
}
}
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)
}
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
}
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)
}
}
}
}
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])
}
}
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) {}
// 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.
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"
])
}
}
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.
// 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.
}
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.
}
| 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)Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks.
Structured manuscript/grant review with checklist-based evaluation. Use when writing formal peer reviews with specific criteria methodology assessment, statistical validity, reporting standards compliance (CONSORT/STROBE), and constructive feedback. Best for actual review writing, manuscript revision. For evaluating claims/evidence quality use scientific-critical-thinking; for quantitative scoring frameworks use scholar-evaluation.
Agile product ownership toolkit for Senior Product Owner including INVEST-compliant user story generation, sprint planning, backlog management, and velocity tracking. Use for story writing, sprint planning, stakeholder communication, and agile ceremonies.
Expert guidance for writing secure, reliable, and performant Claude Code hooks - validates design decisions, enforces best practices, and prevents common pitfalls. Use when creating, reviewing, or debugging Claude Code hooks.
Use when creating or developing anything, before writing code or implementation plans - refines rough ideas into fully-formed designs through structured Socratic questioning, alternative exploration, and incremental validation
Structured manuscript/grant review with checklist-based evaluation. Use when writing formal peer reviews with specific criteria methodology assessment, statistical validity, reporting standards compliance (CONSORT/STROBE), and constructive feedback. Best for actual review writing, manuscript revision. For evaluating claims/evidence quality use scientific-critical-thinking; for quantitative scoring frameworks use scholar-evaluation.
Conducts structured requirements workshops to produce feature specifications, user stories, EARS-format functional requirements, acceptance criteria, and implementation checklists. Use when defining new features, gathering requirements, or writing specifications. Invoke for feature definition, requirements gathering, user stories, EARS format specs, PRDs, acceptance criteria, or requirement matrices.
Use markdown formatting when drafting content intended for external systems (GitHub issues/PRs, Jira tickets, wiki pages, design docs, etc.) so formatting is preserved when the user copies it. Load this skill before producing any draft the user will paste elsewhere.
Take dpearson2699/core-bluetooth from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.