【Swift UI】Bluejay(Bluetoothライブラリ)の使い方!
この記事からわかること
- Swift UIでBluejayを使用してBluetooth接続アプリの実装方法
- セントラルの実装
- インストールやスキャン、接続方法
- Core Bluetoothへ移行するには?
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Xcode:15.0.1
- iOS:17.1
- Swift:5.9
- macOS:Sonoma 14.1
Bluejayとは?
BluejayとはBluetooth(BLE)でアプリと機器を接続するための機能を提供しているiOSアプリライブラリです。iOSアプリでのBLE接続は公式のフレームワークである「Core Bluetooth」が用意されていますが、これをラップして使用しやすいようにしているのがBluejayになります。
BluejayはBLEの複数台接続には対応していないため、同時に周辺機器と接続したい際にはこのライブラリは使用できないので注意してください。
インストール
BluejayライブラリをCarthageやCocoa Podsでのインストールをサポートしています。
Carthage
github "steamclock/bluejay" ~> 0.8
github "DaveWoodCom/XCGLogger" ~> 6.1.0
Cocoa Pods
pod 'Bluejay', '~> 0.8'
Cocoa Podsを使用してXcode15でインストール後、ビルドすると以下のようなエラーが出ました。
セントラル機能の実装と使い方
BLE機能を試したい場合はiPhoneを2台用意してペリフェラル側を実装する必要があるのでCore Bluetoothを使用してペリフェラル側を実装した以下の記事を参考にしてください。
上記の記事で実装したペリフェラル機器があるとしてセントラル側の実装をBluejayを使用して実装していきます。また今回の実装はGitHubに上げてますので参考にしてください。
Privacy - Bluetooth Always Usage Descriptionキーの追加
iOSアプリからBluetooth機能を使用するためには「info.plist」に以下のキーを追加する必要があります。
Privacy - Bluetooth Always Usage Description
忘れずに追加してvalue
には「アプリ内でBluetooth機能を使用するため」など、Bluetooth機能を使用する理由を記述しておきます。
使用するサービス/キャラクタリスティックの準備
サービスとキャラクタリスティックの定義にはServiceIdentifier
とCharacteristicIdentifier
を使用します。引数にはUUIDを渡し、サービスとキャラクタリスティックの紐付けは引数にサービスを渡すだけになっています。
let service = ServiceIdentifier(uuid: "00000000-0000-1111-1111-111111111111")
let readCharacteristic = CharacteristicIdentifier(uuid: "00000000-1111-1111-1111-111111111111", service: service)
let writeCharacteristic = CharacteristicIdentifier(uuid: "00000000-2222-1111-1111-111111111111", service: service)
以下記事でUUIDについても解説しています。
初期設定
Bluejayを使用するためにはデリゲートのセットとstart
メソッドを実行します。これはイニシャライザの中で実行しておきます。
class BluejayManager: ObservableObject {
/// シングルトン
static var shared = BluejayManager()
/// ログ出力用
@Published var log = ""
/// bluejayインスタンス
public var bluejay = Bluejay()
/// スキャン時に発見したペリフェラル一覧
private var discoveries: [ScanDiscovery] = []
init() {
config()
}
/// 初期設定
private func config() {
self.bluejay.register(logObserver: self)
bluejay.registerDisconnectHandler(handler: self)
bluejay.register(connectionObserver: self)
bluejay.register(serviceObserver: self)
self.bluejay.start()
}
}
必要なデリゲートプロトコルへ準拠させておきます。
extension BluejayManager: LogObserver, ConnectionObserver, ServiceObserver, DisconnectHandler {
func didDisconnect(from peripheral: PeripheralIdentifier, with error: Error?, willReconnect autoReconnect: Bool) -> AutoReconnectMode {
AutoReconnectMode.noChange
}
func didModifyServices(from peripheral: PeripheralIdentifier, invalidatedServices: [ServiceIdentifier]) { }
func bluetoothAvailable(_ available: Bool) { }
func connected(to peripheral: PeripheralIdentifier) { }
func disconnected(from peripheral: PeripheralIdentifier) { }
func debug(_ text: String) { }
}
スキャン処理
ペリフェラルをスキャンするにはscan
メソッドを渡します。引数には検索したいサービスを配列形式で渡します。ペリフェラルが見つかるとdiscovery
クロージャーの中から配列形式でScanDiscovery
型で取得できます。ScanDiscovery
型はプロパティにペリフェラルのIDや名称を保持しています。discovery
クロージャーの返り値はScanAction
型でスキャン後の挙動をコントロールすることができます。
bluejay.scan(
serviceIdentifiers: [service], // 検索したいサービス一覧
discovery: { (discovery, discoveries) -> ScanAction in
// ScanDiscovery型でペリフェラル情報を取得
return .continue
},
stopped: { (discoveries, error) in
// エラー発生時
}
)
ScanAction
はスキャンの続行、停止、ブラックリスト、コネクトの4つの挙動を決めることが可能です。
public enum ScanAction {
case `continue`
case blacklist
case stop
case connect(ScanDiscovery, (ConnectionResult) -> Void)
}
例えばスキャンして最初に見つかったペリフェラルにそのまま接続したい場合は以下のようにconnect
を指定します。
public func startScan() {
log.append("スキャン開始\n")
bluejay.scan(
serviceIdentifiers: [service],
discovery: { [weak self] (discovery, discoveries) -> ScanAction in
guard let self = self else { return .stop }
if discoveries.count != 0 {
self.log.append("発見: \(discoveries.count)個\n")
self.discoveries = discoveries
return .connect(discoveries.first!, .none, .default) { result in
switch result {
case .success:
self.log.append("コネクト成功: \n")
case .failure(let error):
self.log.append("コネクト失敗エラー: \(error.localizedDescription)\n")
}
}
} else {
self.log.append("見つかりませんでした\n")
return .continue
}
},
stopped: { [weak self] (discoveries, error) in
guard let self = self else { return }
if let error = error {
self.log.append("スキャン停止エラー: \(error.localizedDescription)\n")
} else {
self.log.append("スキャン停止\n")
}
})
}
接続処理
スキャンの流れでコネクトしない場合はスキャンで検出したペリフェラルを保持しておきconnect
メソッドを使用して接続処理を後から実行することも可能です。引数には接続したいペリフェラルのIDを渡します。
/// コネクト
public func connect() {
log.append("コネクト\n")
guard let peripheral = discoveries.first else {
log.append("サービスなし\n")
return
}
bluejay.connect(peripheral.peripheralIdentifier, timeout: .seconds(15)) { result in
switch result {
case .success:
self.log.append("コネクト成功: \(peripheral.peripheralIdentifier)\n")
case .failure(let error):
self.log.append("コネクト失敗エラー: \(error.localizedDescription)\n")
}
}
}
Read処理
Readキャラクタリスティックを使用するにはread
メソッドを使用します。
/// Read処理
public func read() {
bluejay.read(from: readCharacteristic) { [weak self] (result: ReadResult<UInt8>) in
guard let self = self else { return }
switch result {
case .success(let location):
self.log.append("Read成功: \(location)\n")
case .failure(let error):
self.log.append("Read失敗: \(error.localizedDescription)\n")
}
}
}
Write処理
Writeキャラクタリスティックを使用するにはwrite
メソッドを使用します。
/// Write処理
public func registerData() {
bluejay.write(to: writeCharacteristic, value: "Hello") { [weak self] (result: WriteResult) in
guard let self = self else { return }
switch result {
case .success:
self.log.append("Write成功: \n")
case .failure(let error):
self.log.append("Write失敗: \(error.localizedDescription)\n")
}
}
}
切断
接続中のペリフェラルと切断するためにはdisconnect
メソッドを使用します。bluejay.isConnected
プロパティから接続中であるかどうかを識別することが可能です。immediate
にtrue
を渡すと溜まっているキューを無視して即時切断することが可能です。
/// 切断
public func disconnect() {
if bluejay.isConnected {
log.append("切断\n")
/// 切断(キューの終了を待って)
bluejay.disconnect()
/// 即時切断(キューの終了を待たずに)
// bluejay.disconnect(immediate: true)
}
}
またcancelEverything
メソッドでは接続は維持しつつも行われている操作をリセットすることが可能です。
/// リセット
public func cancelEverything() {
log.append("リセット\n")
/// キューをリセット
bluejay.cancelEverything()
}
Core Bluetoothへ移行する
BluejayではCore BluetoothをラップしているのでCBPeripheral
やCBCentralManager
は外部に公開していません。利用したい場合はstopAndExtractBluetoothState
メソッドを使用することでタプルでそれぞれを取得することが可能です。stopAndExtractBluetoothState
メソッドはすべての操作を停止し、Bluejayのすべての状態をクリアにしてCore Bluetoothを返します。
これによりCore Bluetoothまたは別のBLEライブラリへ移行することが可能です。
/// 次節の「Core Bluetoothから移行する」のために保持
private var cbPeripheral: CBPeripheral?
private var cbCentralManager: CBCentralManager!
/// CoreBluetoothへの移行
public func stopAndExtractBluetoothState() {
log.append("移行\n")
// 定義:public func stopAndExtractBluetoothState() -> (manager: CBCentralManager, peripheral: CBPeripheral?) {}
let status = bluejay.stopAndExtractBluetoothState()
cbCentralManager = status.manager
if let peripheral = status.peripheral {
cbPeripheral = peripheral
}
}
Core Bluetoothから移行する
逆にCore BluetoothからBluejayへ移行したい場合はstart
メソッドのmode
の引数にuse(manager:, peripheral:)
を使用します。
/// 移行後に再スタート
public func reStart() {
guard let cbPeripheral = self.cbPeripheral else {
return
}
log.append("再スタート\n")
bluejay.start(mode: .use(manager: cbCentralManager, peripheral: cbPeripheral))
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。