●FUNDING — Rork raises a $15M seed led by Left Lane Capital●RORK MAX — Rork Max generates native Swift apps instead of React Native●PLATFORM — It targets iPhone, iPad, Watch, and Vision Pro, reaching Live Activities and Core ML●GROWTH — Traffic keeps climbing at 743K monthly visits and 85% growth●TEST — The Companion app lets you test on a real device without a paid Apple Developer account●STACK — Built on React Native and Expo for true native experiences, not web wrappers●FUNDING — Rork raises a $15M seed led by Left Lane Capital●RORK MAX — Rork Max generates native Swift apps instead of React Native●PLATFORM — It targets iPhone, iPad, Watch, and Vision Pro, reaching Live Activities and Core ML●GROWTH — Traffic keeps climbing at 743K monthly visits and 85% growth●TEST — The Companion app lets you test on a real device without a paid Apple Developer account●STACK — Built on React Native and Expo for true native experiences, not web wrappers
Building a Native Bluetooth Pairing Sheet with AccessorySetupKit
iOS 18's AccessorySetupKit lets you pair a single, specific device through a system sheet without the broad Bluetooth permission prompt. Assuming the native Swift that Rork Max generates, here is the path from declaration to picker to connection, with working code.
Trying to connect a peripheral from my own app, the broad "App Name would like to use Bluetooth" dialog would pop up right at launch — and for a while I buried the connect feature deep in a settings screen just to avoid it. As an indie developer running utility apps on the App Store for years, I know a wide permission request in the first few seconds cools the whole experience down.
iOS 18's AccessorySetupKit changes that premise. Instead of asking for access to all of Bluetooth, your app lets the user pick just the target device inside a system-drawn sheet. Only the chosen device is authorized, and the broad permission dialog never appears. Assuming the native Swift Rork Max generates, we will walk from declaration to picker to connection. (This feature requires a native implementation; plain React Native cannot do it.)
Why you avoid asking for "everything"
Classic CoreBluetooth asks for Bluetooth permission the moment you start scanning — from the user's view, a broad "can this app see nearby devices?" grant. AccessorySetupKit inverts this: it narrows the unit of permission down to this one device.
The user picks the nearby target inside a system sheet. Your app receives the identifier of the chosen device and can connect to that device only. You never ask for a broad scan permission, so the first experience stays intact — which matters most when, like an indie developer, you are building trust from scratch.
The required declarations (Info.plist)
First declare "what kind of device you handle" in Info.plist. Miss this and the picker is silently empty.
<key>NSAccessorySetupKitSupports</key><array> <string>Bluetooth</string></array><key>NSAccessorySetupBluetoothServices</key><array> <!-- List every service UUID your device advertises --> <string>0000FE2C-0000-1000-8000-00805F9B34FB</string></array>
Even in a Rork Max project, add these two keys to the native Info.plist. List every service UUID the target advertises under NSAccessorySetupBluetoothServices; devices whose UUID is missing here never appear in the sheet.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A system picker that surfaces only your specific device, built with ASAccessorySession and ASDiscoveryDescriptor (working Swift)
✦How to avoid the broad Bluetooth permission dialog and still connect via CoreBluetooth without a prompt afterward
✦Info.plist NSAccessorySetupKitSupports / service-UUID declarations and how to handle added / renamed / removed events
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Hold one ASAccessorySession, subscribe to events, then present the picker. ASDiscoveryDescriptor decides "what to show," and ASPickerDisplayItem decides "how it looks."
import AccessorySetupKitimport CoreBluetooth@MainActorfinal class AccessoryManager: ObservableObject { private let session = ASAccessorySession() @Published var current: ASAccessory? func activate() { session.activate(on: .main) { [weak self] event in self?.handle(event) } } func showPicker() { let descriptor = ASDiscoveryDescriptor() descriptor.bluetoothServiceUUID = CBUUID(string: "FE2C") // Narrow by a name substring to keep unrelated nearby devices out descriptor.bluetoothNameSubstring = "MyDevice" let item = ASPickerDisplayItem( name: "My Device", productImage: UIImage(named: "device-thumb")!, descriptor: descriptor ) session.showPicker(for: [item]) { error in if let error { print("picker failed: \(error)") } } }}
Adding bluetoothNameSubstring filters out unrelated devices advertising the same service. When similar devices crowd the area, a sheet without it gets hard to choose from, so I recommend narrowing by at least part of the name when you can.
Handle events and connect
The closure you pass to activate(on:) receives picker results and device state as events. Branch by type.
When a device is added
When the user picks and approves a device, .accessoryAdded arrives. Save the identifier of the received ASAccessory and, if needed, proceed to a CoreBluetooth connection.
When renamed or removed
Users can rename a device or unpair it from system settings. Handle .accessoryChanged and .accessoryRemoved and always keep your saved state in sync. Ignore this and a device the user "removed" lingers inside your app as an inconsistency.
On failure or cancel
.activated signals the session is ready. If the user closes the sheet, treat it as a plain cancel rather than an error, and just leave a path to re-present. Returning a cancel as a red error alert hurts the experience, so avoid it.
private func handle(_ event: ASAccessoryEvent) { switch event.eventType { case .accessoryAdded: current = event.accessory // OK to connect via CoreBluetooth from here case .accessoryChanged: current = event.accessory case .accessoryRemoved: current = nil default: break }}
How to bridge into CoreBluetooth
AccessorySetupKit only settles "which device you may use." The actual data exchange still happens over CoreBluetooth. The difference is that for an authorized device, CoreBluetooth connects without the broad permission dialog.
An ASAccessory carries the Bluetooth identifier, so you reclaim the device with CBCentralManager.retrievePeripherals(withIdentifiers:) and connect. No need to scan again. Reclaiming the single authorized device by name also cuts the steps to a connection.
Where people trip up
The first trap is a mismatch between the Info.plist service UUID and the code's bluetoothServiceUUID. Fix only one and the sheet is silently empty, with a hard-to-trace cause. Before shipping to production, always confirm on a real device that the sheet picks up the device once.
Next, the simulator has no Bluetooth, so AccessorySetupKit testing requires a real device. Before you have the target in hand, lock down the UUID and name declarations first, then verify the picker on a device once one is available.
Finally, dropping removal events. If the user unpairs from system settings and your app does not follow, you get the worst inconsistency: "shows connected but will not connect." Write the cleanup in .accessoryRemoved with the same care you give the connection itself.
Start by putting one service UUID of your device into Info.plist and confirming on a real device that showPicker picks it up. Once that passes, the rest is just adding the connection and the cleanup.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.