关于 accessorysetupkit
面向 iOS 18 及以上版本开发者的 AccessorySetupKit 集成指南,介绍如何通过系统选择器发现并授权蓝牙或 Wi-Fi 配件。内容还涵盖配对后的通信、事件处理及从 CoreBluetooth 迁移。
主要功能
- 01蓝牙与 Wi-Fi 配件发现
- 02系统选择器与授权设置
- 03会话事件处理
- 04配对后连接配件
- 05迁移现有配件
适用场景
- 01在应用中引导用户发现并授权配件
- 02通过 CoreBluetooth 连接已配对的蓝牙设备
- 03使用 NetworkExtension 加入配件 Wi-Fi 网络
- 04将现有 CoreBluetooth 配件迁移至新流程
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill accessorysetupkit安装后在 Claude Code 中输入/accessorysetupkit确认已加载。
附带文件evals/evals.jsonreferences/accessorysetupkit-patterns.md
详细介绍
AccessorySetupKit
使用 iOS 18+ 系统选择器,以保护隐私的方式发现并授权蓝牙/Wi-Fi 配件,然后将通信交由 CoreBluetooth 或 NetworkExtension 处理。
目录
设置和 Entitlements
Info.plist 配置
将以下键添加到应用的 Info.plist 中:
| 键 | 类型 | 用途 |
|---|---|---|
NSAccessorySetupSupports | [String] | 必需。包含 Bluetooth 和/或 WiFi 的数组 |
NSAccessorySetupBluetoothServices | [String] | 应用发现的服务 UUID(蓝牙) |
NSAccessorySetupBluetoothNames | [String] | 用于匹配的蓝牙名称或子字符串 |
NSAccessorySetupBluetoothCompanyIdentifiers | [String] | 两字节蓝牙公司标识符 |
蓝牙专用键必须与 ASDiscoveryDescriptor 中使用的值匹配。
如果应用使用的标识符、名称或服务未在 Info.plist 中声明,应用会在 AccessorySetupKit 发现期间崩溃。对于 Wi-Fi 配件,请在
NSAccessorySetupSupports 中包含 WiFi,并确保描述符的 SSID 规则相匹配。
无需蓝牙权限
当应用使用 Bluetooth 声明 NSAccessorySetupSupports 时,创建
CBCentralManager 不再触发系统蓝牙权限对话框。只有当应用通过 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 // Only nearby devices
蓝牙描述符至少需要设置 bluetoothCompanyIdentifier 或
bluetoothServiceUUID 之一。可按需添加更精确的匹配条件:
- 将
bluetoothNameSubstring与公司标识符或服务 UUID 配合使用 - 将
bluetoothManufacturerDataBlob和bluetoothManufacturerDataMask与公司标识符配合使用;blob 和 mask 的长度必须相同 - 将
bluetoothServiceDataBlob和bluetoothServiceDataMask与服务 UUID 配合使用; blob 和 mask 的长度必须相同
Wi-Fi 描述符
var descriptor = ASDiscoveryDescriptor()
descriptor.ssid = "MyAccessory-Network"
// OR use a prefix:
// descriptor.ssidPrefix = "MyAccessory-"
请提供 ssid 或 ssidPrefix,不能同时提供两者。如果两者都设置,应用会崩溃。
ssidPrefix 的长度必须大于零。
蓝牙范围
控制发现所需的物理距离:
| 值 | 行为 |
|---|---|
.default | 标准蓝牙范围 |
.immediate | 仅发现近距离内的配件 |
支持选项
在描述符上设置 supportedOptions,以声明配件的功能:
descriptor.supportedOptions = [.bluetoothPairingLE, .bluetoothTransportBridging]
| 选项 | 用途 |
|---|---|
.bluetoothPairingLE | 支持 BLE 配对 |
.bluetoothTransportBridging | 蓝牙传输桥接 |
.bluetoothHID | 蓝牙 HID 设备 |
显示选择器
创建会话
创建并激活 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 ready. Check session.accessories for previously paired devices.
break
case .accessoryAdded:
guard let accessory = event.accessory else { return }
handleAccessoryAdded(accessory)
case .accessoryChanged:
// Accessory properties changed (e.g., display name updated in Settings)
break
case .accessoryRemoved:
// Accessory removed by user or app
break
case .invalidated:
// Session invalidated, cannot be reused
break
@unknown default:
break
}
}
}
显示选择器
创建包含名称、产品图片和发现描述符的 ASPickerDisplayItem 实例,然后将其传递给已激活的会话:
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)")
}
}
}
选择器在独立的系统进程中运行。它会将每个匹配设备显示为单独的条目。当多个设备与某个描述符匹配时,选择器会创建水平轮播列表。
设置选项
为每个显示项配置选择器行为:
var item = ASPickerDisplayItem(
name: "My Accessory",
productImage: image,
descriptor: descriptor
)
item.setupOptions = [.rename, .confirmAuthorization]
| 选项 | 效果 |
|---|---|
.rename | 允许在设置期间重命名配件 |
.confirmAuthorization | 在设置前显示授权确认 |
.finishInApp | 表示配对后将在应用中继续设置 |
产品图片
选择器会在 180x120 点的容器中显示图片。最佳实践:
- 为所有屏幕缩放系数使用高分辨率图片
- 使用透明背景,以确保在浅色和深色模式下正确显示
- 调整透明边框作为内边距,以控制配件的视觉尺寸
- 在浅色和深色模式下都进行测试
事件处理
事件类型
会话通过事件处理程序传递 ASAccessoryEvent 对象:
| 事件 | 时机 |
|---|---|
.activated | 会话已激活,可查询 session.accessories |
.accessoryAdded | 用户在选择器中选中了配件 |
.accessoryChanged | 配件属性已更新(例如,名称已更改) |
.accessoryRemoved | 配件已从系统中移除 |
.invalidated | 会话已失效,需创建新会话 |
.migrationComplete | 旧版配件迁移已完成 |
.pickerDidPresent | 选择器已显示在屏幕上 |
.pickerDidDismiss | 选择器已关闭 |
.pickerSetupBridging | 正在设置传输桥接 |
.pickerSetupPairing | 正在进行蓝牙配对 |
.pickerSetupFailed | 设置失败 |
.pickerSetupRename | 用户正在重命名配件 |
.accessoryDiscovered | 找到新配件(自定义筛选模式) |
协调选择器关闭
用户选择配件时,.accessoryAdded 会先于 .pickerDidDismiss 触发。若要在选择器关闭后显示自定义设置界面,请在第一个事件中保存配件,并在选择器关闭后处理:
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 }
// Create CBCentralManager — no Bluetooth permission prompt appears
let centralManager = CBCentralManager(delegate: self, queue: nil)
// After poweredOn, retrieve the peripheral
let peripherals = centralManager.retrievePeripherals(
withIdentifiers: [btIdentifier]
)
guard let peripheral = peripherals.first else { return }
centralManager.connect(peripheral, options: nil)
}
要点:
- 仅当应用拥有已配对配件时,
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 join failed: \(error.localizedDescription)")
}
}
}
由于该配件是通过 AccessorySetupKit 发现的,加入网络时不会触发标准的 Wi-Fi 权限提示。
从 CoreBluetooth 迁移
拥有现有 CoreBluetooth 授权配件的应用,可以使用 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
)
// Set the peripheral identifier from CoreBluetooth
migrationItem.peripheralIdentifier = existingPeripheralUUID
// For Wi-Fi accessories:
// migrationItem.hotspotSSID = "MyAccessory-WiFi"
session.showPicker(for: [migrationItem]) { error in
if let error {
print("Migration failed: \(error.localizedDescription)")
}
}
}
迁移规则:
- 如果
showPicker仅包含迁移项,系统会显示信息页面,而不是发现选择器 - 如果迁移项与常规显示项混合,只有在发现并设置新配件时才会进行迁移
- 迁移完成前不要初始化
CBCentralManager,否则会导致错误,且选择器无法显示 - 迁移完成后,会话会收到
.migrationComplete
常见错误
| 错误 | 修正方法 |
|---|---|
| Info.plist 中缺少描述符标识符 | 显示选择器前,声明所有蓝牙服务、名称和公司标识符。 |
同时设置了 ssid 和 ssidPrefix | 选择一种匹配策略。 |
| CoreBluetooth 在迁移完成前启动 | 等待 .migrationComplete,然后创建 CBCentralManager。 |
| 选择器在没有明确用户意图时显示 | 仅在用户执行操作时显示。 |
| 重用了已失效的会话 | 创建、激活并保留新的 ASAccessorySession。 |
审查清单
- 在 Info.plist 中添加
NSAccessorySetupSupports,并设置Bluetooth和/或WiFi - 调用
showPicker前激活会话 - 事件处理程序使用
[weak self],避免循环引用 - 处理所有
ASAccessoryEventType情况,包括@unknown default - 产品图片使用透明背景和适当的分辨率
- 使用
ASAccessory中的bluetoothIdentifier或ssid在设置后连接 - 处理配件移除事件,以清理应用状态