Bluetoothを利用した実装として、1アプリ(セントラル)に対し1デバイス(ペリフェラル)というのが基本だと思いますが、ここでは1アプリに対して複数デバイスの接続方法をみていきたいと思います。
なお、ここではBLEに対してある程度の知識がある前提で進めていきます。
環境
XCode: 9.2
Swift: 4.0
iOS: 11.1
完成形
Scanしたデバイスを一覧で表示し、セルをタップすることでconnect
、disconnect
を切り替えられるサンプルアプリを実装します。
実装
まず、iOSアプリでBLEを利用するためには、CoreBluetoothフレームワークを利用します。(詳しくは、AppleのCoreBluetoothプログラミングガイド参照)
1アプリにつき1デバイスの実装としては、CBCentralManagerDelegate
とCBPeripheralManagerDelegate
に一つのクラスが準拠するサンプルが多いと思います。
複数のデバイスを扱う場合でも単純にそのクラスをデバイス分インスタンスを作ればいいように思えます。
しかしApple Developer Forumsでの回答を見る限り、CBCentralManagerのインスタンスは1アプリにつき一つであることが望ましいようです。(Appleのドキュメントなどで明記されているところを自分は見つけられませんでした)
なので、
CBCentralManagerDelegate
に準拠するDeviceManager
クラス : 1
CBPeripheralDelegate
に準拠するDevice
クラス : n
となるような設計で実装します。
Deviceクラス
import Foundation
import CoreBluetooth
final class Device: NSObject {
let peripheral: CBPeripheral
let rssi: NSNumber
var state = State.disconnected
init(peripheral: CBPeripheral, rssi: NSNumber) {
self.peripheral = peripheral
self.rssi = rssi
super.init()
peripheral.delegate = self
}
}
extension Device {
enum State: String, CustomStringConvertible {
case disconnected
case connected
var description: String {
return rawValue
}
}
}
// MARK: - CBPeripheralDelegate
extension Device: CBPeripheralDelegate {}
CBPeripheral
をメンバ変数に持ち、一覧に状態を表示するためState
を持たせています。
また、CBPeripheralDelegate
に準拠するためには、NSObjectProtocol
に準拠する必要があるためNSObject
を継承します。
今回はコネクトまでの実装なので、CBPeripheralDelegate
のイベントを受け取る処理は実装していません。read
やwrite
をする場合は、この辺の実装する必要があります。
DeviceManagerクラス
import Foundation
import CoreBluetooth
final class DeviceManager: NSObject {
static let deviceUpdated = Notification.Name("deviceUpdated")
private let centralManager: CBCentralManager
private(set) var devices = [Device]() {
didSet {
NotificationCenter.default
.post(name: DeviceManager.deviceUpdated, object: nil)
}
}
override init() {
centralManager = CBCentralManager(delegate: nil, queue: nil)
super.init()
centralManager.delegate = self
}
func scan() {
centralManager.scanForPeripherals(withServices: nil, options: nil)
}
func stopScan() {
centralManager.stopScan()
}
func connect(peripheral: CBPeripheral) {
centralManager.connect(peripheral, options: nil)
}
func disconnect(peripheral: CBPeripheral) {
centralManager.cancelPeripheralConnection(peripheral)
}
func removeDevices() {
devices.removeAll()
}
}
// MARK: - CBCentralManagerDelegate
extension DeviceManager: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("state: \(central.state.rawValue)")
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
let device = Device(peripheral: peripheral, rssi: RSSI)
if let index = devices.index(where: { $0.peripheral.identifier == device.peripheral.identifier }) {
devices[index] = device
} else {
devices.append(device)
}
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
devices.first { $0.peripheral == peripheral }
.map { $0.state = .connected }
NotificationCenter.default
.post(name: DeviceManager.deviceUpdated, object: nil)
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
devices.first { $0.peripheral == peripheral }
.map { $0.state = .disconnected }
NotificationCenter.default
.post(name: DeviceManager.deviceUpdated, object: nil)
}
}
CBCentralManager
をメンバ変数に持ち、複数のデバイスをdevices
で管理します。
またTableView
を更新するための通知としてNotificationCentor
を利用しています。
centralManager = CBCentralManager(delegate: nil, queue: nil)
queue
に引数を渡すと任意のスレッドで、CBCentralManagerDelegate
のイベントを受け取ることができます。デフォルトはメインスレッドです。
func scan() {
centralManager.scanForPeripherals(withServices: nil, options: nil)
}
サンプルなので、withServices
の引数をnil
にしていますが、引数の指定が推奨されています。ただしその場合はペリフェラル側のアドバタイズメントパケットにServiceUUID
が設定されている必要があります。ここは注意です。
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
let device = Device(peripheral: peripheral, rssi: RSSI)
if let index = devices.index(where: { $0.peripheral.identifier == device.peripheral.identifier }) {
devices[index] = device
} else {
devices.append(device)
}
}
centralManager.scanForPeripherals
を呼び出したあと、ペリフェラルが見つかるとdidDiscover
が呼び出されます。ここでDevice
のインスタンスを作成し新規の場合はdevices
に追加、既存の場合は更新しています。
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
devices.first { $0.peripheral == peripheral }
.map { $0.state = .connected }
NotificationCenter.default
.post(name: DeviceManager.deviceUpdated, object: nil)
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
devices.first { $0.peripheral == peripheral }
.map { $0.state = .disconnected }
NotificationCenter.default
.post(name: DeviceManager.deviceUpdated, object: nil)
}
centralManager.connect
を呼び出し、コネクトが完了するとdidConnect
、centralManager.cancelPeripheralConnection
を呼び出し、ディスコネクトが完了するとdidDisconnectPeripheral
が呼び出されます。ここで、Device
のState
を変更し通知を送っています。
おわりに
1アプリ、複数デバイスの接続は割とニッチな内容ですが、誰かのためになれば嬉しいです。
TableView
の実装も含めたものは、githubにあげているので興味がある方は是非見てください。