【Swift UI】Core BluetoothでCentralの実装方法!ServiceとCharacteristic

この記事からわかること
- Swift UIでCore BluetoothでBluetooth接続アプリの実装方法
- Central側の実装
- ServiceとCharacteristicの取得方法
- CBCentralManagerDelegateやCBPeripheralDelegateの使い方
- peripheralから値を取得したり書き込むには?
- peripheralから更新通知を受け取る(notify)には?
index
[open]
\ アプリをリリースしました /
今回はiOSアプリでBluetooth機能を実装するためにCore Bluetoothを使用してCentral側の実装方法をまとめていきます。この記事とPeripheral側も実装することで実際にBluetooth機能をテストできるアプリが完成できるのでハンズオンで試してみてください。
Core Bluetoothとは?
Core BluetoothはアプリからBLE(Bluetooth Low Energy)を使用するための機能を提供するライブラリです。デフォルトで組み込まれているので導入作業は必要なくすぐに使用することができます。
Bluetooth接続を用いる場合は例えばPCとキーボードなど接続しあう2つの登場人物が必要になります。2つのうちBluetoothの機能を使用する親側を「Central(セントラル)」、機能を提供する子側を「Peripheral(ペリフェラル)」と呼びます。例で言うとPCがセントラル、キーボードがペリフェラルです。
そしてペリフェラルが持っている機能のまとまりをサービス、機能1つ1つをキャラクタリスティックと呼びます。
iOSアプリ側もセントラルであることが多いですが、もちろんペリフェラルとして実装することも可能になっています。この記事は前回(↓)の記事でペリフェラル側を実装した後に接続できるようにセントラル側を実装していますので先にペリフェラル側の実装から読んでください。
実装するにあたって
Xcodeで実装するにあたってシミュレーターではBluetooth機能をサポートしていないのでテストする際には実機が必要になります。ペリフェラル側との連携を試したい場合は実機が2台に必要になるので注意してください。
また今回の全体のコードはGitHubに掲載しています。
Central機能の実装方法
セントラル側を実装する際はペリフェラル側の仕様などが決まってから実装することのが多いと思います。今回は前回の記事で実装した機能を使用できるようにしていきます。
流れ
- Privacy - Bluetooth Always Usage Descriptionキーの追加
- Bluetooth機能管理クラスの作成
- CBCentralManagerDelegateへの準拠
- スキャンの実装
- ペリフェラルの検出と接続
- サービス一覧を取得する
- キャラクタリスティック一覧を取得する
- キャラクタリスティックから値を取得する
Core Bluetoothではスキャンや探索などの処理を実行すると結果がデリゲートメソッドから取得できる仕組みが多用されています。デリゲートについて詳しく知りたい方は以下の記事を参考にしてください。
Privacy - Bluetooth Always Usage Descriptionキーの追加
iOSアプリでBluetooth機能を使用するためには「info.plist」に以下のキーを追加します。
Privacy - Bluetooth Always Usage Description
value
には「アプリ内でBluetooth機能を使用するため」など、Bluetooth機能を使用する理由を記述しておきます。
Bluetooth機能管理クラスの作成
Bluetooth機能を管理するためのBlueCentralManager
クラスを作成します。この際にimport CoreBluetooth
も合わせて記述しておきます。Centralとしての機能を使用する提供するCBCentralManager
型のcentralManager
プロパティをとシングルトン用のプロパティを定義しておきます。
import UIKit
import CoreBluetooth
class BlueCentralManager: NSObject, ObservableObject {
// シングルトン
static let shared = BlueCentralManager()
// Centralマネージャー
private var centralManager: CBCentralManager!
// ログ出力用
@Published var log = ""
// MARK: - ペリフェラル側の実装に合わせて定義する
// peripheral側のローカル名を定義
private var peripheralName = "Test Peripheral"
// サービス用のUUID
private let serviceUUID = CBUUID(string:"00000000-0000-1111-1111-111111111111")
// キャラクタリスティック用のUUID
private let readCharacteristicUUID = CBUUID(string:"00000000-1111-1111-1111-111111111111")
private let writeCharacteristicUUID = CBUUID(string:"00000000-2222-1111-1111-111111111111")
private let notifyCharacteristicUUID = CBUUID(string:"00000000-3333-1111-1111-111111111111")
private let indicateCharacteristicUUID = CBUUID(string:"00000000-4444-1111-1111-111111111111")
// キャラクタリスティック保持用
private var readCharacteristic: CBCharacteristic!
private var writeCharacteristic: CBCharacteristic!
private var notifyCharacteristic: CBCharacteristic!
private var indicateCharacteristic: CBCharacteristic!
// MARK: - ペリフェラル側の実装に合わせて定義する
override init() {
super.init()
}
}
また接続したいペリフェラル情報を各プロパティに保持しておきます。後で使用するのでinit
もオーバーライドしておきます。
CBCentralManagerDelegateへの準拠
公式リファレンス:CBCentralManagerDelegate
続いてCBCentralManagerDelegate
にBlueCentralManager
クラスを準拠させていきます。CBCentralManagerDelegate
のデリゲートメソッドからCentralとしての状態や周辺機器の検出結果などを取得することが可能です。準拠させることでcentralManagerDidUpdateState(_: CBCentralManager)
デリゲートメソッドの実装が必須になります。これはCentralの状態が変化するタイミングで呼ばれます。
// ① CBCentralManagerDelegateへの準拠
extension BlueCentralManager: CBCentralManagerDelegate {
// ①Centralの状態が変化するタイミング (実装必須)
func centralManagerDidUpdateState(_ central: CBCentralManager) {
//
}
}
引数のcentral
のstate
プロパティから現在の状態をCBManagerState型で取得できます。以下は状態に応じて分岐させているコードです。
// ① Centralの状態が変化するタイミング (実装必須)
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .unknown:
// 状態不明
log.append("unknown\n")
case .resetting:
// 一時的にリセットされた状態
log.append("resetting\n")
case .unsupported:
// デバイスがBluetooth機能をサポートしていない
log.append("unsupported\n")
case .unauthorized:
// 使用許可がされていない
log.append("unauthorized\n")
case .poweredOff:
// 電源がOFF
log.append("poweredOff\n")
case .poweredOn:
// 電源がON
log.append("poweredOn\n")
@unknown default:
log.append("default\n")
}
}
最後にCBCentralManagerDelegate
に準拠したことでcentralManager
プロパティにCBCentralManager
インスタンスを格納できるようになります。引数delegate
にはself(自身)
をqueue
にはnil
を渡しておきます。
override init() {
super.init()
// ① インスタンスの格納
centralManager = CBCentralManager(delegate: self, queue: nil)
}
スキャンの実装
Bluetoothで接続できるペリフェラルをスキャンするにはscanForPeripherals
メソッドを使用します。これは周辺にあるペリフェラルを全てスキャンします。withServices
にnil
を渡すと無条件でマッチするものを検索しますが、サービスのUUIDを渡すことでスキャンを絞ることが可能です。使用するサービスが確定している場合は指定した方が効率が上がります。
// ②:スキャンの実装
public func startScan() {
if centralManager.state == .poweredOn {
log.append("スキャン開始\n")
centralManager.scanForPeripherals(withServices: nil, options: nil)
}
}
またscanForPeripherals
メソッドは明示的にstop
するまでスキャンし続けてしまい電池を無駄に消耗してしまうので、後述するstopScan
メソッドを使用して適切なスキャンの停止の実装が必要です。(例えば10秒のタイマーを設置するなど)
ペリフェラルの検出と接続
検出されたペリフェラルをクラス内で保持しておくために新しくプロパティを定義しておきます。
// ③:ペリフェラルの検出
private var connectPeripheral: CBPeripheral!
// ログ出力用
@Published var log = ""
scanForPeripherals
を実行してペリフェラルが検出されるとcentralManager(_:,didDiscover peripheral:,advertisementData:,rssi)
メソッドが呼ばれます。引数のperipheral
から検出されたペリフェラルを参照することができます。ここでは定義しておいたペリフェラルの名前と一致するペリフェラルがあれば接続処理をconnect
メソッドを使用して実装しています。
// ③:ペリフェラルの検出
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// advertisementDataからローカル名を取得
if let localName = advertisementData["kCBAdvDataLocalName"] as? String{
// 取得したローカル名とマッチしたいペリフェラル名を比較
if localName == self.peripheralName {
log.append("対象のペリフェラル:\(localName)を検出\n")
self.connectPeripheral = peripheral
// ペリフェラルと接続
central.connect(peripheral, options: nil)
// スキャンの停止
centralManager.stopScan()
}
}
}
また接続処理を実行したら明示的にstopScan
メソッドでスキャンを停止させておきます。
ペリフェラルの接続結果を取得する
connect
メソッドで接続リクエストを送信し、接続が成功したらcentralManager(_:,didConnect peripheral:)
が失敗したらcentralManager(_:, didFailToConnect peripheral:,error:)
が呼ばれます。
// ④:ペリフェラルの接続結果を取得:成功
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
log.append("接続成功\n")
}
// ④:ペリフェラルの接続結果を取得:失敗
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
log.append("接続失敗\n")
}
サービス一覧を取得する
ペリフェラルとの接続が完了したらそのペリフェラルが持っているサービス一覧とさらにその中のキャラクタリスティック一覧を取得します。そのためにはCBPeripheralDelegate
に準拠させる必要があります。さらにextension
していきます。
// ⑤:サービス/キャラクタリスティックの取得
extension BlueCentralManager: CBPeripheralDelegate {
}
少しコードを戻ってペリフェラルの接続が成功した際にperipheral.delegate
にデリゲートのセットとサービスの探索を開始するdiscoverServices
メソッドを実装します。引数に探索対象のUUIDを渡して絞り込んでおきます。
// ④:ペリフェラルの接続結果を取得:成功
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
log.append("接続成功\n")
// ⑤:サービス/キャラクタリスティックの取得
peripheral.delegate = self
// サービスの探索開始
let services = [serviceUUID]
peripheral.discoverServices(services) // nilを渡すことも可能だが電池消費が激しい
}
discoverServices
メソッドが実行されサービスが見つかるとperipheral(_:didDiscoverServices error:)
デリゲートメソッドが呼ばれます。services
プロパティから配列形式でサービス一覧が取得できるのでサービス1つ1つに対してdiscoverCharacteristics
メソッドを実行してキャラクタリスティックの探索開始します。ここでも明示的にUUIDを指定することで電池消費を抑えることが可能です。
// ⑤:サービスが見つかった際に呼ばれる
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
log.append("サービスが見つかりました\n")
// servicesプロパティから配列形式のCBServiceが取得できる
if let services:Array<CBService> = peripheral.services {
for service in services {
let characteristicUUIDs = [
readCharacteristicUUID,
writeCharacteristicUUID,
notifyCharacteristicUUID,
indicateCharacteristicUUID
]
// キャラクタリスティックを探索開始
// nilを渡すことも可能だが電池消費が激しい
peripheral.discoverCharacteristics(characteristicUUIDs, for: service)
}
}
}
キャラクタリスティック一覧を取得する
discoverCharacteristics
メソッドが実行されキャラクタリスティックが見つかるとperipheral(_:, didDiscoverCharacteristicsFor service:, error:)
デリゲートメソッドが呼ばれます。characteristics
プロパティから配列形式でキャラクタリスティック一覧が取得できます。
// ⑤:キャラクタリスティックが見つかった際に呼ばれる
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
// characteristicsプロパティから配列形式のCBCharacteristicが取得できる
if let characteristics:Array<CBCharacteristic> = service.characteristics {
log.append("キャラクタリスティックは\(characteristics.count)個見つかりました\n")
for characteristic in characteristics {
// 一致するUUIDを検索してそれぞれに格納
if characteristic.uuid == readCharacteristicUUID {
readCharacteristic = characteristic
} else if characteristic.uuid == writeCharacteristic {
writeCharacteristic = characteristic
} else if characteristic.uuid == notifyCharacteristicUUID {
notifyCharacteristic = characteristic
} else if characteristic.uuid == indicateCharacteristicUUID {
indicateCharacteristic = characteristic
}
}
}
}
キャラクタリスティックの一覧から1つずつUUIDを比較しマッチすればそれぞれのプロパティへ格納しておきます。
キャラクタリスティックから値を取得する
キャラクタリスティックから値を取得するにはreadData
メソッドを新しく定義し、その中で保持しているペリフェラルからreadValue
メソッドを呼び出します。引数には読み出し対象のキャラクタリスティックを渡します。
// ⑥ キャラクタリスティックから値を取得する
public func readData() {
if readCharacteristic != nil {
connectPeripheral.readValue(for: readCharacteristic)
}
}
値が取得できるとperipheral(_:, didUpdateValueFor characteristic:, error:)
デリゲートメソッドが呼ばれcharacteristic
からUUIDや値など参照することができます。
// ⑥ キャラクタリスティックから値を取得する
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
log.append("ペリフェラルから値を取得しました。\n")
log.append("キャラクタリスティックの値:\(characteristic.value)\n")
}
ペリフェラルに値を書き込む
キャラクタリスティックに値を書き込むにregisterData
メソッドを新しく定義し、その中で保持しているペリフェラルからwriteValue
メソッドを呼び出します。引数には対象のデータ、書き込み対象のキャラクタリスティックを渡します。
// ⑦ ペリフェラルに値を書き込む
public func registerData() {
if writeCharacteristic != nil {
connectPeripheral.writeValue("Hello".data(using: .utf8)!, for: writeCharacteristic!, type: CBCharacteristicWriteType.withResponse)
}
}
書き込みが成功するとperipheral(_:, didWriteValueFor characteristic:, error:)
が呼ばれます。
// ⑦ 書き込み成功時に呼ばれる
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
log.append("書き込み成功\n")
}
Notifyの通知を受け取る
Notifyはセントラルではなくペリフェラルから値の発火のタイミングが操作されています。セントラル側ではNotifyを検知する状態にしておくことでペリフェラル側からの値の更新を通知します。setNotifyValue
メソッドにtrue
を渡せば検知状態、false
なら検知を停止させることができます。
// ⑧ Notifyを検知開始する
public func observeNotify() {
if notifyCharacteristic != nil {
// Notifyの検知を開始
connectPeripheral.setNotifyValue(true, for: notifyCharacteristic)
}
}
// ⑧ Notifyを検知停止する
public func stopNotify() {
if notifyCharacteristic != nil {
// Notifyの検知を停止
connectPeripheral.setNotifyValue(false, for: notifyCharacteristic)
}
}
Notifyで受け取った値はread
と同じくperipheral(_:, didUpdateValueFor characteristic:, error:)
デリゲートメソッドが呼ばれるのでここから取得できます。
// ⑥ キャラクタリスティックから値を取得する
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
log.append("ペリフェラルから値を取得しました。\n")
log.append("キャラクタリスティックの値:\(characteristic.value)\n")
}
おすすめ参考書:iOS×BLE Core Bluetoothプログラミング
iOSアプリでBLEを使用した機能を実装したいなら一度は読んでおくことをおすすめする参考書です。iOSでのCore Bluetoothを使用した実装だけでなく、Bluetoothに関する細かい知識やノウハウも詰まっているので網羅的に理解したい方にはバッチリだと思います。
少し古い参考書であり、Objective-CとSwift両方のコードで実装方法が記述されています。Swift UIでの実装方法は載っていませんが、基本的なコードは昔からあまり変わっていないのでつまづくところはなく実装できると思います。
BLEを食材や店員などに例えて解説してくれるので素人でもBLEの概念がつかみやすく記述されています。約500ページくらいあるのでボリュームがすごいですが、ここから得られる知識は数知れませんでした。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
個人開発に限界を感じたらiOSに特化したプログラミングスクール「iOSアカデミア」も検討してみてください!無料相談可能で「最短・最速」でiOSエンジニアになれるように手助けしてくれます。