accessorysetupkit

accessorysetupkit

热门

使用 AccessorySetupKit 发现与配置蓝牙及 Wi-Fi 配件。适用于展示隐私保护型配件选择器、为 BLE 或 Wi-Fi 设备定义发现描述符、处理配件会话事件、从基于权限的 CoreBluetooth 扫描方案迁移,或在无需全局蓝牙权限的情况下配对设置配件。

967Star
49Fork
更新于 2026/7/31
SKILL.md
只读
名称
accessorysetupkit
描述

使用 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  // 仅限近距离设备

蓝牙描述符至少需要提供 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 时,其长度不能为 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 中声明 声明