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 AccessorySetupKit. Covers custom filtering, multiple accessory types, removal handling, accessory images, batch setup, error handling, authorization management, and picker display settings.
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.
import AccessorySetupKit
let settings = ASPickerDisplaySettings.default
settings.options.insert(.filterDiscoveryResults)
settings.discoveryTimeout = .unbounded // No time limit for filtering
session.pickerDisplaySettings = settingsWhen 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
}
}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
}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
}
}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()
}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.
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)")
}
}
}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]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.
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])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.
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)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.
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)
}The picker displays product images in a 180x120 point container. The system scales the image to fit.
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")!
}
}Control how long the picker searches before timing out:
let settings = ASPickerDisplaySettings.default
settings.discoveryTimeout = .medium // Moderate search duration
session.pickerDisplaySettings = settings| Timeout | Use Case |
|---|---|
.short | Accessory expected to be immediately nearby |
.medium | Standard discovery |
.long | Accessory may take time to become discoverable |
.unbounded | Custom filtering, no automatic timeout |
Custom timeout values use TimeInterval:
settings.discoveryTimeout = ASPickerDisplaySettings.DiscoveryTimeout(rawValue: 30.0)let settings = ASPickerDisplaySettings.default
settings.discoveryTimeout = .unbounded
settings.options.insert(.filterDiscoveryResults)
session.pickerDisplaySettings = settingsEach accessory tracks its authorization state:
| State | Meaning |
|---|---|
.unauthorized | Not authorized for use |
.awaitingAuthorization | Authorization in progress |
.authorized | Fully authorized, ready to use |
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)")
}
}
}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)")
}
}
}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)")
}
}
}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 | Meaning | Recovery |
|---|---|---|
.activationFailed | Session activation failed | Retry activation |
.invalidated | Session was invalidated | Create a new session |
.invalidRequest | Invalid picker/descriptor configuration | Check descriptor and plist values |
.extensionNotFound | Required system extension missing | Verify OS version and entitlements |
.pickerRestricted | Picker restricted by MDM or parental controls | Inform the user |
.pickerAlreadyActive | Another picker is already showing | Wait for current picker to dismiss |
.userCancelled | User dismissed the picker | No action needed |
.userRestricted | User restricted from adding accessories | Inform the user |
.connectionFailed | Failed to connect to selected accessory | Prompt retry |
.discoveryTimeout | Discovery timed out without results | Suggest user check accessory power |
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)
}
}case .pickerSetupFailed:
if let error = event.error {
handleSetupFailure(error)
}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
}
}
}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)")
}
}
}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
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