core-bluetooth

core-bluetooth

熱門

使用 Core Bluetooth 建立直接的藍牙低功耗(BLE)工作流程。適用於實作 BLE 中央或周邊 GATT 通訊、使用 CBCentralManager 掃描或連線、使用 CBPeripheral 探索服務與特徵、讀取/寫入/訂閱、使用 CBPeripheralManager 發布本地服務、處理藍牙授權、背景 BLE 模式、狀態還原、寫入流量控制或基於 CBUUID 的工作流程。若需隱私保護的配件設定/挑選流程,請先使用 accessorysetupkit,再回到此技能進行設定後的 GATT 通訊。

936星標
47分支
更新於 2026/7/15
SKILL.md
唯讀
名稱
core-bluetooth
描述

使用 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.authorizationmanager.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-centralbluetooth-peripheral
  • [ ] 若 App 需要支援因 BLE 事件重新啟動,已設定狀態還原識別碼
  • [ ] 使用狀態還原時,已實作 willRestoreState 委派方法
  • [ ] 發現目標周邊後停止掃描
  • [ ] 處理斷線,並可選擇加入自動重新連線邏輯
  • [ ] 寫入類型符合特徵屬性(.withResponse.withoutResponse

參考資料