swift-bluetooth-expert
iOS/iPadOSのSwift CoreBluetoothおよびBLEに関するスキルです。スキャン、接続、GATT読み取り/書き込み/通知、アドバタイジング、バックグラウンド実行、状態復元、複数周辺機器の自動再接続、送信キューのバックプレッシャー制御、ATTリクエスト処理、L2CAPチャネル、MTUネゴシエーション、CBError/CBATTErrorのリカバリに対応できます。SwiftUI @Observable/@MainActorおよびasync-await/nonisolated delegatesの実装、プロトコルベースのBLEモッキングにも対応します。CBCentralManager、CBPeripheral、CBPeripheralManager、CBL2CAPChannel、CBUUID、AllowDuplicatesKey、アドバタイジング制限、updateValue、canSendWriteWithoutResponse、maximumWriteValueLength、RSSI、willRestoreState、didReceiveWriteRequestsなどのAPI、バックグラウンドモード、NSBluetoothAlwaysUsageDescriptionの設定、BLEテストに対応します。AndroidのBluetooth、Web Bluetooth、classic BR/EDR、Bluetooth以外のネットワーク通信は対象外です。
description の原文を見る
Swift CoreBluetooth/BLE on iOS/iPadOS: scanning, connecting, GATT read/write/notify, advertising, background execution, state restoration, multi-peripheral auto-reconnect, transmit-queue backpressure, ATT request handling, L2CAP channels, MTU negotiation, CBError/CBATTError recovery, SwiftUI @Observable/@MainActor, async-await/nonisolated delegates, protocol-based BLE mocking. Trigger on CBCentralManager, CBPeripheral, CBPeripheralManager, CBL2CAPChannel, CBUUID, AllowDuplicatesKey, advertisement limits, updateValue, canSendWriteWithoutResponse, maximumWriteValueLength, RSSI, willRestoreState, didReceiveWriteRequests, background modes, NSBluetoothAlwaysUsageDescription, or BLE testing. Not for Android Bluetooth, Web Bluetooth, classic BR/EDR, or non-Bluetooth networking.
SKILL.md 本文
Swift Bluetooth エキスパート
Apple の CoreBluetooth フレームワークを使用して iOS/iPadOS 上で Bluetooth Low Energy デバイスと通信する、堅牢でイディオマティックな Swift アプリを構築するサポートをします。抽象的な理論ではなく、直接的なガイダンス、モダン Swift での動作するコード例、およびドキュメントに基づいた推奨事項を提供します。
実際の問題を特定することから始める
CoreBluetooth の質問は複数のレイヤーが混在しています。回答する前に、タスクを駆動しているレイヤーを特定してください:
- セントラルロール(スキャン、発見、接続、特性の読み書き)
- ペリフェラルロール(アドバタイジング、サービス構築、リクエストへの応答)
- データモデル(サービス、特性、ディスクリプタ、UUID)
- バックグラウンド実行と状態保存
- SwiftUI 統合とアーキテクチャ
- モダン Swift 並行処理(デリゲート API を async/await またはアクターでラップする)
- Info.plist 設定とパーミッション
- テストとシミュレーション
バージョンや環境の詳細が重要な場合は、早期に確認してください。通常、最も有用な欠落した詳細は:
- iOS デプロイメントターゲットと Xcode バージョン
- アプリがバックグラウンド BLE 必要か(どのモードか)
- セントラルロール vs ペリフェラルロール(または両方)
- 関連する特定の BLE デバイスまたはサービス UUID
- エラーメッセージまたは予期しない動作
作業スタイル
- 直接的な回答を優先し、その後コードを示してください。
- 例は最小限ですが機能的に。Swift のモダン機能(
@Observable、async/await、アクター、SwiftUI など)を使用してください。 - すべての CoreBluetooth コードは API レベルではデリゲートベースです。ユーザーのコードベースがモダンパターンを使用する場合は、デリゲートを非同期ストリームまたは Combine パブリッシャーにブリッジする方法を示してください。クラシックなデリゲートパターンを使用している場合は、そのスタイル内で作業してください。
- パターンが重要な理由を説明してください: バッテリー寿命、状態機械の正確性、バックグラウンド制約、またはスレッドセーフティー。
- CoreBluetooth API ガイダンスを SwiftUI 統合とアーキテクチャの選択から分離してください。BLE マネージャークラスは UI フレームワークに関わらず機能する必要があります。
- アドバイスが iOS バージョンに依存する場合は、明示的に述べてください。
ドメインガイダンス
セントラルロール
セントラルロールは最も一般的なスタートポイントです。CBCentralManager はペリフェラルをスキャンし、接続し、サービスと特性を発見し、値を読み書き/サブスクライブします。
正しく実装すべきキーパターン:
- 初期化: デリゲートを持つ
CBCentralManagerを作成します。最初のコールバックは常にcentralManagerDidUpdateState(_:)です。スキャンは.poweredOnになるまで待つ必要があります。 - スキャン: 可能な限り特定のサービス UUID を指定して
scanForPeripherals(withServices:options:)を呼び出します。nilを渡すとすべてを発見しますが、バッテリーを浪費します。 - ペリフェラルを保持する: 発見した
CBPeripheralオブジェクトへの強い参照を保存してください。CoreBluetooth はそれらを保持しません。保持しない場合、それらは割り当て解除され、接続は静かに失敗します。 - 接続:
connect(_:options:)はタイムアウトしません。システムはcancelPeripheralConnection(_:)が呼ばれるまで無限に再試行します。独自のタイムアウトを実装してください。 - サービス/特性発見: 接続後、
discoverServices(_:)を呼び出し、次にdiscoverCharacteristics(_:for:)を呼び出します。不要な無線使用を避けるために、特定の UUID を渡してください。 - 値の読み取り:
readValue(for:)はperipheral(_:didUpdateValueFor:error:)をトリガーします。characteristic.value(Data?)をパースしてください。 - 値の書き込み: 確認が必要な場合は
.withResponseを使用し、ストリーミングデータの場合は.withoutResponseを使用します。.withResponseの書き込みについてはperipheral(_:didWriteValueFor:error:)で確認してください。 - 応答なしの書き込みフロー制御: 書き込む前に
peripheral.canSendWriteWithoutResponseを確認してください。falseの場合はperipheralIsReady(toSendWriteWithoutResponse:)を待ってください。これなしでは、バッファが満杯のときに書き込みは静かに削除されます。 - MTU 認識:
peripheral.maximumWriteValueLength(for:)を呼び出して、最大ペイロードサイズを確認します。デフォルトの ATT MTU は 23 バイト(使用可能 20 バイト)です。iOS は対応するペリフェラルとの大きな MTU を自動ネゴシエーションします(多くの場合 185 バイト以上)。それに応じてデータをチャンク化してください。 - サブスクリプション: 通知を有効にするために
setNotifyValue(true, for:)を実行します。値の更新は読み取りと同じdidUpdateValueForデリゲートメソッドを通じて到着します。 - 切断: 完了時に
cancelPeripheralConnection(_:)を常に呼び出してください。まずサブスクリプションをsetNotifyValue(false, for:)でクリーンアップします。
ペリフェラルロール
ローカルデバイスを BLE ペリフェラルとしてセットアップし、サービスをアドバタイズして接続されたセントラルに応答します。
キーパターン:
- 初期化: デリゲートを持つ
CBPeripheralManagerを作成します。サービスを追加する前に.poweredOnを待ってください。 - サービスツリーの構築:
CBMutableCharacteristic(プロパティ、値、パーミッション付き)を作成 →CBMutableServiceに追加 → ペリフェラルマネージャーでadd(_:)を呼び出します。 - 特性値: 初期化時に nil でない値を設定すると、キャッシュされ、直接提供されます。動的値をデリゲートコールバック経由で配信したい場合は
nilに設定してください。 - アドバタイジング:
startAdvertising(_:)はCBAdvertisementDataLocalNameKeyとCBAdvertisementDataServiceUUIDsKeyのみを受け入れます。アドバタイジングデータは 28 バイト(スキャン応答では 10 バイトのローカル名が追加)に制限されています。 - リクエストへの応答:
peripheralManager(_:didReceiveReadRequest:)とperipheralManager(_:didReceiveWriteRequests:)を実装します。常に各コールバックあたりrespond(to:withResult:)を正確に 1 回呼び出してください。 - 通知の送信:
updateValue(_:for:onSubscribedCentrals:)を使用します。falseを返す場合は、送信キューが満杯です。peripheralManagerIsReady(toUpdateSubscribers:)を待ってください。
UUID
- 標準 Bluetooth SIG サービス/特性は 16 ビット UUID を使用します(例: ハートレート用の
"180D")。CoreBluetooth はこれらを内部的に 128 ビットに展開します。 - カスタムサービスは完全な 128 ビット UUID が必要です。ターミナルで
uuidgenを使用して生成してください。 CBUUID(string:)でCBUUIDオブジェクトを作成します。- よくある定義済み UUID:
"180D"(ハートレート)、"180A"(デバイス情報)、"180F"(バッテリー)、"2A37"(ハートレート測定)、"2A29"(メーカー名)。
バックグラウンド実行
iOS でのバックグラウンド BLE には、スキャンとアドバタイジングの動作を根本的に変える厳しい制約があります。
- バックグラウンドモード: Info.plist の
UIBackgroundModesにbluetooth-centralと/またはbluetooth-peripheralを追加します。 - バックグラウンドのセントラル: スキャンは引き続き機能しますが、
CBCentralManagerScanOptionAllowDuplicatesKeyは無視されます(検出が統合されます)。スキャン間隔は増加します。システムはデリゲートコールバックのためにアプリをウェイクアップします。 - バックグラウンドのペリフェラル:
CBAdvertisementDataLocalNameKeyは無視されます。サービス UUID は「オーバーフロー」エリアに移動し、その UUID を明示的にスキャンしているデバイスのみが発見できます。アドバタイジング周波数は低下します。 - 10 秒ルール: バックグラウンドでウェイクアップされるときは、作業を迅速に完了してください。システムは時間がかかるアプリをスロットルまたは終了する可能性があります。
- iOS 26+: アプリに
CBManagerがあり、Live Activity を開始する場合、バックグラウンド中でもフォアグラウンド相当のスキャン特権が得られます(重複統合なし、スキャン間隔増加なし)。
状態保存と復元
アプリ終了後も動作するような長時間実行の BLE タスク(ユーザーが帰宅したときに自動再接続するドアロックなど)の場合:
- オプトイン: マネージャー作成時に
CBCentralManagerOptionRestoreIdentifierKey(またはCBPeripheralManagerOptionRestoreIdentifierKey)を渡します。 - 再起動を処理:
application(_:didFinishLaunchingWithOptions:)で、復元識別子のUIApplication.LaunchOptionsKey.bluetoothCentrals/.bluetoothPeripheralsを確認してください。 - 状態を復元:
centralManager(_:willRestoreState:)またはperipheralManager(_:willRestoreState:)を実装します。ディクショナリは保存されたペリフェラル、サービス、スキャン情報を含みます。 - システムは次を保存します: スキャン対象のサービス、接続/接続中のペリフェラル、サブスクライブされた特性。
Info.plist の要件
- 必須:
NSBluetoothAlwaysUsageDescription— iOS 13 以降なしではアプリがクラッシュします。 - レガシー: iOS 12 以前の場合は
NSBluetoothPeripheralUsageDescription。 - バックグラウンドモード: バックグラウンド BLE が必要な場合は、
bluetooth-centralと/またはbluetooth-peripheralを指定したUIBackgroundModes。
SwiftUI 統合
CoreBluetooth はデリゲートベースのため、ブリッジレイヤーが必要です。推奨アーキテクチャ:
CBCentralManager/CBPeripheralManagerを所有し、デリゲートプロトコルを実装するBluetoothManagerクラス(またはアクター)を作成します。- iOS 17+ では
@Observableとして、iOS 14+ ではObservableObjectとして標記してください。これにより SwiftUI ビューが状態変更に反応できます。 - BLE 状態を発行プロパティとして公開します: 接続状態、発見されたペリフェラル、特性値。
- クリーンなアクセスのため
.environment()または@Environmentで注入してください。 - BLE マネージャーから UI ロジックを削除してください。ビューは状態を読み取り、
scan()、connect(to:)、disconnect()などのメソッドを呼び出すべきです。
モダン Swift 並行処理
CoreBluetooth API はコールバック/デリゲートベースですが、モダン Swift パターンでより整潔にできます:
- AsyncStream: デリゲートコールバック(発見されたペリフェラルや特性値の更新など)を
AsyncStreamでラップして、呼び出し側がfor awaitでそれらを反復できるようにします。 - チェック済みコンティニュエーション: 接続、サービス発見、または単一読み取りなどのワンショット操作に
withCheckedThrowingContinuationを使用します。 - アクター: スレッドセーフティーのためにアクターベースの BLE マネージャーを検討してください。ただし、CoreBluetooth はデリゲート呼び出しを特定のディスパッチキューで要求します。デリゲート適合に
nonisolatedメソッドを使用し、状態をアクターにリレーしてください。 - Combine ブリッジ: 既に Combine を使用しているコードベース用に、
PassthroughSubject/CurrentValueSubjectは特性値ストリームのための自然なブリッジになります。
並行処理ラッパーについては実用的になってください。複雑な複数ステップフロー(スキャン → 接続 → 発見 → サブスクリプション)に価値を追加しますが、単純なユースケースではデリゲートパターンが問題なく、デバッグしやすいです。
@MainActor + nonisolated パターン(Swift 6 厳密な並行処理):
SwiftUI アプリ用の推奨パターンは、BLE マネージャークラスを @MainActor で標記し、CBCentralManager を queue: .main で作成することです。これにより、すべての状態変異が SwiftUI 観測に対して安全になります。デリゲートプロトコル適合は nonisolated が必要です。CoreBluetooth はディスパッチキューからデリゲートを呼び出すため:
@MainActor
final class BLEManager: NSObject, @Observable {
var peripherals: [CBPeripheral] = []
private var centralManager: CBCentralManager!
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: .main)
}
}
extension BLEManager: CBCentralManagerDelegate {
nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) {
MainActor.assumeIsolated {
// 安全: queue: .main を設定したため、これはメインアクターです
// ここで状態を更新
}
}
}
MainActor.assumeIsolated 呼び出しは安全です。初期化時に queue: .main を指定したため、すべてのデリゲートコールバックは既にメインスレッド上にあるためです。これにより Task { @MainActor in } が必要になることを避けます。この方法は非同期ホップと順序問題を引き起こします。
デリゲートコールバックをメインキューで実行すると UI がカクつく高スループットアプリの場合は、代わりにマネージャーを専用の連続 DispatchQueue で作成し、状態更新を @MainActor に明示的にディスパッチしてください。
再接続戦略
再接続の 3 つの方法(優先順):
retrievePeripherals(withIdentifiers:)— 保存した UUID で以前既知のデバイスに再接続します。最速。retrieveConnectedPeripherals(withServices:)— システムに既に接続されているデバイスを見つけます(別のアプリでもある可能性)。scanForPeripherals(withServices:)— 完全スキャン。最も遅く、最もバッテリーを使用します。
最初の発見後、ペリフェラルの identifier(UUID)を UserDefaults または同様のストレージに保存して、次の起動時にオプション 1 を使用できるようにしてください。
複数ペリフェラル シナリオでは、段階的なアプローチを使用してください: タイヤ 1 を短いタイムアウト(~5 秒)で試し、タイヤ 2 にエスカレートし、その後タイヤ 3。これにより、1 つの戦略で時間を無駄にすることを避けられます。別の戦略がより速く成功する場合。
複数のペリフェラルが同時に切断される場合(ユーザーが範囲外に歩いていくなど)、指数バックオフをジッターと共に使用して、再接続試行を分散させ、BLE 無線での群れの衝突を避けてください。
L2CAP チャネル
GATT 特性が効率的に処理できる範囲を超えるスループットのデータ転送の場合、CoreBluetooth は L2CAP Connection-Oriented Channels(CoC)をサポートしています:
- 使用時期: ファームウェアアップデート、ファイル転送、オーディオストリーミング、または属性レベルの読み書きではなくストリーム指向のデータ転送が必要なシナリオ。L2CAP は GATT オーバーヘッド(属性ヘッダー、特性ごとのキューイング)を回避し、生のバイトストリームを提供します。
- ペリフェラルセットアップ:
peripheralManager.publishL2CAPChannel(withEncryption:)を呼び出します。システムはperipheralManager(_:didPublishL2CAPChannel:error:)を通じて PSM(Protocol/Service Multiplexer)を割り当てます。この PSM 値をセントラルにアドバタイズします(例: GATT 特性内)。 - セントラルセットアップ: PSM を発見後、
peripheral.openL2CAPChannel(_:)を呼び出します。チャネルをperipheral(_:didOpen:error:)で処理してください。 - データ転送:
CBL2CAPChannelは入出力InputStream/OutputStreamオブジェクトを提供します。標準的なストリームデリゲートパターンを使用するか、非同期シーケンスでラップしてください。 - 暗号化: 暗号化リンクを要求する場合(必要に応じてペアリングをトリガー)、
publishL2CAPChannel(withEncryption:)にtrueを渡します。
エラーハンドリング
CoreBluetooth は 2 つのドメインを通じてエラーを通信します:
-
CBError(フレームワークレベル): 接続失敗、無効なパラメータ、ピア切断。一般的なケース:
.connectionFailed— 汎用接続失敗.peerRemovedPairingInformation— ペリフェラルがペアリング情報を削除しました。ユーザーは設定でデバイスを削除して再ペアリングする必要があります。.connectionLimitReached— 同時接続数が多すぎます(デバイス依存、通常 7~10).encryptionTimedOut— ペアリング/暗号化ネゴシエーションがタイムアウトしました。
-
CBATTError(GATT レベル): 読み取り/書き込み/通知操作からのエラー。一般的なケース:
.invalidHandle、.readNotPermitted、.writeNotPermitted— 操作を試みる前に特性プロパティを確認してください。.insufficientEncryption、.insufficientAuthentication— 特性はボンド/暗号化接続を必要とします。まずペアリングをトリガーしてください。.attributeNotFound— コード内とペリフェラルファームウェア間のサービス/特性 UUID の不一致。
回復パターン:
.peerRemovedPairingInformationの場合: ユーザーに iOS 設定でデバイスを削除させ、再スキャンして再ペアリングしてください。- 一時的な接続失敗の場合: 指数バックオフで再試行してください(1 秒 → 2 秒 → 4 秒、30 秒でキャップ、±20% ジッターを追加)。
.connectionLimitReachedの場合: 接続をキューイングし、別の接続から切断した後にのみ次のデバイスに接続してください。- すべてのデリゲートコールバックの
errorパラメータをいつも確認してください。成功を仮定しないでください。
CoreBluetooth のテスト
CoreBluetooth は iOS シミュレーターで動作しません。すべての BLE テストは物理デバイスが必要です。このアクティビティ制約に基づいてテスト戦略を計画してください:
- プロトコルベースの抽象化: CoreBluetooth API をミラーする
BluetoothScanningやBluetoothPeripheralなどのプロトコルを定義します。BLE マネージャーはこれらのプロトコルに依存し、テストではモック実装を注入します。これは、ハードウェアなしで BLE ロジックをユニットテストする最も効果的な方法です。 - 状態機械テスト: BLE マネージャーが明示的な状態機械を使用する場合(複数ペリフェラルシナリオではそうすべき)、CoreBluetooth に独立して状態遷移をテストしてください。モックイベントをフィードして、状態変更を検証します。
- 2 デバイステスト: ペリフェラルロールアプリの場合、2 番目の iOS デバイスを実行してセントラルロール テストハーネス(またはその逆)を実行してください。Apple の「Core Bluetooth Transfer」サンプルプロジェクトはスタートポイントとして有用です。
- nRF Connect / LightBlue: サードパーティ BLE スキャナーアプリはデバッグに非常に有用です。アドバタイジングデータを検査したり、特性を読み書きしたり、ペリフェラルの GATT 構造を検証したりできます。
- バックグラウンドテスト: アプリをバックグラウンドに送信し、デリゲートコールバックが引き続き発火することを確認して、バックグラウンド BLE をテストしてください。Xcode の「Simulate Background Fetch」を使用するか、単にホームボタンを押して待ってください。
ベストプラクティス
- 無線使用を最小化: 必要な場合のみスキャンしてください。デバイスを見つけた後にスキャンを停止してください。特定のサービス UUID を使用してください。
- 必要なもののみ発見:
discoverServices(_:)とdiscoverCharacteristics(_:for:)に特定の UUID 配列を渡してください。 - ポーリングではなくサブスクリプション: 頻繁に変わる値のために繰り返した
readValue(for:)の代わりに通知(setNotifyValue(true, for:))を使用してください。 - 完了時に切断: 必要なデータを取得したら、サブスクリプションをキャンセルしてから接続をキャンセルしてください。
- 状態遷移を処理: 常に
centralManagerDidUpdateState(_:)/peripheralManagerDidUpdateState(_:)を確認してください。BLE はいつでも非表示にされたり、許可されなくなったり、サポートされなくなることがあります。 - アドバタイジング制限:
CBAdvertisementDataLocalNameKeyとCBAdvertisementDataServiceUUIDsKeyのみがサポートされます。フォアグラウンド アドバタイジングデータの合計は 28 バイトに制限されています。
レスポンスパターン
必要に応じてこの形を使用してください:
- 直接的な診断または推奨
- 動作する Swift コード
- このパターンが重要な理由(バッテリー、正確性、バックグラウンド制約、スレッドセーフティー)
- iOS バージョンまたはデプロイメントターゲットの注
- 関連する公式ドキュメントリンク
小さなリクエストには厳密なテンプレートを強制しないでください。ただし、回答をスキャン可能に保ってください。
BLE コードを確認またはリファクタリングするとき
CBPeripheralへの強い参照を欠落(#1 の静かな失敗)を確認します。- サービス UUID なしでスキャン(バッテリードレイン)を確認します。
- 接続前に
centralManagerDidUpdateState/ 状態チェック を欠落を確認します。 - デリゲートが接続後にペリフェラルで設定されていることを確認します。
- メインスレッドでの同期 BLE 操作ブロッキングを見てください。
ライセンス: Unlicense(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- trancee
- リポジトリ
- trancee/MeshLink-old
- ライセンス
- Unlicense
- 最終更新
- 2026/5/10
Source: https://github.com/trancee/MeshLink-old / ライセンス: Unlicense