accessorysetupkit✓ Pass
Discover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.
// Install Skill
Install Skill
Skills are third-party code from public GitHub repositories. SkillHub scans for known malicious patterns but cannot guarantee safety. Review the source code before installing.
Install globally (user-level):
npx skillhub install dpearson2699/swift-ios-skills/accessorysetupkitInstall in current project:
npx skillhub install dpearson2699/swift-ios-skills/accessorysetupkit --projectSuggested path: ~/.claude/skills/accessorysetupkit/
SKILL.md Content
---
name: accessorysetupkit
description: "Discover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions."
---
# AccessorySetupKit
Privacy-preserving accessory discovery and setup for Bluetooth and Wi-Fi
devices. Replaces broad Bluetooth/Wi-Fi permission prompts with a
system-provided picker that grants per-accessory access with a single tap.
Available iOS 18+ / Swift 6.3.
After setup, apps continue using CoreBluetooth and NetworkExtension for
communication. AccessorySetupKit handles only the discovery and authorization
step.
## Contents
- [Setup and Entitlements](#setup-and-entitlements)
- [Discovery Descriptors](#discovery-descriptors)
- [Presenting the Picker](#presenting-the-picker)
- [Event Handling](#event-handling)
- [Bluetooth Accessories](#bluetooth-accessories)
- [Wi-Fi Accessories](#wi-fi-accessories)
- [Migration from CoreBluetooth](#migration-from-corebluetooth)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)
## Setup and Entitlements
### Info.plist Configuration
Add these keys to the app's Info.plist:
| Key | Type | Purpose |
|---|---|---|
| `NSAccessorySetupSupports` | `[String]` | Required. Array containing `Bluetooth` and/or `WiFi` |
| `NSAccessorySetupBluetoothServices` | `[String]` | Service UUIDs the app discovers (Bluetooth) |
| `NSAccessorySetupBluetoothNames` | `[String]` | Bluetooth names or substrings to match |
| `NSAccessorySetupBluetoothCompanyIdentifiers` | `[Number]` | Bluetooth company identifiers |
The Bluetooth-specific keys must match the values used in `ASDiscoveryDescriptor`.
If the app uses identifiers, names, or services not declared in Info.plist, the
app crashes at discovery time.
### No Bluetooth Permission Required
When an app declares `NSAccessorySetupSupports` with `Bluetooth`, creating a
`CBCentralManager` no longer triggers the system Bluetooth permission dialog.
The central manager's state transitions to `poweredOn` only when the app has
at least one paired accessory via AccessorySetupKit.
## Discovery Descriptors
`ASDiscoveryDescriptor` defines the matching criteria for finding accessories.
The system matches scanned results against all rules in the descriptor to
filter for the target accessory.
### Bluetooth Descriptor
```swift
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
```
A Bluetooth descriptor requires `bluetoothCompanyIdentifier` or
`bluetoothServiceUUID`, plus at least one of:
- `bluetoothNameSubstring`
- `bluetoothManufacturerDataBlob` and `bluetoothManufacturerDataMask` (same length)
- `bluetoothServiceDataBlob` and `bluetoothServiceDataMask` (same length)
### Wi-Fi Descriptor
```swift
var descriptor = ASDiscoveryDescriptor()
descriptor.ssid = "MyAccessory-Network"
// OR use a prefix:
// descriptor.ssidPrefix = "MyAccessory-"
```
Supply either `ssid` or `ssidPrefix`, not both. The app crashes if both are set.
The `ssidPrefix` must have a non-zero length.
### Bluetooth Range
Control the physical proximity required for discovery:
| Value | Behavior |
|---|---|
| `.default` | Standard Bluetooth range |
| `.immediate` | Only accessories in close physical proximity |
### Support Options
Set `supportedOptions` on the descriptor to declare the accessory's capabilities:
```swift
descriptor.supportedOptions = [.bluetoothPairingLE, .bluetoothTransportBridging]
```
| Option | Purpose |
|---|---|
| `.bluetoothPairingLE` | BLE pairing support |
| `.bluetoothTransportBridging` | Bluetooth transport bridging |
| `.bluetoothHID` | Bluetooth HID device |
## Presenting the Picker
### Creating the Session
Create and activate an `ASAccessorySession` to manage discovery lifecycle:
```swift
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
default:
break
}
}
}
```
### Showing the Picker
Create `ASPickerDisplayItem` instances with a name, product image, and
discovery descriptor, then pass them to the session:
```swift
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)")
}
}
}
```
The picker runs in a separate system process. It shows each matching device
as a separate item. When multiple devices match a given descriptor, the picker
creates a horizontal carousel.
### Setup Options
Configure picker behavior per display item:
```swift
var item = ASPickerDisplayItem(
name: "My Accessory",
productImage: image,
descriptor: descriptor
)
item.setupOptions = [.rename, .confirmAuthorization]
```
| Option | Effect |
|---|---|
| `.rename` | Allow renaming the accessory during setup |
| `.confirmAuthorization` | Show authorization confirmation before setup |
| `.finishInApp` | Signal that setup continues in the app after pairing |
### Product Images
The picker displays images in a 180x120 point container. Best practices:
- Use high-resolution images for all screen scale factors
- Use transparent backgrounds for correct light/dark mode appearance
- Adjust transparent borders as padding to control apparent accessory size
- Test in both light and dark mode
## Event Handling
### Event Types
The session delivers `ASAccessoryEvent` objects through the event handler:
| Event | When |
|---|---|
| `.activated` | Session is active, query `session.accessories` |
| `.accessoryAdded` | User selected an accessory in the picker |
| `.accessoryChanged` | Accessory properties updated (e.g., renamed) |
| `.accessoryRemoved` | Accessory removed from system |
| `.invalidated` | Session invalidated, create a new one |
| `.migrationComplete` | Migration of legacy accessories completed |
| `.pickerDidPresent` | Picker appeared on screen |
| `.pickerDidDismiss` | Picker dismissed |
| `.pickerSetupBridging` | Transport bridging setup in progress |
| `.pickerSetupPairing` | Bluetooth pairing in progress |
| `.pickerSetupFailed` | Setup failed |
| `.pickerSetupRename` | User is renaming the accessory |
| `.accessoryDiscovered` | New accessory found (custom filtering mode) |
### Coordinating Picker Dismissal
When the user selects an accessory, `.accessoryAdded` fires before
`.pickerDidDismiss`. To show custom setup UI after the picker closes, store the
accessory on the first event and act on it after dismissal:
```swift
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)
}
default:
break
}
}
```
## Bluetooth Accessories
After an accessory is added via the picker, use CoreBluetooth to communicate.
The `bluetoothIdentifier` on the `ASAccessory` maps to a `CBPeripheral`.
```swift
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)
}
```
Key points:
- `CBCentralManager` state reaches `.poweredOn` only when the app has paired accessories
- Scanning with `scanForPeripherals(withServices:)` returns only
accessories paired through AccessorySetupKit
- No `NSBluetoothAlwaysUsageDescription` is needed when using AccessorySetupKit
exclusively
## Wi-Fi Accessories
For Wi-Fi accessories, the `ssid` on the `ASAccessory` identifies the network.
Use `NEHotspotConfiguration` from NetworkExtension to join it:
```swift
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)")
}
}
}
```
Because the accessory was discovered through AccessorySetupKit, joining the
network does not trigger the standard Wi-Fi access prompt.
## Migration from CoreBluetooth
Apps with existing CoreBluetooth-authorized accessories can migrate them to
AccessorySetupKit using `ASMigrationDisplayItem`. This is a one-time operation
that registers known accessories in the new system.
```swift
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)")
}
}
}
```
Migration rules:
- If `showPicker` contains only migration items, the system shows an
informational page instead of a discovery picker
- If migration items are mixed with regular display items, migration happens
only when a new accessory is discovered and set up
- Do not initialize `CBCentralManager` before migration completes — doing so
causes an error and the picker fails to appear
- The session receives `.migrationComplete` when migration finishes
## Common Mistakes
### DON'T: Omit Info.plist keys for Bluetooth discovery
The app crashes if it uses identifiers, names, or services in descriptors that
are not declared in Info.plist.
```swift
// WRONG — service UUID not in NSAccessorySetupBluetoothServices
var descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothServiceUUID = CBUUID(string: "UNDECLARED-UUID")
session.showPicker(for: [item]) { _ in } // Crash
// CORRECT — declare all UUIDs in Info.plist first
// Info.plist: NSAccessorySetupBluetoothServices = ["ABCD1234-..."]
var descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothServiceUUID = CBUUID(string: "ABCD1234-...")
```
### DON'T: Set both ssid and ssidPrefix
```swift
// WRONG — crashes at runtime
var descriptor = ASDiscoveryDescriptor()
descriptor.ssid = "MyNetwork"
descriptor.ssidPrefix = "My" // Cannot set both
// CORRECT — use one or the other
var descriptor = ASDiscoveryDescriptor()
descriptor.ssid = "MyNetwork"
```
### DON'T: Initialize CBCentralManager before migration
```swift
// WRONG — migration fails, picker does not appear
let central = CBCentralManager(delegate: self, queue: nil)
session.showPicker(for: [migrationItem]) { error in
// error is non-nil
}
// CORRECT — wait for .migrationComplete before using CoreBluetooth
session.activate(on: .main) { event in
if event.eventType == .migrationComplete {
let central = CBCentralManager(delegate: self, queue: nil)
}
}
```
### DON'T: Show the picker without user intent
```swift
// WRONG — picker appears unexpectedly on app launch
override func viewDidLoad() {
super.viewDidLoad()
session.showPicker(for: items) { _ in }
}
// CORRECT — bind picker to a user action
@IBAction func addAccessoryTapped(_ sender: UIButton) {
session.showPicker(for: items) { _ in }
}
```
### DON'T: Reuse an invalidated session
```swift
// WRONG — session is dead after invalidation
session.showPicker(for: items) { _ in } // No effect
// CORRECT — create a new session
let newSession = ASAccessorySession()
newSession.activate(on: .main) { event in
// Handle events
}
```
## Review Checklist
- [ ] `NSAccessorySetupSupports` added to Info.plist with `Bluetooth` and/or `WiFi`
- [ ] Bluetooth-specific plist keys (`NSAccessorySetupBluetoothServices`, `NSAccessorySetupBluetoothNames`, `NSAccessorySetupBluetoothCompanyIdentifiers`) match descriptor values
- [ ] Session activated before calling `showPicker`
- [ ] Event handler uses `[weak self]` to avoid retain cycles
- [ ] All `ASAccessoryEventType` cases handled, including `@unknown default`
- [ ] Product images use transparent backgrounds and appropriate resolution
- [ ] `ssid` and `ssidPrefix` are never set simultaneously on a descriptor
- [ ] Picker presentation tied to explicit user action, not automatic
- [ ] `CBCentralManager` not initialized until after migration completes (if migrating)
- [ ] `bluetoothIdentifier` or `ssid` from `ASAccessory` used to connect post-setup
- [ ] Invalidated sessions replaced with new instances
- [ ] Accessory removal events handled to clean up app state
## References
- Extended patterns (custom filtering, batch setup, removal handling, error recovery): [references/accessorysetupkit-patterns.md](references/accessorysetupkit-patterns.md)
- [AccessorySetupKit framework](https://sosumi.ai/documentation/accessorysetupkit)
- [ASAccessorySession](https://sosumi.ai/documentation/accessorysetupkit/asaccessorysession)
- [ASDiscoveryDescriptor](https://sosumi.ai/documentation/accessorysetupkit/asdiscoverydescriptor)
- [ASPickerDisplayItem](https://sosumi.ai/documentation/accessorysetupkit/aspickerdisplayitem)
- [ASAccessory](https://sosumi.ai/documentation/accessorysetupkit/asaccessory)
- [ASAccessoryEvent](https://sosumi.ai/documentation/accessorysetupkit/asaccessoryevent)
- [ASMigrationDisplayItem](https://sosumi.ai/documentation/accessorysetupkit/asmigrationdisplayitem)
- [Discovering and configuring accessories](https://sosumi.ai/documentation/accessorysetupkit/discovering-and-configuring-accessories)
- [Setting up and authorizing a Bluetooth accessory](https://sosumi.ai/documentation/accessorysetupkit/setting-up-and-authorizing-a-bluetooth-accessory)
- [Meet AccessorySetupKit — WWDC24](https://sosumi.ai/videos/play/wwdc2024/10203/)
// Install Skill
Install Skill
Skills are third-party code from public GitHub repositories. SkillHub scans for known malicious patterns but cannot guarantee safety. Review the source code before installing.
Install globally (user-level):
npx skillhub install dpearson2699/swift-ios-skills/accessorysetupkitInstall in current project:
npx skillhub install dpearson2699/swift-ios-skills/accessorysetupkit --projectSuggested path: ~/.claude/skills/accessorysetupkit/