
core-bluetooth
熱門使用 Core Bluetooth 建立直接的藍牙低功耗(BLE)工作流程。適用於實作 BLE 中央或周邊 GATT 通訊、使用 CBCentralManager 掃描或連線、使用 CBPeripheral 探索服務與特徵、讀取/寫入/訂閱、使用 CBPeripheralManager 發布本地服務、處理藍牙授權、背景 BLE 模式、狀態還原、寫入流量控制或基於 CBUUID 的工作流程。若需隱私保護的配件設定/挑選流程,請先使用 accessorysetupkit,再回到此技能進行設定後的 GATT 通訊。
使用 Core Bluetooth 建立直接的藍牙低功耗(BLE)工作流程。適用於實作 BLE 中央或周邊 GATT 通訊、使用 CBCentralManager 掃描或連線、使用 CBPeripheral 探索服務與特徵、讀取/寫入/訂閱、使用 CBPeripheralManager 發布本地服務、處理藍牙授權、背景 BLE 模式、狀態還原、寫入流量控制或基於 CBUUID 的工作流程。若需隱私保護的配件設定/挑選流程,請先使用 accessorysetupkit,再回到此技能進行設定後的 GATT 通訊。
Core Bluetooth
掃描、連線並與藍牙低功耗(BLE)裝置交換資料。
涵蓋中央角色(掃描並連線至周邊裝置)、周邊
角色(廣播服務)、背景模式及狀態還原。
使用 accessorysetupkit 進行隱私保護的配件探索與設定;
此技能則用於直接的 Core Bluetooth GATT 通訊。
目錄
設定
Info.plist 鍵值
| 鍵 | 用途 |
|---|---|
NSBluetoothAlwaysUsageDescription |
必要。說明 App 為何使用藍牙 |
UIBackgroundModes 搭配 bluetooth-central |
背景掃描與連線 |
UIBackgroundModes 搭配 bluetooth-peripheral |
背景廣播 |
藍牙授權
Core Bluetooth 沒有明確的權限請求 API。加入
NSBluetoothAlwaysUsageDescription,在 App 準備好使用藍牙時建立管理器,
然後檢查 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 } // 過濾弱訊號
// 重要:保留 peripheral,否則會被釋放
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 {
// 系統正在自動重新連線
return
}
// 處理斷線 — 可選擇重新連線
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("心率:\(heartRate) bpm")
}
case CBUUID(string: "2A19"):
let batteryLevel = data.first.map { Int($0) } ?? 0
print("電量:\(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)
}
// .withResponse 寫入的確認回呼。
func peripheral(
_ peripheral: CBPeripheral,
didWriteValueFor characteristic: CBCharacteristic,
error: Error?
) {
if let error {
print("寫入失敗:\(error.localizedDescription)")
}
}
// 在此恢復排隊的 .withoutResponse 寫入。
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {}
訂閱通知
// 訂閱
peripheral.setNotifyValue(true, for: characteristic)
// 取消訂閱
peripheral.setNotifyValue(false, for: characteristic)
// 確認
func peripheral(
_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?
) {
if characteristic.isNotifying {
print("現在接收 \(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 移至溢位區域,需要明確的服務掃描
狀態還原
狀態還原允許系統在 App 因 BLE 事件終止並重新啟動後,重新建立您的中央或周邊管理器。
中央管理器狀態還原
// 1. 使用還原識別碼建立
centralManager = CBCentralManager(
delegate: self,
queue: nil,
options: [CBCentralManagerOptionRestoreIdentifierKey: "myCentral"]
)
// 2. 實作還原委派方法
func centralManager(
_ central: CBCentralManager,
willRestoreState dict: [String: Any]
) {
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey]
as? [CBPeripheral] {
for peripheral in peripherals {
// 重新指派委派並保留
peripheral.delegate = self
discoveredPeripheral = peripheral
}
}
let restoredServices = dict[CBCentralManagerRestoredStateScanServicesKey]
as? [CBUUID]
let restoredOptions = dict[CBCentralManagerRestoredStateScanOptionsKey]
as? [String: Any]
// 若仍需掃描,使用 restoredServices/restoredOptions 恢復掃描。
}
周邊管理器狀態還原
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]
// 視需要將 App 狀態重新連接到還原的服務/廣播。
}
常見錯誤
| 錯誤 | 修正 |
|---|---|
在 .poweredOn 前掃描/連線 |
從 centralManagerDidUpdateState 開始 BLE 工作。 |
| 發現的周邊未保留 | 在連線與探索期間持有強引用。 |
正式環境掃描傳入 nil 服務 |
依功能需要的服務 UUID 進行過濾。 |
在 didConnect 前開始服務探索 |
僅從委派回呼推進,並處理失敗/斷線路徑。 |
| 寫入忽略特徵屬性或負載限制 | 選擇支援的寫入類型,遵守 maximumWriteValueLength,並在 .withoutResponse 前檢查 canSendWriteWithoutResponse。 |
審查清單
- [ ] 在 Info.plist 中加入
NSBluetoothAlwaysUsageDescription - [ ] 所有 BLE 操作在
centralManagerDidUpdateState回傳.poweredOn後才執行 - [ ] 發現的周邊以強引用保留
- [ ] 正式環境掃描使用特定服務 UUID(非
nil) - [ ] 在呼叫
discoverServices前設定CBPeripheralDelegate - [ ] 在讀取/寫入/通知前檢查特徵屬性
- [ ] 寫入負載不超過
maximumWriteValueLength(for:) - [ ]
.withoutResponse寫入遵循canSendWriteWithoutResponse - [ ] 若需要背景模式,已加入
bluetooth-central或bluetooth-peripheral - [ ] 若 App 需要支援因 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
- 設定背景執行模式



