iOSでBLE(Bluetooth Low Energy)通信を実装する際、キャラクタリスティック(データ項目)に対する操作の基本となるのが Read(読み込み)、Write(書き込み)、Notify(通知) の3つです。
この記事では、これら3つの通信方式の違い、メリット・デメリット、そしてSwift(CoreBluetooth)での具体的な実装コードを分かりやすく解説します。
1. Read / Write / Notify の比較一覧
まずは、それぞれの通信方式の特徴を一覧表で比較してみましょう。
| 方式 | データの流れ | 通信のトリガー | 主な用途 |
|---|---|---|---|
| Read | ペリフェラル ➔ iOS | iOS側からの要求 | バッテリー残量、シリアル番号など(変化の少ないデータ) |
| Write | iOS ➔ ペリフェラル | iOS側からの要求 | LEDの点灯、設定値の変更、コマンド送信 |
| Notify | ペリフェラル ➔ iOS | デバイス側のデータ変化 | 心拍数、センサー値、ボタン押下など(リアルタイムデータ) |
2. 各通信方式の詳細とSwift実装例
① Read(読み込み)
iOS(セントラル)からデバイス(ペリフェラル)に対して、**「現在のデータを1回ちょうだい」**とリクエストする方式です。
- メリット: 必要な時だけ通信するため、消費電力を抑えられる。
- デメリット: リアルタイムに変化するデータの取得には向かない。
💻 Swiftコード例
// 1. Readを要求する
if characteristic.properties.contains(.read) {
peripheral.readValue(for: characteristic)
}
// 2. データが返ってきたらデリゲートで受け取る
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
print("Readエラー: \(error.localizedDescription)")
return
}
// 取得したバイナリデータ(Data型)
guard let data = characteristic.value else { return }
print("受信データ: \(data)")
}
② Write(書き込み)
iOSからデバイスに対して、**「このデータを設定して/この処理を実行して」**と命令を送る方式です。
CoreBluetoothでは、主に次の2種類を使い分けます。
-
.withResponse: デバイスからの「書き込み完了通知」を待つ(確実性重視) -
.withoutResponse: 完了通知を待たずに次々送る(速度重視)
💻 Swiftコード例
// 送信したいデータ(例: [0x01] という1バイトのコマンド)
let commandBytes: [UInt8] = [0x01]
let data = Data(commandBytes)
// 1. Writeを実行する(ここでは応答ありを指定)
if characteristic.properties.contains(.write) {
peripheral.writeValue(data, for: characteristic, type: .withResponse)
}
// 2. (withResponseの場合のみ)書き込みが完了したら呼ばれる
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
print("Writeエラー: \(error.localizedDescription)")
return
}
print("書き込みが正常に完了しました")
}
③ Notify(通知 / サブスクリプション)
iOS側から最初に「通知をON」にしておくことで、デバイス側のデータが更新されたタイミングで自動的かつ連続的にデータが送られてくる方式です。Youtubeのチャンネル登録のような仕組みです。
- メリット: 常に最新のデータを最速で受け取れる(リクエストを毎回送る無駄がない)。
- デメリット: 常に通信可能な状態を維持するため、Readに比べて電力消費が増える。
💻 Swiftコード例
// 1. Notify(通知)をONにする
if characteristic.properties.contains(.notify) {
peripheral.setNotifyValue(true, for: characteristic)
}
// 2. データが自動で送られてくるたびに、Readと同じデリゲートメソッドが呼ばれる
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
guard let data = characteristic.value else { return }
print("リアルタイム通知データ: \(data)")
}
// 3. (参考)通知状態が切り替わったときに呼ばれるデリゲート
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if characteristic.isNotifying {
print("通知が開始されました")
} else {
print("通知が停止されました")
}
}
3. 開発時によくあるトラップと対策
トラップ1:Notifyのデータ受け取り関数はReadと同じ
コード例の通り、Read でデータが返ってきたときと、Notify でデータが自動通知されたときに呼ばれるデリゲートメソッドはどちらも peripheral(_:didUpdateValueFor:error:) で共通です。
そのため、関数内で characteristic.uuid をチェックして、どちらのデータかを判別する必要があります。
トラップ2:プロパティ(properties)に合わない操作をするとクラッシュやエラーになる
デバイス側が「Read専用」として作っているキャラクタリスティックに対して、iOSから writeValue を行うと、エラーが返ってくるか通信が拒否されます。必ず characteristic.properties.contains(...) でその操作が許可されているか確認してから実行するのが安全です。
まとめ
- Read:欲しいときに1回だけ取りにいく(静的なデータ用)
- Write:iOSからデバイスを操作・命令する(制御用)
- Notify:一度登録して自動で送り続けてもらう(動的なセンサー用)
デバイスの仕様書(GATT仕様)を見ながら、どのキャラクタリスティックがどのプロパティを持っているかを確認し、適切なメソッドを呼び出しましょう!