使用 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 // 僅限近距離裝置
藍牙描述符至少需要 bluetoothCompanyIdentifier 或 bluetoothServiceUUID 其中之一。可依需求新增更精確的比對條件:
- 搭配公司識別碼或服務 UUID 使用
bluetoothNameSubstring - 搭配公司識別碼使用
bluetoothManufacturerDataBlob與bluetoothManufacturerDataMask;blob 與 mask 必須具有相同長度 - 搭配服務 UUID 使用
bluetoothServiceDataBlob與bluetoothServiceDataMask;blob 與 mask 必須具有相同長度
Wi-Fi 描述符
var descriptor = ASDiscoveryDescriptor()
descriptor.ssid = "MyAccessory-Network"
// 或使用前綴:
// descriptor.ssidPrefix = "MyAccessory-"
請僅提供 ssid 或 ssidPrefix 其中之一,切勿同時設定兩者。若兩者皆設定會導致 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 -->




