使用 AccessorySetupKit 发现与配置蓝牙及 Wi-Fi 配件。适用于展示隐私保护型配件选择器、为 BLE 或 Wi-Fi 设备定义发现描述符、处理配件会话事件、从基于权限的 CoreBluetooth 扫描方案迁移,或在无需全局蓝牙权限的情况下配对设置配件。
AccessorySetupKit
使用 iOS 18+ 系统选择器,在保护用户隐私的前提下发现蓝牙与 Wi-Fi 配件并完成授权,随后交由 CoreBluetooth 或 NetworkExtension 进行后续通信。
目录
配置与 Entitlements
Info.plist 配置
在 App 的 Info.plist 中添加以下 Key:
| Key | 类型 | 作用 |
|---|---|---|
NSAccessorySetupSupports |
[String] |
必填。包含 Bluetooth 和/或 WiFi 的字符串数组 |
NSAccessorySetupBluetoothServices |
[String] |
App 需要发现的服务 UUID(蓝牙) |
NSAccessorySetupBluetoothNames |
[String] |
待匹配的蓝牙名称或子字符串 |
NSAccessorySetupBluetoothCompanyIdentifiers |
[String] |
双字节蓝牙厂商标识符(Company Identifier) |
蓝牙专属的 Key 必须与 ASDiscoveryDescriptor 中使用的值完全一致。如果 App 使用了未在 Info.plist 中声明的标识符、名称或服务,应用将在 AccessorySetupKit 搜索发现过程中崩溃。对于 Wi-Fi 配件,请确保在 NSAccessorySetupSupports 中包含 WiFi,并与描述符中的 SSID 规则保持一致。
无需蓝牙权限
当 App 在 NSAccessorySetupSupports 中声明了 Bluetooth 时,创建 CBCentralManager 将不再触发系统蓝牙权限弹窗。只有当 App 通过 AccessorySetupKit 成功配对至少一个配件后,Central Manager 的状态才会转为 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 时,其长度不能为 0。
蓝牙信号距离(Range)
控制搜寻配件所需的物理近距范围:
| 取值 | 行为描述 |
|---|---|
.default |
标准蓝牙信号覆盖范围 |
.immediate |
仅匹配极近物理距离内的配件 |
功能支持选项(Support Options)
在描述符上设置 supportedOptions,以声明配件具备的功能特性:
descriptor.supportedOptions = [.bluetoothPairingLE, .bluetoothTransportBridging]
| 选项 | 作用 |
|---|---|
.bluetoothPairingLE |
支持 BLE 配对 |
.bluetoothTransportBridging |
支持蓝牙传输桥接(Transport Bridging) |
.bluetoothHID |
蓝牙 HID 人机接口设备 |
展示选择器
创建会话(Session)
创建并激活 ASAccessorySession 以管理搜寻生命周期。请务必等待收到 .activated 事件后再读取 session.accessories 或展示选择器:
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.accessories 以获取先前已配对的设备。
break
case .accessoryAdded:
guard let accessory = event.accessory else { return }
handleAccessoryAdded(accessory)
case .accessoryChanged:
// 配件属性发生变更(例如在“设置”中修改了显示名称)
break
case .accessoryRemoved:
// 配件已被用户或应用移除
break
case .invalidated:
// 会话失效,无法继续重用
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: "我的蓝牙配件",
productImage: image,
descriptor: descriptor
)
session.showPicker(for: [item]) { error in
if let error {
print("选择器展示失败: \(error.localizedDescription)")
}
}
}
选择器运行在独立的系统进程中。每一个匹配成功的设备都会作为独立卡片显示。如果同一个描述符匹配到多个设备,选择器会展示为一个水平轮播视图。
配对设置选项(Setup Options)
针对每个展示项配置选择器的行为:
var item = ASPickerDisplayItem(
name: "我的配件",
productImage: image,
descriptor: descriptor
)
item.setupOptions = [.rename, .confirmAuthorization]
| 选项 | 效果 |
|---|---|
.rename |
允许用户在配对过程中重命名配件 |
.confirmAuthorization |
在配对前弹出授权确认窗口 |
.finishInApp |
标识配对完成后将在 App 内部继续后续设置 |
产品图片规范
选择器在 180x120 pt 的容器中展示图片。最佳实践建议:
- 针对所有屏幕缩放因子(Scale Factors)提供高分辨率图片
- 使用透明背景,以完美适配浅色/深色模式
- 将透明边框作为 Padding 边距调整,以掌控配件的视觉显示大小
- 在浅色与深色模式下均需进行实际测试
事件处理
事件类型
会话通过事件处理回调传递 ASAccessoryEvent 对象:
| 事件 | 触发时机 |
|---|---|
.activated |
会话成功激活,此时可查询 session.accessories |
.accessoryAdded |
用户在选择器中选中了某个配件 |
.accessoryChanged |
配件属性发生更新(例如被重命名) |
.accessoryRemoved |
配件已被系统移除 |
.invalidated |
会话已被废弃失效,需重新创建新会话 |
.migrationComplete |
旧版配件迁移完成 |
.pickerDidPresent |
选择器已在屏幕上弹出展示 |
.pickerDidDismiss |
选择器已被关闭/消失 |
.pickerSetupBridging |
正在配置传输桥接(Transport Bridging) |
.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 时,检索对应外设
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 方案,无需在 Info.plist 中配置
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: "我的配件",
productImage: image,
descriptor: descriptor
)
// 设置来自 CoreBluetooth 的外设 UUID 标识符
migrationItem.peripheralIdentifier = existingPeripheralUUID
// 若为 Wi-Fi 配件:
// migrationItem.hotspotSSID = "MyAccessory-WiFi"
session.showPicker(for: [migrationItem]) { error in
if let error {
print("迁移失败: \(error.localizedDescription)")
}
}
}
迁移规则:
- 如果
showPicker传入的仅包含迁移项,系统会直接展示提示说明页面,而非设备搜寻选择器 - 如果将迁移项与普通展示项混合传入,则仅当发现并配置新配件时才会触发迁移
- 在迁移完成前切勿初始化
CBCentralManager,否则会导致错误并使选择器无法正常弹出 - 迁移全部完成后,会话将收到
.migrationComplete事件
常见错误
| 常见错误 | 修复方案 |
|---|---|
| 描述符标识符未在 Info.plist 中声明 | 声明 |




