WWDC Quick Look 💓 By SwiftGGTeam
Meet AccessorySetupKit

Meet AccessorySetupKit

Watch original video

Highlight

Accessory setup has long been a pain point. The old flow required Bluetooth permission, possibly joining the accessory’s Wi-Fi hotspot, then Bluetooth pairing—each step adding friction. iOS 18’s AccessorySetupKit compresses all of that into a single system picker.


Core Content

How many steps to pair a Bluetooth accessory? Before iOS 17: request Bluetooth permission, wait for the system dialog, user grants access, scan for devices, possibly join a Wi-Fi hotspot separately, then start Bluetooth pairing—each step can lose users. Apple’s own research shows friction during first-time accessory setup is the top reason users abandon the process.

iOS 18’s AccessorySetupKit compresses the flow to one tap. Your app calls showPicker(); the system scans nearby accessories in a separate process, displays them in the picker with your product image and name, and the user taps to complete pairing. Bluetooth and Wi-Fi access are granted in that single tap—no separate permission dialogs.

Privacy design is central. The picker runs in a separate process; your app sends only discovery rules and display assets, never touching Bluetooth scanning directly. The picker shows both your product name/image and the accessory’s broadcast hardware name so users can verify authenticity. After pairing, communication still uses standard CoreBluetooth and NetworkExtension APIs—no new communication model to learn.

Detailed Content

First, declare supported technologies and discovery rules in Info.plist. Add Bluetooth (or Wi-Fi) to the AccessorySetupKit - Supports array, then fill in the GATT service UUID your accessory advertises under Bluetooth Services (04:12).

The core API is ASAccessorySession. Create and activate the session, register an event callback (06:02):

import AccessorySetupKit

// Create a session
var session = ASAccessorySession()

// Activate session with event handler
session.activate(on: DispatchQueue.main, eventHandler: handleSessionEvent(event:))

// Handle event
func handleSessionEvent(event: ASAccessoryEvent) {
    switch event.eventType {
    case .activated:
        print("Session is activated and ready to use")
        print(session.accessories)
    default:
        print("Received event type \(event.eventType)")
    }
}

Key points:

  • ASAccessorySession is the central object for showing the picker, receiving events, and managing accessories
  • activate(on:eventHandler:) requires a DispatchQueue and event callback
  • The .activated event means the session is ready; you can read existing session.accessories or start a new pairing flow

Next, create ASPickerDisplayItem for each accessory variant with name, image, and discovery rules (07:23):

// Create descriptor for pink dice
let pinkDescriptor = ASDiscoveryDescriptor()
pinkDescriptor.bluetoothServiceUUID = pinkUUID
// Create descriptor for blue dice
let blueDescriptor = ASDiscoveryDescriptor()
blueDescriptor.bluetoothServiceUUID = blueUUID

// Create picker display items
let pinkDisplayItem = ASPickerDisplayItem(
    name: "Pink Dice",
    productImage: UIImage(named: "pink")!,
    descriptor: pinkDescriptor
)
let blueDisplayItem = ASPickerDisplayItem(
    name: "Blue Dice",
    productImage: UIImage(named: "blue")!,
    descriptor: blueDescriptor
)

Key points:

  • ASDiscoveryDescriptor combines discovery rules; the system matches all rules in a descriptor to filter target accessories
  • If an accessory supports both Bluetooth and Wi-Fi, configure both in one descriptor—one tap grants both
  • Different variants (e.g., colors) need separate descriptors and display items

Call showPicker() to present the accessory picker (08:10):

// Invoke accessory picker
Button {
    session.showPicker(for: [pinkDisplayItem, blueDisplayItem]) { error in
        if let error {
            // Handle error
        }
    }
} label: {
    Text("Add Dice")
}

Key points:

  • showPicker(for:) accepts an array of display items; the picker scans for all matching descriptors
  • The picker runs in a separate process—your app never touches Bluetooth scanning directly
  • Handle errors in the callback when the picker can’t be shown (e.g., system restrictions)

After the user selects an accessory, your app receives an accessoryAdded event (09:26):

// Handle event
func handleSessionEvent(event: ASAccessoryEvent) {
    switch event.eventType {

    case .accessoryAdded:
        let newDice: ASAccessory = event.accessory!

    case .accessoryChanged:
        print("Accessory properties changed")

    case .accessoryRemoved:
        print("Accessory removed from system")

    default:
        print("Received event with type: \(event.eventType)")
    }
}

Key points:

  • .accessoryAdded carries an ASAccessory with all paired accessory info
  • .accessoryChanged fires when accessory properties change, e.g., display name edited in Settings
  • .accessoryRemoved fires when the user or app removes an accessory

After pairing, communicate with the accessory via CoreBluetooth (10:22):

// Connect to accessory using CoreBluetooth
let central = CBCentralManager(delegate: self, queue: nil)

func centralManagerDidUpdateState(_ central: CBCentralManager) {
    switch central.state {
    case .poweredOn:
        let peripheral = central.retrievePeripherals(withIdentifiers: [newDice.bluetoothIdentifier]).first
        central.connect(peripheral)
    default:
        print("Received event type \(event.eventType)")
    }
}

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    peripheral.delegate = self
    peripheral.discoverServices(pinkUUID)
}

Key points:

  • Because you declared AccessorySetupKit support in Info.plist, users aren’t prompted for Bluetooth permission
  • CBCentralManager state becomes .poweredOn only when your app has paired accessories
  • Get the CBPeripheral identifier from bluetoothIdentifier on ASAccessory, then use retrievePeripherals(withIdentifiers:)
  • Scan APIs still work but return only accessories paired with your app

For existing accessories, use ASMigrationDisplayItem (11:58):

// Create migration items
let pinkMigration = ASMigrationDisplayItem(name: "Pink Dice", productImage: UIImage(named: "pink")!, descriptor: pinkDescriptor)
pinkMigration.peripheralIdentifier = pinkPeripheral.identifier

// Present picker with migration items
session.showPicker(for: [pinkMigration]) { error in
    if let error {
        // Handle error
    }
}

Key points:

  • ASMigrationDisplayItem is a subclass of ASPickerDisplayItem for migrating existing accessories to AccessorySetupKit management
  • Set peripheralIdentifier to the existing CBPeripheral’s identifier
  • If showPicker contains only migration items, the system shows an info page about migration; if mixed with non-migration items, migration happens only when the user selects and sets up a new accessory

Product image display follows design guidelines. The picker provides a 180×120 pt container; images should have transparent backgrounds for light/dark mode. Transparent border width affects scaling—wider borders make the accessory appear smaller (12:54).

Core Takeaways

  • What to do: Adopt AccessorySetupKit for Bluetooth/Wi-Fi accessory apps and replace the old Bluetooth permission flow. Why it’s worth it: Reduce pairing from multiple steps to one, lower drop-off, and improve privacy—your app no longer needs global Bluetooth permission. How to start: Add AccessorySetupKit - Supports and discovery rules to Info.plist, create ASAccessorySession, configure display items with ASPickerDisplayItem, call showPicker().

  • What to do: Implement migration for shipped accessory apps, moving existing accessories from the old permission model to AccessorySetupKit. Why it’s worth it: Legacy users with Bluetooth permission won’t auto-migrate; without migration you lose fine-grained permission control. How to start: Use ASMigrationDisplayItem with existing peripheral identifiers and show migration in showPicker.

  • What to do: Optimize picker display assets so product images are clear in light and dark mode. Why it’s worth it: Product images are how users identify and verify accessories; poor images hurt completion rates. How to start: Prepare high-resolution images for the 180×120 pt container, transparent backgrounds, adjust border width for scaling, test both modes.

  • What to do: For accessories supporting both Bluetooth and Wi-Fi, configure both interface rules in one discovery descriptor. Why it’s worth it: One tap grants both Bluetooth and Wi-Fi access—no double pairing flow. How to start: Set both bluetoothServiceUUID and Wi-Fi SSID on the same ASDiscoveryDescriptor; the picker handles both technologies.

Comments

GitHub Issues · utterances