accessorysetupkit

accessorysetupkit

熱門

使用 AccessorySetupKit 尋找與設定藍牙及 Wi-Fi 配件。適用於顯示注重隱私的配件選擇器、定義 BLE 或 Wi-Fi 裝置的搜尋描述符 (discovery descriptors)、處理配件 Session 事件、從基於權限的 CoreBluetooth 掃描機制進行遷移,或是無需請求廣泛藍牙權限即可設定配件時。

967星標
49分支
更新於 2026/7/31
SKILL.md
唯讀
名稱
accessorysetupkit
描述

使用 AccessorySetupKit 尋找與設定藍牙及 Wi-Fi 配件。適用於顯示注重隱私的配件選擇器、定義 BLE 或 Wi-Fi 裝置的搜尋描述符 (discovery descriptors)、處理配件 Session 事件、從基於權限的 CoreBluetooth 掃描機制進行遷移,或是無需請求廣泛藍牙權限即可設定配件時。

AccessorySetupKit

使用 iOS 18+ 的系統選擇器來進行注重隱私保護的藍牙/Wi-Fi 配件搜尋與授權,接著將通訊接管給 CoreBluetooth 或 NetworkExtension。

目錄

設定與權限

Info.plist 設定

在 App 的 Info.plist 中新增以下 Key:

Key Type Purpose
NSAccessorySetupSupports [String] 必填。包含 Bluetooth 及/或 WiFi 的陣列
NSAccessorySetupBluetoothServices [String] App 要搜尋的服務 UUID (藍牙)
NSAccessorySetupBluetoothNames [String] 要比對的藍牙名稱或子字串
NSAccessorySetupBluetoothCompanyIdentifiers [String] 兩位元組 (Two-byte) 的藍牙公司識別碼

藍牙專用的 Key 必須與 ASDiscoveryDescriptor 中使用的值相符。如果 App 使用了未在 Info.plist 中宣告的識別碼、名稱或服務,App 會在 AccessorySetupKit 搜尋期間發生當機 (crash)。對於 Wi-Fi 配件,請在 NSAccessorySetupSupports 中包含 WiFi,並符合描述符的 SSID 規則。

無需藍牙權限

當 App 在 NSAccessorySetupSupports 中宣告了 Bluetooth 時,建立 CBCentralManager 將不再觸發系統藍牙權限對話框。Central Manager 的狀態只有在 App 透過 AccessorySetupKit 配對至少一個配件後,才會轉變為 poweredOn

搜尋描述符

ASDiscoveryDescriptor 用於定義尋找配件時的比對條件。系統會將掃描到的結果與描述符中的所有規則進行比對,藉此篩選出目標配件。

藍牙描述符

import AccessorySetupKit
import CoreBluetooth

var descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothServiceUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABC")
descriptor.bluetoothNameSubstring = "MyDevice"
descriptor.bluetoothRange = .immediate  // 僅限近距離裝置

藍牙描述符至少需要 bluetoothCompanyIdentifierbluetoothServiceUUID 其中之一。可依需求新增更精確的比對條件:

  • 搭配公司識別碼或服務 UUID 使用 bluetoothNameSubstring
  • 搭配公司識別碼使用 bluetoothManufacturerDataBlobbluetoothManufacturerDataMask;blob 與 mask 必須具有相同長度
  • 搭配服務 UUID 使用 bluetoothServiceDataBlobbluetoothServiceDataMask;blob 與 mask 必須具有相同長度

Wi-Fi 描述符

var descriptor = ASDiscoveryDescriptor()
descriptor.ssid = "MyAccessory-Network"
// 或使用前綴:
// descriptor.ssidPrefix = "MyAccessory-"

請僅提供 ssidssidPrefix 其中之一,切勿同時設定兩者。若兩者皆設定會導致 App 當機。ssidPrefix 的長度不得為零。

藍牙距離範圍

控制搜尋時所需的物理接近程度:

Value Behavior
.default 標準藍牙範圍
.immediate 僅限物理距離極近的配件

支援選項

在描述符上設定 supportedOptions 以宣告配件的能力:

descriptor.supportedOptions = [.bluetoothPairingLE, .bluetoothTransportBridging]
Option Purpose
.bluetoothPairingLE 支援 BLE 配對
.bluetoothTransportBridging 支援藍牙傳輸橋接 (Transport Bridging)
.bluetoothHID 藍牙 HID 裝置

顯示選擇器

建立 Session

建立並啟用 ASAccessorySession 以管理搜尋生命週期。在讀取 session.accessories 或顯示選擇器之前,請先等待 .activated 事件:

import AccessorySetupKit

final class AccessoryManager {
    private let session = ASAccessorySession()

    func start() {
        session.activate(on: .main) { [weak self] event in
            self?.handleEvent(event)
        }
    }

    private func handleEvent(_ event: ASAccessoryEvent) {
        switch event.eventType {
        case .activated:
            // Session 已就緒。檢查 session.accessories 以取得先前已配對的裝置。
            break
        case .accessoryAdded:
            guard let accessory = event.accessory else { return }
            handleAccessoryAdded(accessory)
        case .accessoryChanged:
            // 配件屬性已變更(例如在「設定」中更新了顯示名稱)
            break
        case .accessoryRemoved:
            // 配件已被使用者或 App 移除
            break
        case .invalidated:
            // Session 已失效,無法重複使用
            break
        @unknown default:
            break
        }
    }
}

顯示選擇器

建立包含名稱、產品圖片及搜尋描述符的 ASPickerDisplayItem 執行個體,然後傳入已啟用的 session:

func showAccessoryPicker() {
    var descriptor = ASDiscoveryDescriptor()
    descriptor.bluetoothServiceUUID = CBUUID(string: "ABCD1234-0000-1000-8000-00805F9B34FB")

    guard let image = UIImage(named: "my-accessory") else { return }

    let item = ASPickerDisplayItem(
        name: "My Bluetooth Accessory",
        productImage: image,
        descriptor: descriptor
    )

    session.showPicker(for: [item]) { error in
        if let error {
            print("Picker failed: \(error.localizedDescription)")
        }
    }
}

選擇器是在獨立的系統程序 (system process) 中執行。它會將每一個符合條件的裝置顯示為單個項目。當有多個裝置符合指定的描述符時,選擇器會建立一個水平輪播視圖 (carousel)。

設定選項

為每個顯示項目設定選擇器行為:

var item = ASPickerDisplayItem(
    name: "My Accessory",
    productImage: image,
    descriptor: descriptor
)
item.setupOptions = [.rename, .confirmAuthorization]
Option Effect
.rename 允許在設定過程中重新命名配件
.confirmAuthorization 在設定前顯示授權確認
.finishInApp 提示在配對完成後繼續於 App 內進行設定

產品圖片

選擇器會在 180x120 pt 的容器中顯示圖片。最佳做法:

  • 針對所有螢幕縮放倍率 (scale factors) 提供高解析度圖片
  • 使用透明背景,以確保在淺色/深色模式下皆能正確顯示
  • 調整透明邊框作為內距 (padding),以控制配件的視覺大小
  • 請在淺色與深色模式下皆進行測試

事件處理

事件類型

Session 會透過事件處理常式 (event handler) 傳遞 ASAccessoryEvent 物件:

Event When
.activated Session 已啟用,可查詢 session.accessories
.accessoryAdded 使用者在選擇器中選取了配件
.accessoryChanged 配件屬性已更新(例如重新命名)
.accessoryRemoved 配件已從系統中移除
.invalidated Session 已失效,需建立新的 Session
.migrationComplete 舊版配件的遷移已完成
.pickerDidPresent 選擇器已顯示在螢幕上
.pickerDidDismiss 選擇器已關閉
.pickerSetupBridging 傳輸橋接設定進行中
.pickerSetupPairing 藍牙配對進行中
.pickerSetupFailed 設定失敗
.pickerSetupRename 使用者正在重新命名配件
.accessoryDiscovered 搜尋到新配件(自訂篩選模式)

協調選擇器關閉

當使用者選取配件時,.accessoryAdded 會在 .pickerDidDismiss 之前觸發。若要在選擇器關閉後顯示自訂的設定 UI,請在第一個事件時儲存該配件,並於關閉後執行相應操作:

private var pendingAccessory: ASAccessory?

private func handleEvent(_ event: ASAccessoryEvent) {
    switch event.eventType {
    case .accessoryAdded:
        pendingAccessory = event.accessory
    case .pickerDidDismiss:
        if let accessory = pendingAccessory {
            pendingAccessory = nil
            beginCustomSetup(accessory)
        }
    @unknown default:
        break
    }
}

藍牙配件

透過選擇器新增配件後,請使用 CoreBluetooth 進行通訊。ASAccessory 上的 bluetoothIdentifier 會對應到 CBPeripheral

import CoreBluetooth

func handleAccessoryAdded(_ accessory: ASAccessory) {
    guard let btIdentifier = accessory.bluetoothIdentifier else { return }

    // 建立 CBCentralManager — 不會跳出藍牙權限提示視窗
    let centralManager = CBCentralManager(delegate: self, queue: nil)

    // 轉為 poweredOn 後,檢索 (retrieve) 周邊裝置
    let peripherals = centralManager.retrievePeripherals(
        withIdentifiers: [btIdentifier]
    )
    guard let peripheral = peripherals.first else { return }
    centralManager.connect(peripheral, options: nil)
}

重點摘要:

  • 只有在 App 擁有已配對的配件時,CBCentralManager 的狀態才會達到 .poweredOn
  • 使用 scanForPeripherals(withServices:) 進行掃描時,僅會傳回透過 AccessorySetupKit 配對的配件
  • 當專門使用 AccessorySetupKit 時,無需設定 NSBluetoothAlwaysUsageDescription

Wi-Fi 配件

對於 Wi-Fi 配件,ASAccessory 上的 ssid 用於識別網路。請使用 NetworkExtension 中的 NEHotspotConfiguration 來加入網路:

import NetworkExtension

func handleWiFiAccessoryAdded(_ accessory: ASAccessory) {
    guard let ssid = accessory.ssid else { return }

    let configuration = NEHotspotConfiguration(ssid: ssid)
    NEHotspotConfigurationManager.shared.apply(configuration) { error in
        if let error {
            print("Wi-Fi 連線失敗:\(error.localizedDescription)")
        }
    }
}

由於該配件是透過 AccessorySetupKit 搜尋到的,因此加入網路時不會觸發標準的 Wi-Fi 存取提示。

從 CoreBluetooth 遷移

擁有現有 CoreBluetooth 授權配件的 App,可以使用 ASMigrationDisplayItem 將配件遷移至 AccessorySetupKit。這是一次性操作,會在新系統中註冊已知配件。

func migrateExistingAccessories() {
    guard let image = UIImage(named: "my-accessory") else { return }

    var descriptor = ASDiscoveryDescriptor()
    descriptor.bluetoothServiceUUID = CBUUID(string: "ABCD1234-0000-1000-8000-00805F9B34FB")

    let migrationItem = ASMigrationDisplayItem(
        name: "My Accessory",
        productImage: image,
        descriptor: descriptor
    )
    // 設定來自 CoreBluetooth 的 peripheral 識別碼
    migrationItem.peripheralIdentifier = existingPeripheralUUID

    // 對於 Wi-Fi 配件:
    // migrationItem.hotspotSSID = "MyAccessory-WiFi"

    session.showPicker(for: [migrationItem]) { error in
        if let error {
            print("遷移失敗:\(error.localizedDescription)")
        }
    }
}

遷移規則:

  • showPicker 僅包含遷移項目,系統將顯示說明頁面而非搜尋選擇器
  • 若遷移項目與一般顯示項目混合使用,則僅會在搜尋並設定新配件時進行遷移
  • 請勿在遷移完成前初始化 CBCentralManager — 否則會引發錯誤並導致選擇器無法顯示
  • 當遷移結束時,Session 會收到 .migrationComplete 事件

常見錯誤

Mistake Fix
Descriptor identifiers are absent from Info.plist Declar

<!-- truncated for translation batch; full body continues in source -->