CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

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

90

Quality

90%

Does it follow best practices?

Impact

Average score across 248 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

audioaccessorykit-patterns.mdskills/audioaccessorykit/references/

AudioAccessoryKit Patterns

Extended patterns and recipes for AudioAccessoryKit integration. This file supplements the main SKILL.md with complete workflows and coordination strategies.

Contents

  • Complete Registration Flow
  • Placement Monitoring
  • Multi-Device Audio Source Management
  • Error Recovery Patterns
  • AccessorySetupKit Integration
  • Architecture Patterns

Complete Registration Flow

Pairing Through Registration

The container app owns AccessorySetupKit pairing and AudioAccessoryKit registration. Keep app-extension updates in separate types.

import AccessorySetupKit
import AudioAccessoryKit
import CoreBluetooth

final class AudioAccessoryRegistrar {
    private let session = ASAccessorySession()
    private var registeredAccessories = Set<ASAccessory>()

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

    private func handleEvent(_ event: ASAccessoryEvent) {
        switch event.eventType {
        case .activated:
            // Check for previously paired accessories
            for accessory in session.accessories {
                Task { await registerIfNeeded(accessory) }
            }
        case .accessoryAdded:
            guard let accessory = event.accessory else { return }
            Task { await registerIfNeeded(accessory) }
        case .accessoryRemoved:
            if let accessory = event.accessory {
                registeredAccessories.remove(accessory)
            }
        default:
            break
        }
    }

    private func registerIfNeeded(_ accessory: ASAccessory) async {
        guard !registeredAccessories.contains(accessory) else { return }

        do {
            let configuration = AccessoryControlDevice.Configuration(
                devicePlacement: .offHead,
                deviceCapabilities: [.audioSwitching, .placement]
            )
            try await AccessoryControlDevice.register(accessory, configuration)
            registeredAccessories.insert(accessory)
        } catch {
            print("Registration failed: \(error)")
        }
    }
}

Registration with Initial Configuration

Provide full initial state in the registration configuration:

func registerWithInitialState(
    _ accessory: ASAccessory,
    placement: AccessoryControlDevice.Placement,
    primarySource: Data?
) async throws {
    let configuration = AccessoryControlDevice.Configuration(
        devicePlacement: placement,
        deviceCapabilities: [.audioSwitching, .placement],
        primaryAudioSourceDeviceIdentifier: primarySource
    )
    try await AccessoryControlDevice.register(accessory, configuration)
}

Placement Monitoring

Run placement updates from the app extension after the container app has registered the .placement capability.

Placement State Machine

Track and report placement transitions based on sensor data from the accessory:

final class PlacementMonitor {
    private let accessory: ASAccessory
    private var currentPlacement: AccessoryControlDevice.Placement = .offHead

    init(accessory: ASAccessory) {
        self.accessory = accessory
    }

    /// Call when the accessory firmware reports a new wear state.
    func reportPlacementChange(
        _ newPlacement: AccessoryControlDevice.Placement
    ) async {
        guard newPlacement != currentPlacement else { return }

        let previousPlacement = currentPlacement
        currentPlacement = newPlacement

        do {
            let device = try AccessoryControlDevice.current(for: accessory)
            var config = device.configuration
            config.devicePlacement = newPlacement
            try await device.update(config)
        } catch {
            // Revert local state on failure
            currentPlacement = previousPlacement
            print("Placement update failed: \(error)")
        }
    }
}

Mapping Firmware Sensor Data to Placement

Translate raw sensor readings from the accessory into placement values:

extension PlacementMonitor {
    /// Map raw proximity/wear sensor data to an AudioAccessoryKit placement.
    func placementFromSensorData(
        isWorn: Bool,
        sensorType: AccessoryHardwareType
    ) -> AccessoryControlDevice.Placement {
        guard isWorn else { return .offHead }

        switch sensorType {
        case .inEarBud:
            return .inEar
        case .onEarHeadphone:
            return .onHead
        case .overEarHeadphone:
            return .overTheEar
        }
    }
}

enum AccessoryHardwareType {
    case inEarBud
    case onEarHeadphone
    case overEarHeadphone
}

Debounced Placement Updates

Avoid rapid placement toggles from noisy sensor data:

final class DebouncedPlacementMonitor {
    private let accessory: ASAccessory
    private var pendingPlacement: AccessoryControlDevice.Placement?
    private var debounceTask: Task<Void, Never>?

    private let debounceInterval: Duration = .milliseconds(500)

    init(accessory: ASAccessory) {
        self.accessory = accessory
    }

    func reportRawPlacementChange(
        _ newPlacement: AccessoryControlDevice.Placement
    ) {
        pendingPlacement = newPlacement
        debounceTask?.cancel()

        debounceTask = Task { [weak self] in
            try? await Task.sleep(for: self?.debounceInterval ?? .milliseconds(500))
            guard !Task.isCancelled else { return }
            guard let placement = self?.pendingPlacement else { return }
            await self?.commitPlacement(placement)
        }
    }

    private func commitPlacement(
        _ placement: AccessoryControlDevice.Placement
    ) async {
        do {
            let device = try AccessoryControlDevice.current(for: accessory)
            var config = device.configuration
            config.devicePlacement = placement
            try await device.update(config)
        } catch {
            print("Debounced placement update failed: \(error)")
        }
    }
}

Multi-Device Audio Source Management

Run connected-source updates from the app extension after registration.

Tracking Connected Bluetooth Sources

Maintain a list of connected Bluetooth devices and update source identifiers when connections change:

final class AudioSourceTracker {
    private let accessory: ASAccessory
    private var connectedDevices: [Data] = []

    init(accessory: ASAccessory) {
        self.accessory = accessory
    }

    func deviceConnected(bluetoothAddress: Data) async {
        connectedDevices.append(bluetoothAddress)
        await syncSourceIdentifiers()
    }

    func deviceDisconnected(bluetoothAddress: Data) async {
        connectedDevices.removeAll { $0 == bluetoothAddress }
        await syncSourceIdentifiers()
    }

    private func syncSourceIdentifiers() async {
        do {
            let device = try AccessoryControlDevice.current(for: accessory)
            var config = device.configuration

            config.primaryAudioSourceDeviceIdentifier = connectedDevices.first
            config.secondaryAudioSourceDeviceIdentifier = connectedDevices.count > 1
                ? connectedDevices[1]
                : nil

            try await device.update(config)
        } catch {
            print("Source identifier update failed: \(error)")
        }
    }
}

Prioritizing Audio Sources

When multiple devices are connected, choose the primary source based on application-specific logic:

extension AudioSourceTracker {
    func updatePrimarySource(
        to preferredAddress: Data
    ) async {
        // Move preferred device to front
        connectedDevices.removeAll { $0 == preferredAddress }
        connectedDevices.insert(preferredAddress, at: 0)
        await syncSourceIdentifiers()
    }
}

Error Recovery Patterns

Retry with Backoff

Handle transient failures during container-app registration:

func registerWithRetry(
    _ accessory: ASAccessory,
    configuration: AccessoryControlDevice.Configuration,
    maxAttempts: Int = 3
) async throws {
    var lastError: Error?

    for attempt in 0..<maxAttempts {
        do {
            try await AccessoryControlDevice.register(accessory, configuration)
            return
        } catch let error as AccessoryControlDevice.Error {
            lastError = error

            switch error {
            case .accessoryNotCapable:
                // Hardware limitation, do not retry
                throw error
            case .invalidRequest:
                // Bad parameters, do not retry
                throw error
            case .invalidated, .unknown:
                // Potentially transient, retry with backoff
                let delay = Duration.seconds(Int64(1 << attempt))
                try await Task.sleep(for: delay)
            @unknown default:
                throw error
            }
        }
    }

    if let lastError { throw lastError }
}

Invalidation Recovery

When an app-extension update sees invalidation, stop using that device handle and coordinate with the container app to register the accessory again:

enum AudioAccessoryUpdateRecovery {
    case needsContainerRegistration(ASAccessory)
}

func updateOrRequestRegistration(
    accessory: ASAccessory,
    config: AccessoryControlDevice.Configuration
) async throws -> AudioAccessoryUpdateRecovery? {
    do {
        let device = try AccessoryControlDevice.current(for: accessory)
        try await device.update(config)
        return nil
    } catch AccessoryControlDevice.Error.invalidated {
        return .needsContainerRegistration(accessory)
    }
}

AccessorySetupKit Integration

Coordinating Pairing and Audio Registration

Show the AccessorySetupKit picker and register for audio features on successful pairing:

import AccessorySetupKit
import AudioAccessoryKit
import CoreBluetooth

final class AccessorySetupCoordinator {
    private let session = ASAccessorySession()
    private var pendingAccessory: ASAccessory?

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

    func showPicker(descriptor: ASDiscoveryDescriptor, image: UIImage) {
        let item = ASPickerDisplayItem(
            name: "Audio Accessory",
            productImage: image,
            descriptor: descriptor
        )

        session.showPicker(for: [item]) { error in
            if let error {
                print("Picker failed: \(error)")
            }
        }
    }

    private func handleEvent(_ event: ASAccessoryEvent) {
        switch event.eventType {
        case .accessoryAdded:
            guard let accessory = event.accessory else { return }
            pendingAccessory = accessory
        case .pickerDidDismiss:
            guard let accessory = pendingAccessory else { return }
            pendingAccessory = nil
            Task { await registerAudioFeatures(accessory) }
        default:
            break
        }
    }

    private func registerAudioFeatures(_ accessory: ASAccessory) async {
        let configuration = AccessoryControlDevice.Configuration(
            devicePlacement: .offHead,
            deviceCapabilities: [.audioSwitching, .placement]
        )

        do {
            try await AccessoryControlDevice.register(accessory, configuration)
        } catch {
            print("Audio registration failed: \(error)")
        }
    }
}

Handling Previously Paired Accessories

On app launch, re-register previously paired accessories that are already authorized:

extension AccessorySetupCoordinator {
    func restoreRegistrations() {
        for accessory in session.accessories {
            Task {
                await registerAudioFeatures(accessory)
            }
        }
    }
}

Architecture Patterns

Observable Audio Accessory State

Expose app-extension accessory state to SwiftUI views using Observation:

import AudioAccessoryKit
import AccessorySetupKit
import Observation

@Observable
final class AudioAccessoryExtensionState {
    private(set) var placement: AccessoryControlDevice.Placement?
    private(set) var hasPrimarySource = false
    private(set) var hasSecondarySource = false

    private var accessory: ASAccessory?

    func bind(to accessory: ASAccessory) {
        self.accessory = accessory
        refreshState()
    }

    func updatePlacement(
        _ newPlacement: AccessoryControlDevice.Placement
    ) async throws {
        guard let accessory else { return }
        let device = try AccessoryControlDevice.current(for: accessory)
        var config = device.configuration
        config.devicePlacement = newPlacement
        try await device.update(config)
        placement = newPlacement
    }

    private func refreshState() {
        guard let accessory,
              let device = try? AccessoryControlDevice.current(for: accessory)
        else { return }

        let config = device.configuration
        placement = config.devicePlacement
        hasPrimarySource = config.primaryAudioSourceDeviceIdentifier != nil
        hasSecondarySource = config.secondaryAudioSourceDeviceIdentifier != nil
    }
}

SwiftUI Integration

Use the observable state in a SwiftUI view:

import SwiftUI

struct AudioAccessoryView: View {
    @State private var state = AudioAccessoryExtensionState()

    var body: some View {
        List {
            Section("Status") {
                if let placement = state.placement {
                    LabeledContent("Placement", value: placementLabel(placement))
                }
            }

            Section("Connected Sources") {
                LabeledContent("Primary", value: state.hasPrimarySource ? "Connected" : "None")
                LabeledContent("Secondary", value: state.hasSecondarySource ? "Connected" : "None")
            }
        }
    }

    private func placementLabel(
        _ placement: AccessoryControlDevice.Placement
    ) -> String {
        switch placement {
        case .inEar: "In Ear"
        case .onHead: "On Head"
        case .overTheEar: "Over the Ear"
        case .offHead: "Off Head"
        @unknown default: "Unknown"
        }
    }
}

Separating Transport and Audio Concerns

Keep Bluetooth communication (CoreBluetooth) separate from audio configuration (AudioAccessoryKit):

/// Handles Bluetooth communication with the accessory firmware.
final class AccessoryTransport {
    private var peripheral: CBPeripheral?

    func connect(bluetoothIdentifier: UUID, centralManager: CBCentralManager) {
        let peripherals = centralManager.retrievePeripherals(
            withIdentifiers: [bluetoothIdentifier]
        )
        guard let peripheral = peripherals.first else { return }
        self.peripheral = peripheral
        centralManager.connect(peripheral)
    }

    /// Called when firmware reports new sensor data.
    var onPlacementChanged: ((AccessoryControlDevice.Placement) -> Void)?
    var onConnectionStateChanged: ((Data, Bool) -> Void)?
}

/// Coordinates transport events with AudioAccessoryKit registration.
final class AudioAccessoryCoordinator {
    private let transport: AccessoryTransport
    private let placementMonitor: PlacementMonitor
    private let sourceTracker: AudioSourceTracker

    init(accessory: ASAccessory) {
        self.transport = AccessoryTransport()
        self.placementMonitor = PlacementMonitor(accessory: accessory)
        self.sourceTracker = AudioSourceTracker(accessory: accessory)

        transport.onPlacementChanged = { [weak self] placement in
            Task { await self?.placementMonitor.reportPlacementChange(placement) }
        }

        transport.onConnectionStateChanged = { [weak self] address, connected in
            Task {
                if connected {
                    await self?.sourceTracker.deviceConnected(bluetoothAddress: address)
                } else {
                    await self?.sourceTracker.deviceDisconnected(bluetoothAddress: address)
                }
            }
        }
    }
}

skills

audioaccessorykit

README.md

tile.json