mcpField
A
accessorysetupkit

accessorysetupkit

github.com/dpearson2699開発1.2k3.1k 回インストール13 週間前に更新
リポジトリ

accessorysetupkit について

iOS 18 以降を対象とする開発者向けの AccessorySetupKit 統合ガイドです。システムピッカーによる Bluetooth・Wi-Fi アクセサリの検出と認証に加え、セットアップ後の通信や CoreBluetooth からの移行を説明します。

主な機能

  1. 01Bluetooth・Wi-Fi アクセサリの検出
  2. 02システムピッカーと認証設定
  3. 03セッションイベントの処理
  4. 04ペアリング後の接続
  5. 05既存アクセサリの移行

活用シーン

  1. 01アプリ内でアクセサリの検出と認証を案内
  2. 02CoreBluetooth でペアリング済み Bluetooth 機器に接続
  3. 03NetworkExtension でアクセサリの Wi-Fi に接続
  4. 04既存の CoreBluetooth アクセサリを新しい方式へ移行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill accessorysetupkit

インストール後、Claude Code で/accessorysetupkitを入力して読み込まれたか確認します。

同梱ファイルevals/evals.jsonreferences/accessorysetupkit-patterns.md

詳細

AccessorySetupKit

iOS 18 以降のシステムピッカーを使用して、プライバシーを保護しながら Bluetooth/Wi-Fi アクセサリを検出して認証し、その後、通信を CoreBluetooth または NetworkExtension に引き継ぎます。

目次

セットアップとエンタイトルメント

Info.plist の設定

アプリの Info.plist に次のキーを追加します。

キー型用途
NSAccessorySetupSupports[String]必須です。Bluetooth および/または WiFi を含む配列です
NSAccessorySetupBluetoothServices[String]アプリが検出するサービス UUID(Bluetooth)
NSAccessorySetupBluetoothNames[String]照合する Bluetooth 名または部分文字列
NSAccessorySetupBluetoothCompanyIdentifiers[String]2 バイトの Bluetooth 企業識別子

Bluetooth 固有のキーは、ASDiscoveryDescriptor で使用する値と一致させる必要があります。 Info.plist で宣言されていない識別子、名前、またはサービスをアプリが使用すると、 AccessorySetupKit による検出時にアプリがクラッシュします。Wi-Fi アクセサリの場合は、 NSAccessorySetupSupports に WiFi を含め、ディスクリプターの SSID ルールと一致させてください。

Bluetooth の許可は不要

アプリが NSAccessorySetupSupports に Bluetooth を指定すると、 CBCentralManager を作成してもシステムの Bluetooth 許可ダイアログは表示されなくなります。 AccessorySetupKit を介して少なくとも 1 つのアクセサリをペアリングした場合にのみ、 セントラルマネージャーの状態は poweredOn に遷移します。

検出ディスクリプター

ASDiscoveryDescriptor は、アクセサリを検索するための照合条件を定義します。 システムはスキャン結果をディスクリプター内のすべてのルールと照合し、 対象のアクセサリに絞り込みます。

Bluetooth ディスクリプター
import AccessorySetupKit
import CoreBluetooth

var descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothServiceUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABC")
descriptor.bluetoothNameSubstring = "MyDevice"
descriptor.bluetoothRange = .immediate  // Only nearby devices

Bluetooth ディスクリプターには、bluetoothCompanyIdentifier または bluetoothServiceUUID の少なくとも一方が必要です。必要に応じて、さらに絞り込む条件を追加します。

  • bluetoothNameSubstring と企業識別子またはサービス UUID の組み合わせ
  • bluetoothManufacturerDataBlob と bluetoothManufacturerDataMask を企業識別子と組み合わせて使用します。blob と mask は同じ長さである必要があります
  • bluetoothServiceDataBlob と bluetoothServiceDataMask をサービス UUID と組み合わせて使用します。blob と mask は同じ長さである必要があります
Wi-Fi ディスクリプター
var descriptor = ASDiscoveryDescriptor()
descriptor.ssid = "MyAccessory-Network"
// OR use a prefix:
// descriptor.ssidPrefix = "MyAccessory-"

ssid または ssidPrefix のいずれか一方を指定し、両方は指定しないでください。 両方が設定されているとアプリがクラッシュします。 ssidPrefix は空でない長さである必要があります。

Bluetooth の通信範囲

検出に必要な物理的な近さを設定します。

値動作
.default標準的な Bluetooth の通信範囲
.immediate物理的に近い場所にあるアクセサリのみ
サポートオプション

ディスクリプターの supportedOptions を設定して、アクセサリの機能を宣言します。

descriptor.supportedOptions = [.bluetoothPairingLE, .bluetoothTransportBridging]
オプション用途
.bluetoothPairingLEBLE ペアリングのサポート
.bluetoothTransportBridgingBluetooth トランスポートブリッジング
.bluetoothHIDBluetooth HID デバイス

ピッカーの表示

セッションの作成

ASAccessorySession を作成して有効にし、検出のライフサイクルを管理します。 session.accessories の読み取りやピッカーの表示は、.activated を待ってから行います。

import AccessorySetupKit

final class AccessoryManager {
    private let session = ASAccessorySession()

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

    private func handleEvent(_ event: ASAccessoryEvent) {
        switch event.eventType {
        case .activated:
            // Session ready. Check session.accessories for previously paired devices.
            break
        case .accessoryAdded:
            guard let accessory = event.accessory else { return }
            handleAccessoryAdded(accessory)
        case .accessoryChanged:
            // Accessory properties changed (e.g., display name updated in Settings)
            break
        case .accessoryRemoved:
            // Accessory removed by user or app
            break
        case .invalidated:
            // Session invalidated, cannot be reused
            break
        @unknown default:
            break
        }
    }
}
ピッカーの表示

名前、製品画像、検出ディスクリプターを指定して ASPickerDisplayItem のインスタンスを作成し、 有効になったセッションに渡します。

func showAccessoryPicker() {
    var descriptor = ASDiscoveryDescriptor()
    descriptor.bluetoothServiceUUID = CBUUID(string: "ABCD1234-0000-1000-8000-00805F9B34FB")

    guard let image = UIImage(named: "my-accessory") else { return }

    let item = ASPickerDisplayItem(
        name: "My Bluetooth Accessory",
        productImage: image,
        descriptor: descriptor
    )

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

ピッカーは別のシステムプロセスで実行されます。一致するデバイスはそれぞれ個別の項目として表示されます。 特定のディスクリプターに一致するデバイスが複数ある場合、ピッカーには横方向のカルーセルが表示されます。

セットアップオプション

各表示項目のピッカーの動作を設定します。

var item = ASPickerDisplayItem(
    name: "My Accessory",
    productImage: image,
    descriptor: descriptor
)
item.setupOptions = [.rename, .confirmAuthorization]
オプション動作
.renameセットアップ中にアクセサリの名前を変更できるようにします
.confirmAuthorizationセットアップ前に認証確認を表示します
.finishInAppペアリング後もアプリ内でセットアップが続くことを通知します
製品画像

ピッカーは、180x120ポイントのコンテナに画像を表示します。ベストプラクティスは次のとおりです。

  • すべての画面スケール係数に対応した高解像度の画像を使用します
  • ライトモードとダークモードで正しく表示されるよう、透明な背景を使用します
  • 透明な境界を余白として調整し、アクセサリの見かけ上のサイズを制御します
  • ライトモードとダークモードの両方でテストします

イベント処理

イベントの種類

セッションは、イベントハンドラーを通じてASAccessoryEventオブジェクトを配信します。

イベント発生時
.activatedセッションがアクティブになったとき。session.accessoriesを照会します
.accessoryAddedユーザーがピッカーでアクセサリを選択したとき
.accessoryChangedアクセサリのプロパティが更新されたとき(名前の変更など)
.accessoryRemovedアクセサリがシステムから削除されたとき
.invalidatedセッションが無効になったとき。新しいセッションを作成します
.migrationCompleteレガシーアクセサリの移行が完了したとき
.pickerDidPresentピッカーが画面に表示されたとき
.pickerDidDismissピッカーが閉じられたとき
.pickerSetupBridgingトランスポートのブリッジングをセットアップ中
.pickerSetupPairingBluetoothのペアリング中
.pickerSetupFailedセットアップに失敗したとき
.pickerSetupRenameユーザーがアクセサリの名前を変更しているとき
.accessoryDiscovered新しいアクセサリが見つかったとき(カスタムフィルタリングモード)
ピッカーを閉じるタイミングの調整

ユーザーがアクセサリを選択すると、.accessoryAddedは.pickerDidDismissより先に発生します。ピッカーが閉じた後にカスタムのセットアップUIを表示するには、最初のイベントでアクセサリを保存し、閉じた後に処理します。

private var pendingAccessory: ASAccessory?

private func handleEvent(_ event: ASAccessoryEvent) {
    switch event.eventType {
    case .accessoryAdded:
        pendingAccessory = event.accessory
    case .pickerDidDismiss:
        if let accessory = pendingAccessory {
            pendingAccessory = nil
            beginCustomSetup(accessory)
        }
    @unknown default:
        break
    }
}

Bluetoothアクセサリ

ピッカーからアクセサリを追加した後、CoreBluetoothを使用して通信します。ASAccessoryのbluetoothIdentifierはCBPeripheralに対応します。

import CoreBluetooth

func handleAccessoryAdded(_ accessory: ASAccessory) {
    guard let btIdentifier = accessory.bluetoothIdentifier else { return }

    // Create CBCentralManager — no Bluetooth permission prompt appears
    let centralManager = CBCentralManager(delegate: self, queue: nil)

    // After poweredOn, retrieve the peripheral
    let peripherals = centralManager.retrievePeripherals(
        withIdentifiers: [btIdentifier]
    )
    guard let peripheral = peripherals.first else { return }
    centralManager.connect(peripheral, options: nil)
}

重要な点:

  • アプリにペアリング済みのアクセサリがある場合にのみ、CBCentralManagerの状態は.poweredOnになります
  • scanForPeripherals(withServices:)によるスキャンでは、AccessorySetupKitを通じてペアリングされたアクセサリのみが返されます
  • AccessorySetupKitのみを使用する場合、NSBluetoothAlwaysUsageDescriptionは必要ありません

Wi-Fiアクセサリ

Wi-Fiアクセサリの場合、ASAccessoryのssidでネットワークを識別します。NetworkExtensionのNEHotspotConfigurationを使用して接続します。

import NetworkExtension

func handleWiFiAccessoryAdded(_ accessory: ASAccessory) {
    guard let ssid = accessory.ssid else { return }

    let configuration = NEHotspotConfiguration(ssid: ssid)
    NEHotspotConfigurationManager.shared.apply(configuration) { error in
        if let error {
            print("Wi-Fi join failed: \(error.localizedDescription)")
        }
    }
}

アクセサリはAccessorySetupKitを通じて検出されているため、ネットワークへの接続時に標準のWi-Fi接続確認プロンプトは表示されません。

CoreBluetoothからの移行

CoreBluetoothで認証済みのアクセサリを登録しているアプリは、ASMigrationDisplayItemを使用してAccessorySetupKitに移行できます。これは既知のアクセサリを新しいシステムに登録する一度限りの操作です。

func migrateExistingAccessories() {
    guard let image = UIImage(named: "my-accessory") else { return }

    var descriptor = ASDiscoveryDescriptor()
    descriptor.bluetoothServiceUUID = CBUUID(string: "ABCD1234-0000-1000-8000-00805F9B34FB")

    let migrationItem = ASMigrationDisplayItem(
        name: "My Accessory",
        productImage: image,
        descriptor: descriptor
    )
    // Set the peripheral identifier from CoreBluetooth
    migrationItem.peripheralIdentifier = existingPeripheralUUID

    // For Wi-Fi accessories:
    // migrationItem.hotspotSSID = "MyAccessory-WiFi"

    session.showPicker(for: [migrationItem]) { error in
        if let error {
            print("Migration failed: \(error.localizedDescription)")
        }
    }
}

移行ルール:

  • showPickerに移行項目のみが含まれている場合、システムは検出ピッカーの代わりに情報ページを表示します
  • 移行項目と通常の表示項目が混在している場合、新しいアクセサリが検出され、セットアップされたときにのみ移行が行われます
  • 移行が完了する前にCBCentralManagerを初期化しないでください。初期化するとエラーが発生し、ピッカーが表示されません
  • 移行が完了すると、セッションは.migrationCompleteを受け取ります

よくある間違い

間違い修正方法
記述子の識別子がInfo.plistにないピッカーを表示する前に、すべてのBluetoothサービス、名前、会社識別子を宣言します。
ssidとssidPrefixの両方が設定されている一致方法を必ずどちらか一方だけ選びます。
CoreBluetoothが移行完了前に開始される.migrationCompleteを待ってから、CBCentralManagerを作成します。
ユーザーの明示的な操作なしにピッカーが表示されるユーザーの操作からのみピッカーを表示します。
無効になったセッションが再利用される新しいASAccessorySessionを作成して有効化し、保持します。

レビューチェックリスト

  • NSAccessorySetupSupports を Info.plist に追加し、Bluetooth および/または WiFi を指定している
  • showPicker を呼び出す前にセッションを有効化している
  • イベントハンドラーで [weak self] を使用し、保持サイクルを回避している
  • @unknown default を含め、すべての ASAccessoryEventType ケースを処理している
  • 製品画像に透明な背景を使用し、適切な解像度にしている
  • セットアップ後の接続に ASAccessory の bluetoothIdentifier または ssid を使用している
  • アプリの状態をクリーンアップできるよう、アクセサリの削除イベントを処理している

参考資料

コメント