关于 core-bluetooth
面向 Swift 开发者的 Core Bluetooth 技能,介绍 BLE 设备扫描、连接与 GATT 数据交换,也涵盖外围设备广播、后台运行和状态恢复。
主要功能
- 01扫描并连接 BLE 外围设备
- 02发现服务与特征
- 03读写数据并订阅通知
- 04作为外围设备广播服务
- 05配置后台 BLE 模式
- 06恢复蓝牙管理器状态
适用场景
- 01连接并读取 BLE 传感器数据
- 02向外围设备写入特征值
- 03让应用在后台扫描或广播
- 04在 BLE 事件后恢复应用状态
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill core-bluetooth安装后在 Claude Code 中输入/core-bluetooth确认已加载。
附带文件evals/evals.jsonreferences/ble-patterns.md
详细介绍
Core Bluetooth
扫描、连接蓝牙低功耗(BLE)设备并与其交换数据。
涵盖中心角色(扫描并连接外围设备)、外围角色(广播服务)、后台模式和状态恢复。
对于保护隐私的配件发现和设置,请使用 accessorysetupkit;
对于直接的 Core Bluetooth GATT 通信,请使用此技能。
目录
设置
Info.plist 键
| 键 | 用途 |
|---|---|
NSBluetoothAlwaysUsageDescription | 必需。说明应用使用蓝牙的原因 |
UIBackgroundModes with bluetooth-central | 后台扫描和连接 |
UIBackgroundModes with bluetooth-peripheral | 后台广播 |
蓝牙授权
Core Bluetooth 没有显式的权限请求 API。添加
NSBluetoothAlwaysUsageDescription,在应用准备好使用蓝牙时创建管理器,然后检查 manager.authorization 和 manager.state。
将 .denied 和 .restricted 视为终止状态,直到用户在“设置”中更改权限;
等待状态变为 .poweredOn 后,再扫描、连接、广播或发布服务。
中心角色:扫描
创建中心管理器
扫描前始终等待状态变为 poweredOn。
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()
}
}
扫描外围设备
扫描特定服务 UUID 以节省电量。传入 nil 可发现所有外围设备(不建议在生产环境中这样做)。
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
}
发现服务和特征
实现 CBPeripheralDelegate,遍历服务/特征树。
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)")
}
}
外围角色:广播
使用 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"
])
}
}
后台 BLE
后台中心模式
将 bluetooth-central 添加到 UIBackgroundModes。在后台:
- 扫描必须指定一个或多个服务 UUID;使用
nil扫描仅适用于前台 - 扫描选项(包括
CBCentralManagerScanOptionAllowDuplicatesKey)均不起作用
后台外围模式
将 bluetooth-peripheral 添加到 UIBackgroundModes。在后台:
- 如果未启用此模式,应用暂停时发布的服务内容将被禁用
- 不会广播本地名称
- 服务 UUID 会移至溢出区域,需要显式扫描服务
状态恢复
状态恢复允许系统在应用因 BLE 事件而终止并重新启动后,重新创建中心或外围管理器。
中心管理器状态恢复
// 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.
}
常见错误
| 错误 | 修复方法 |
|---|---|
在 .poweredOn 之前扫描/连接 | 从 centralManagerDidUpdateState 开始 BLE 操作。 |
| 未保留已发现的外围设备 | 在连接和发现期间持有强引用。 |
生产环境扫描传入 nil 服务 | 按功能所需的服务 UUID 进行筛选。 |
在 didConnect 之前开始发现服务 | 仅在代理回调中推进流程,并处理失败/断开连接的情况。 |
| 写入时忽略特征属性或有效负载限制 | 选择受支持的写入类型,遵守 maximumWriteValueLength,并通过 canSendWriteWithoutResponse 判断是否可使用 .withoutResponse。 |
检查清单
- 已将
NSBluetoothAlwaysUsageDescription添加到 Info.plist - 所有 BLE 操作均以
centralManagerDidUpdateState返回.poweredOn为前提 - 发现的外围设备均通过强引用保留
- 在生产环境中,扫描使用特定的服务 UUID(而非
nil) - 在调用
discoverServices之前设置CBPeripheralDelegate - 读取、写入或启用通知前,检查特征的属性
- 写入载荷不超过
maximumWriteValueLength(for:) -
.withoutResponse写入遵循canSendWriteWithoutResponse - 如有需要,添加后台模式(
bluetooth-central或bluetooth-peripheral) - 如果应用需要支持在 BLE 事件发生时重新启动,则设置状态恢复标识符
- 使用状态恢复时,实现
willRestoreState代理方法 - 发现目标外围设备后停止扫描
- 处理断开连接,并可选择实现自动重连逻辑
- 写入类型与特征属性匹配(
.withResponse与.withoutResponse)
参考资料
- 扩展模式(重连策略、数据解析、SwiftUI 集成):references/ble-patterns.md
- Core Bluetooth 框架
- CBCentralManager
- CBPeripheral
- CBPeripheralManager
- CBService
- CBCharacteristic
- CBUUID
- CBCentralManagerDelegate
- CBPeripheralDelegate
- NSBluetoothAlwaysUsageDescription
- CBManagerAuthorization
- scanForPeripherals(withServices:options:)
- startAdvertising(_:)
- writeValue(_:for:type:)
- maximumWriteValueLength(for:)
- canSendWriteWithoutResponse
- 配置后台执行模式