Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Extended patterns and recipes for AudioAccessoryKit integration. This file
supplements the main SKILL.md with complete workflows and coordination
strategies.
The full lifecycle from discovery to audio switching registration:
import AccessorySetupKit
import AudioAccessoryKit
import CoreBluetooth
final class AudioAccessoryManager {
private let session = ASAccessorySession()
private var registeredDevice: AccessoryControlDevice?
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:
registeredDevice = nil
default:
break
}
}
private func registerIfNeeded(_ accessory: ASAccessory) async {
// Check if already registered
if let existing = try? AccessoryControlDevice.current(for: accessory) {
registeredDevice = existing
return
}
do {
let capabilities: AccessoryControlDevice.Capabilities = [
.audioSwitching,
.placement
]
try await AccessoryControlDevice.register(accessory, capabilities)
registeredDevice = try AccessoryControlDevice.current(for: accessory)
} catch {
print("Registration failed: \(error)")
}
}
}Provide full initial state immediately after registration:
func registerWithInitialState(
_ accessory: ASAccessory,
placement: AccessoryControlDevice.Placement,
primarySource: Data?
) async throws {
let capabilities: AccessoryControlDevice.Capabilities = [
.audioSwitching,
.placement
]
try await AccessoryControlDevice.register(accessory, capabilities)
let device = try AccessoryControlDevice.current(for: accessory)
var config = device.configuration
config.devicePlacement = placement
config.primaryAudioSourceDeviceIdentifier = primarySource
try await device.update(config)
}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)")
}
}
}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
}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)")
}
}
}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)")
}
}
}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()
}
}Handle transient failures during registration or updates:
func registerWithRetry(
_ accessory: ASAccessory,
capabilities: AccessoryControlDevice.Capabilities,
maxAttempts: Int = 3
) async throws {
var lastError: Error?
for attempt in 0..<maxAttempts {
do {
try await AccessoryControlDevice.register(accessory, capabilities)
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(pow(2.0, Double(attempt)))
try await Task.sleep(for: delay)
@unknown default:
throw error
}
}
}
if let lastError { throw lastError }
}Automatically re-register when the device is invalidated:
func updateWithReregistration(
accessory: ASAccessory,
capabilities: AccessoryControlDevice.Capabilities,
config: AccessoryControlDevice.Configuration
) async throws {
do {
let device = try AccessoryControlDevice.current(for: accessory)
try await device.update(config)
} catch AccessoryControlDevice.Error.invalidated {
// Re-register then apply configuration
try await AccessoryControlDevice.register(accessory, capabilities)
let device = try AccessoryControlDevice.current(for: accessory)
try await device.update(config)
}
}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 capabilities: AccessoryControlDevice.Capabilities = [
.audioSwitching,
.placement
]
do {
try await AccessoryControlDevice.register(accessory, capabilities)
} catch {
print("Audio registration failed: \(error)")
}
}
}On app launch, re-register previously paired accessories that are already authorized:
extension AccessorySetupCoordinator {
func restoreRegistrations() {
for accessory in session.accessories {
Task {
// Check if already registered
if let _ = try? AccessoryControlDevice.current(for: accessory) {
return // Already registered
}
await registerAudioFeatures(accessory)
}
}
}
}Expose accessory state to SwiftUI views using Observation:
import AudioAccessoryKit
import AccessorySetupKit
import Observation
@Observable
final class AudioAccessoryState {
private(set) var isRegistered = false
private(set) var placement: AccessoryControlDevice.Placement?
private(set) var hasPrimarySource = false
private(set) var hasSecondarySource = false
private var accessory: ASAccessory?
func register(_ accessory: ASAccessory) async throws {
let capabilities: AccessoryControlDevice.Capabilities = [
.audioSwitching,
.placement
]
try await AccessoryControlDevice.register(accessory, capabilities)
self.accessory = accessory
isRegistered = true
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
}
}Use the observable state in a SwiftUI view:
import SwiftUI
struct AudioAccessoryView: View {
@State private var state = AudioAccessoryState()
var body: some View {
List {
Section("Status") {
LabeledContent("Registered", value: state.isRegistered ? "Yes" : "No")
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"
}
}
}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
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references