Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
90
90%
Does it follow best practices?
Impact
—
Average score across 248 eval scenarios
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 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)")
}
}
}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)
}Run placement updates from the app extension after the container app has
registered the .placement capability.
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)")
}
}
}Run connected-source updates from the app extension after registration.
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 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 }
}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)
}
}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)")
}
}
}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)
}
}
}
}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
}
}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"
}
}
}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)
}
}
}
}
}.tessl-plugin
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
references
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