CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

accessorysetupkit-patterns.mdskills/accessorysetupkit/references/

AccessorySetupKit Patterns

Extended patterns and recipes for AccessorySetupKit. Covers custom filtering, multiple accessory types, removal handling, accessory images, batch setup, error handling, authorization management, and picker display settings.

Contents

  • Custom Filtering
  • Multiple Accessory Types
  • Removal and Lifecycle Management
  • Accessory Images
  • Picker Display Settings
  • Authorization Management
  • Error Handling
  • Full Manager Pattern
  • SwiftUI Integration

Custom Filtering

The default picker shows all matching accessories automatically. For apps that need to inspect over-the-air data before showing an accessory (e.g., verifying authenticity, checking pairing mode), use custom filtering with ASPickerDisplaySettings.

Enabling Filtered Discovery

import AccessorySetupKit

let settings = ASPickerDisplaySettings.default
settings.options.insert(.filterDiscoveryResults)
settings.discoveryTimeout = .unbounded  // No time limit for filtering
session.pickerDisplaySettings = settings

Processing Discovered Accessories

When filtering is enabled, the session delivers .accessoryDiscovered events instead of automatically populating the picker. Inspect each accessory and decide whether to display it:

session.activate(on: .main) { [weak self] event in
    guard let self else { return }

    switch event.eventType {
    case .accessoryDiscovered:
        guard let discovered = event.accessory as? ASDiscoveredAccessory else { return }
        processDiscoveredAccessory(discovered)
    case .accessoryAdded:
        guard let accessory = event.accessory else { return }
        handleAccessoryAdded(accessory)
    default:
        break
    }
}

Validating and Displaying Accessories

Use advertisement data and RSSI to validate accessories before adding them to the picker:

func processDiscoveredAccessory(_ discovered: ASDiscoveredAccessory) {
    // Check RSSI for proximity
    guard let rssi = discovered.bluetoothRSSI, rssi > -60 else { return }

    // Inspect manufacturer data for authenticity
    guard let advertisementData = discovered.bluetoothAdvertisementData,
          isAuthentic(advertisementData) else { return }

    // Create a customized display item for this specific accessory
    let displayItem = ASDiscoveredDisplayItem(
        name: extractProductName(from: advertisementData),
        productImage: loadImageForModel(advertisementData),
        accessory: discovered
    )

    session.updatePicker(showing: [displayItem]) { error in
        if let error {
            print("Failed to update picker: \(error)")
        }
    }
}

private func isAuthentic(_ advertisementData: [AnyHashable: Any]?) -> Bool {
    guard let data = advertisementData,
          let manufacturerData = data["kCBAdvDataManufacturerData"] as? Data else {
        return false
    }
    // Verify manufacturer-specific authentication bytes
    return manufacturerData.count >= 4
}

Finishing Discovery Early

If the app completes filtering before the timeout, or if no accessories pass validation, end discovery explicitly:

func finishFilteredDiscovery() {
    session.finishPickerDiscovery { error in
        if let error {
            print("Finish discovery error: \(error)")
        }
        // If no items were added, the picker shows a timeout message
    }
}

Retry After Timeout

If the expected accessory was not found, wait for picker dismissal and try again. Suggest the user verify the accessory is powered on and nearby:

case .pickerDidDismiss:
    if !accessoryFound {
        // Prompt user to check that the accessory is powered on
        showRetryPrompt()
    }

Multiple Accessory Types

Apps supporting multiple accessory models create separate display items with distinct descriptors for each model. The picker shows all matching devices in a scrollable carousel.

Distinct Models with Different Service UUIDs

func showMultiModelPicker() {
    let sensorDescriptor = ASDiscoveryDescriptor()
    sensorDescriptor.bluetoothServiceUUID = CBUUID(string: "AAAA1111-...")
    sensorDescriptor.bluetoothNameSubstring = "TempSensor"

    let hubDescriptor = ASDiscoveryDescriptor()
    hubDescriptor.bluetoothServiceUUID = CBUUID(string: "BBBB2222-...")
    hubDescriptor.bluetoothNameSubstring = "SmartHub"

    let sensorItem = ASPickerDisplayItem(
        name: "Temperature Sensor",
        productImage: UIImage(named: "sensor-image")!,
        descriptor: sensorDescriptor
    )

    let hubItem = ASPickerDisplayItem(
        name: "Smart Hub",
        productImage: UIImage(named: "hub-image")!,
        descriptor: hubDescriptor
    )

    session.showPicker(for: [sensorItem, hubItem]) { error in
        if let error {
            print("Picker error: \(error)")
        }
    }
}

Mixed Bluetooth and Wi-Fi Accessories

A single descriptor can specify both Bluetooth and Wi-Fi properties. The system grants access to both interfaces with one tap:

var descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothServiceUUID = CBUUID(string: "AAAA1111-...")
descriptor.ssid = "MyAccessory-WiFi"
descriptor.supportedOptions = [.bluetoothPairingLE]

Using Company Identifiers

When multiple accessories share a company identifier but differ by manufacturer data:

var modelA = ASDiscoveryDescriptor()
modelA.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0x1234)
modelA.bluetoothManufacturerDataBlob = Data([0x01, 0x00])
modelA.bluetoothManufacturerDataMask = Data([0xFF, 0x00])

var modelB = ASDiscoveryDescriptor()
modelB.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0x1234)
modelB.bluetoothManufacturerDataBlob = Data([0x02, 0x00])
modelB.bluetoothManufacturerDataMask = Data([0xFF, 0x00])

The blob and mask must be the same length. The system performs a bitwise AND with the mask on the scanned data and compares to the blob.

Service Data Matching

Match accessories by service data instead of manufacturer data:

var descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothServiceUUID = CBUUID(string: "AAAA1111-...")
descriptor.bluetoothServiceDataBlob = Data([0xAB, 0xCD])
descriptor.bluetoothServiceDataMask = Data([0xFF, 0xFF])

Removal and Lifecycle Management

Removing an Accessory Programmatically

func removeAccessory(_ accessory: ASAccessory) {
    session.removeAccessory(accessory) { error in
        if let error {
            print("Removal failed: \(error)")
        }
    }
}

After removal, the session fires .accessoryRemoved. Clean up any CoreBluetooth or NetworkExtension state associated with the accessory.

Handling External Removal

Users can remove accessories from Settings > Privacy & Security > Accessories. Handle this in the event handler:

case .accessoryRemoved:
    guard let accessory = event.accessory else { return }
    disconnectAndCleanUp(accessory)

Renaming Accessories

Rename an accessory programmatically:

func renameAccessory(_ accessory: ASAccessory) {
    session.renameAccessory(accessory, options: []) { error in
        if let error {
            print("Rename failed: \(error)")
        }
    }
}

For Wi-Fi accessories, pass .ssid in rename options to also update the SSID:

session.renameAccessory(accessory, options: .ssid) { error in
    // SSID updated along with display name
}

After renaming, the session fires .accessoryChanged.

Session Invalidation

An invalidated session cannot be reused. Create a new session when needed:

case .invalidated:
    // Clean up references
    currentSession = nil
    // Create a new session if the app needs to continue
    if shouldRestart {
        currentSession = ASAccessorySession()
        currentSession?.activate(on: .main, eventHandler: handleEvent)
    }

Accessory Images

The picker displays product images in a 180x120 point container. The system scales the image to fit.

Image Requirements

  • Use PNG with transparent background
  • Provide sufficient resolution for 3x displays (540x360 pixels minimum)
  • Test in both light mode and dark mode
  • Use transparent borders around the product to control apparent size

Dynamic Image Loading

When using custom filtering, load images dynamically based on the discovered accessory's advertisement data:

func loadImageForModel(_ advertisementData: [AnyHashable: Any]?) -> UIImage {
    guard let data = advertisementData,
          let modelByte = (data["kCBAdvDataManufacturerData"] as? Data)?.first else {
        return UIImage(named: "generic-accessory")!
    }

    switch modelByte {
    case 0x01: return UIImage(named: "model-a")!
    case 0x02: return UIImage(named: "model-b")!
    default: return UIImage(named: "generic-accessory")!
    }
}

Picker Display Settings

Discovery Timeout

Control how long the picker searches before timing out:

let settings = ASPickerDisplaySettings.default
settings.discoveryTimeout = .medium  // Moderate search duration
session.pickerDisplaySettings = settings
TimeoutUse Case
.shortAccessory expected to be immediately nearby
.mediumStandard discovery
.longAccessory may take time to become discoverable
.unboundedCustom filtering, no automatic timeout

Custom timeout values use TimeInterval:

settings.discoveryTimeout = ASPickerDisplaySettings.DiscoveryTimeout(rawValue: 30.0)

Combining Settings

let settings = ASPickerDisplaySettings.default
settings.discoveryTimeout = .unbounded
settings.options.insert(.filterDiscoveryResults)
session.pickerDisplaySettings = settings

Authorization Management

Authorization States

Each accessory tracks its authorization state:

StateMeaning
.unauthorizedNot authorized for use
.awaitingAuthorizationAuthorization in progress
.authorizedFully authorized, ready to use

Finishing Authorization

For accessories using .finishInApp setup option, complete authorization after the picker closes:

func finishSetup(for accessory: ASAccessory) {
    let settings = ASAccessorySettings.default
    session.finishAuthorization(for: accessory, settings: settings) { error in
        if let error {
            print("Authorization failed: \(error)")
        }
    }
}

Failing Authorization

If app-side validation fails after the picker closes, reject the accessory:

func rejectAccessory(_ accessory: ASAccessory) {
    session.failAuthorization(for: accessory) { error in
        if let error {
            print("Fail authorization error: \(error)")
        }
    }
}

Updating Authorization

Update the discovery descriptor for an already-authorized accessory:

func updateAccessoryDescriptor(_ accessory: ASAccessory) {
    var updatedDescriptor = ASDiscoveryDescriptor()
    updatedDescriptor.bluetoothServiceUUID = CBUUID(string: "NEW-UUID-...")

    session.updateAuthorization(
        for: accessory,
        descriptor: updatedDescriptor
    ) { error in
        if let error {
            print("Update failed: \(error)")
        }
    }
}

Transport Bridging

For accessories that support transport bridging between Bluetooth and Wi-Fi, configure the bridging identifier in accessory settings:

let settings = ASAccessorySettings.default
settings.bluetoothTransportBridgingIdentifier = bridgingData

session.finishAuthorization(for: accessory, settings: settings) { error in
    if let error {
        print("Bridging setup failed: \(error)")
    }
}

Error Handling

ASError Codes

ErrorMeaningRecovery
.activationFailedSession activation failedRetry activation
.invalidatedSession was invalidatedCreate a new session
.invalidRequestInvalid picker/descriptor configurationCheck descriptor and plist values
.extensionNotFoundRequired system extension missingVerify OS version and entitlements
.pickerRestrictedPicker restricted by MDM or parental controlsInform the user
.pickerAlreadyActiveAnother picker is already showingWait for current picker to dismiss
.userCancelledUser dismissed the pickerNo action needed
.userRestrictedUser restricted from adding accessoriesInform the user
.connectionFailedFailed to connect to selected accessoryPrompt retry
.discoveryTimeoutDiscovery timed out without resultsSuggest user check accessory power

Robust Error Handling

session.showPicker(for: items) { error in
    guard let error = error as? ASError else { return }

    switch error.code {
    case .userCancelled:
        // Normal dismissal, no action needed
        break
    case .pickerAlreadyActive:
        // Wait for current picker to finish
        break
    case .discoveryTimeout:
        self.showRetryPrompt(
            message: "Accessory not found. Check that it's powered on and nearby."
        )
    case .connectionFailed:
        self.showRetryPrompt(
            message: "Connection failed. Move closer and try again."
        )
    case .invalidRequest:
        // Developer error — check descriptor and Info.plist configuration
        assertionFailure("Invalid AccessorySetupKit request")
    default:
        self.showErrorAlert(error)
    }
}

Handling Picker Events for Error States

case .pickerSetupFailed:
    if let error = event.error {
        handleSetupFailure(error)
    }

Full Manager Pattern

A complete AccessoryManager class handling the full lifecycle:

import AccessorySetupKit
import CoreBluetooth
import UIKit

@Observable
final class AccessoryManager {
    private(set) var pairedAccessories: [ASAccessory] = []
    private(set) var isPickerPresented = false

    private var session = ASAccessorySession()
    private var pendingAccessory: ASAccessory?
    private var centralManager: CBCentralManager?
    private var onAccessoryReady: ((ASAccessory) -> Void)?

    func activate() {
        session.activate(on: .main) { [weak self] event in
            self?.handleEvent(event)
        }
    }

    func showPicker(
        items: [ASPickerDisplayItem],
        onReady: @escaping (ASAccessory) -> Void
    ) {
        onAccessoryReady = onReady
        session.showPicker(for: items) { error in
            if let error {
                print("Picker error: \(error)")
            }
        }
    }

    func remove(_ accessory: ASAccessory) {
        session.removeAccessory(accessory) { error in
            if let error {
                print("Remove error: \(error)")
            }
        }
    }

    private func handleEvent(_ event: ASAccessoryEvent) {
        switch event.eventType {
        case .activated:
            pairedAccessories = session.accessories
        case .accessoryAdded:
            pendingAccessory = event.accessory
            if let accessory = event.accessory {
                pairedAccessories.append(accessory)
            }
        case .accessoryRemoved:
            if let accessory = event.accessory {
                pairedAccessories.removeAll { $0 == accessory }
            }
        case .accessoryChanged:
            if let accessory = event.accessory,
               let index = pairedAccessories.firstIndex(of: accessory) {
                pairedAccessories[index] = accessory
            }
        case .pickerDidPresent:
            isPickerPresented = true
        case .pickerDidDismiss:
            isPickerPresented = false
            if let accessory = pendingAccessory {
                pendingAccessory = nil
                onAccessoryReady?(accessory)
            }
        case .invalidated:
            session = ASAccessorySession()
            activate()
        default:
            break
        }
    }
}

SwiftUI Integration

AccessorySetupKit in a SwiftUI View

import SwiftUI
import AccessorySetupKit
import CoreBluetooth

struct AccessorySetupView: View {
    @State private var manager = AccessoryManager()
    @State private var showError = false
    @State private var errorMessage = ""

    var body: some View {
        List {
            Section("Paired Accessories") {
                ForEach(manager.pairedAccessories, id: \.displayName) { accessory in
                    HStack {
                        Text(accessory.displayName)
                        Spacer()
                        Text(accessory.state == .authorized ? "Connected" : "Pending")
                            .foregroundStyle(.secondary)
                    }
                }
            }

            Section {
                Button("Add Accessory") {
                    addAccessory()
                }
                .disabled(manager.isPickerPresented)
            }
        }
        .onAppear {
            manager.activate()
        }
        .alert("Error", isPresented: $showError) {
            Button("OK") { }
        } message: {
            Text(errorMessage)
        }
    }

    private func addAccessory() {
        var descriptor = ASDiscoveryDescriptor()
        descriptor.bluetoothServiceUUID = CBUUID(string: "ABCD1234-...")

        guard let image = UIImage(named: "my-accessory") else { return }

        let item = ASPickerDisplayItem(
            name: "My Accessory",
            productImage: image,
            descriptor: descriptor
        )

        manager.showPicker(items: [item]) { accessory in
            print("Accessory ready: \(accessory.displayName)")
        }
    }
}

Tracking Picker State

Disable UI elements while the picker is active to prevent double presentation:

Button("Add Accessory") {
    addAccessory()
}
.disabled(manager.isPickerPresented)

The picker runs in a separate process and occludes part of the app. Avoid making UI updates that would not be visible while the picker is shown. Use .pickerDidPresent and .pickerDidDismiss events to track visibility.

skills

accessorysetupkit

references

accessorysetupkit-patterns.md

SKILL.md

CHANGELOG.md

README.md

tile.json