本記事では、Core Bluetoothを使ってiPhoneとApple Watchで通信する方法を解説します。
目次
バージョン
本記事執筆時点での各バージョンは下記の通りです。
- Xcode 11.5
- Swift 5
本記事のゴール
本記事では、iPhoneとApple WatchでBLEのコミュニケーションが取れるようになること、これをゴールとします。それ以外の細々したところや難しいところは省きます。
BLEの本格的なアプリを作ろうとすると、色々なことを考慮しなければいけなくなり、頭がパンクしてしまいます。そのため、本記事では、発展的な内容には触れず、基本的な内容に注力し、シンプルに動くアプリを作ります。動くアプリさえあれば、それを改造して発展的な内容も試せるようになるでしょう。
本記事では下記のようなアプリを作り、Core Bluetoothの使い方を学びます。

BLEの役割として、iPhoneをPeripheral、Apple WatchをCentralとします。PeripheralとCentralは逆の方が良いのでは?と思うかもしれませんが、WatchOSの制限でApple WatchはCentralとしてしか動作できません。(WatchOS6の時点)
BLEでは色々なメッセージのやりとりの仕方がありますが、よく使うと思われる下記の2つの方式に絞って作ります。
- CentralからPeripheralへの書き込み
- PeripheralからCentralへの通知
Core Bluetoothを使ったアプリの作成
ではアプリを作っていきます。先ほどの図をより具体的にして、最終的には下記のアプリを作ります。

このアプリは次のように使います。
まずはiPhoneとApple Watchを接続します。
- iPhone側でアドバタイズの開始(スタートボタン)
- Apple Watch側でスキャンの開始(開始ボタン)
- Apple Watch側でiPhoneを見つけたら、該当のボタンをタップ(図のsampleボタン)
Apple Watch→iPhoneに書き込みを行うには、次のように操作します。今回はカウントアップする数値を書き込みます。
- Apple Watch側で書き込みを実行(書き込みボタン)
- iPhone側で書き込みを受信(Centralからの書き込みの隣の数値)
iPhone→Apple Watchに通知を行うには、次のように操作します。こちらも同様に通知にはカウントアップする数値を通知します。
- iPhone側で通知を実行(通知ボタン)
- Apple Watch側で通知を受信(通知の受信の隣の数値)
プロジェクトの作成
下記の手順でプロジェクトを作成します。
- 「File」→「New」→「Project…」をクリック
- 「watchOS」→「iOS App with Watch App」を選択
- 「Next」ボタンをクリックし、あとは指示に従ってください

iPhone側(Peripheral)のアプリを作る
Core BluetoothでPeripheralを作るには、下記の2つがキーとなります。
- CBPeripheralManager
- CBPeripheralMangerDelegate
CBPeripheralManagerを通して、PeripheralのServiceやCharacteristicの登録、値の更新といったことを行います。
CBPeripheralManagerDelegateは、Peripheralの各種状態変化やイベント通知を受け取るためのプロトコルです。
これら2つを使って、次のような手順でPeripheralの動作を実現していきます。
- ServiceとCharacteristicの登録
- アドバタイズの開始
- 通信のやりとり(通知や、書き込み)
以降で順番に説明していきます。
Bluetoothのパーミッション
ちょっとその前に、iPhoneでBluetoothを使うアプリを作る際には、パーミッションが必要になります。info.plistを開いて下記の設定をしてください。
| Key | Value |
|---|---|
| NSBluetoothAlwaysUsageDescription | Bluetoothを使う理由を書きます |

パーミッションを設定することで、アプリ起動時にBluetooth使用許可の確認ダイアログが表示されるようになります。使用許可を拒否してしまった時の対処などは、一般的な話になりますので、他のドキュメントを参考にしてください。
ServiceとCharacteristicの登録
第1ステップとして、ServiceとCharacteristicを登録しましょう。今回はServiceは1つ、Characteristicは書き込み用と通知用の2つを作ることにします。それぞれUUIDが必要なので、構造体にまとめて定義しておきます。
struct Const {
static let SERVICE_UUID = CBUUID(string: "6B5C4A86-CFB7-4031-98EB-09DF5E2543F0")
static let WRITE_CHARACTERISTIC_UUID = CBUUID(string: "4551ABAE-CE14-4F9E-B31A-E61FC372E480")
static let NOTIFY_CHARACTERISTIC_UUID = CBUUID(string: "DFB13451-1740-4F8A-B0F0-7C20D79E8BDF")
}
では、PeripheralとしてMyPeripheralクラスを作りたいと思います。
class MyPeripheral: NSObject, ObservableObject, CBPeripheralManagerDelegate {
let peripheralManager = CBPeripheralManager()
override init() {
super.init()
peripheralManager.delegate = self
}
// PeripheralManagerの状態が変わったとき
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case .poweredOff:
print("[BLE] PoweredOff")
break
case .poweredOn:
print("[BLE] poweredOn")
break
case .resetting:
print("[BLE] resetting")
break
case .unauthorized:
print("[BLE] unauthorized")
break
case .unknown:
print("[BLE] unknown")
break
case .unsupported:
print("[BLE] unsupported")
break
@unknown default:
print("[BLE] illegalstate \(peripheral.state)")
}
}
}
このコードはまだ入れ物として用意しただけです。
CBPeripheralManagerはメンバとして保持します。また、CBPeripheralManagerDelegateで必須のメソッドperipheralManagerDidUpdateState()をとりあえず書きました。peripheralManagerDidUpdateState()メソッドは今回はログ出力しているだけです。
また、ObservableObjectをクラスに付けているのは、あとでSwiftUIにバインドしたいものがあるからです。読み進めていけば理由はお分かりいただけると思います。
このクラスでServiceとCharacteristicを登録するregist()メソッドを用意します。
// レジスト
func regist() {
print("[BLE] start regist")
// create service
let service = CBMutableService(type: Const.SERVICE_UUID, primary: true)
// create characteristic
let writeCharacteristic = CBMutableCharacteristic(type: Const.WRITE_CHARACTERISTIC_UUID, properties: [.read, .writeWithoutResponse], value: nil, permissions: [.readable, .writeable])
let notifyCharacteristic = CBMutableCharacteristic(type: Const.NOTIFY_CHARACTERISTIC_UUID, properties: [.read, .notify], value: nil, permissions: [.readable, .writeable])
service.characteristics = [writeCharacteristic, notifyCharacteristic]
// regist
peripheralManager.add(service)
}
コードを見れば大体何をやってるかわかると思いますので、かんたんに説明します。
ServiceとCharacteristicのインスタンスをそれぞれ作成し、Characteristicはservice.characteristicsに配列で格納します。CBPeripheralManagerのadd()メソッドの引数に作成したServiceを指定すれば、レジストが開始されます。
登録処理は非同期で行われます。登録が完了したら呼び出されるメソッドがCBPeripheralManagerDelegateにあるので、そこでアドバタイズを開始します。
アドバタイズを開始する
登録完了を受けてアドバタイズを開始するコードは次のようになります。
var service: CBMutableService?
var writeCharacteristic: CBMutableCharacteristic?
var notifyCharacteristic: CBMutableCharacteristic?
// レジストが終わったとき
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
print("[BLE] didAdd error=\(String(describing: error))")
// Service と Characteristicを持っておく
self.service = service as? CBMutableService
for characteristic in service.characteristics! {
if characteristic.uuid == Const.NOTIFY_CHARACTERISTIC_UUID {
notifyCharacteristic = characteristic as? CBMutableCharacteristic
} else if characteristic.uuid == Const.WRITE_CHARACTERISTIC_UUID {
writeCharacteristic = characteristic as? CBMutableCharacteristic
}
}
// アドバタイズを開始
let advertisementData = [CBAdvertisementDataServiceUUIDsKey: [service.uuid], CBAdvertisementDataLocalNameKey: "sample"] as [String : Any]
peripheralManager.startAdvertising(advertisementData)
}
後々の通信でServiceやCharacteristicのインスタンスが欲しいので、メンバに保持しておきました。
アドバタイズの開始はCBPeripheralManagerのstartAdvertising()メソッドを使います。このとき、引数でアドバタイズメントデータを指定することができ、ServiceのUUIDとローカル名を指定します。
このあとは、Peripheral側はCentralからの購読や書き込みが来るのを待ちます。
通信のやりとり
通信のやりとり部分を実装していきましょう。
まずはCentral側からの書き込みを受信してみます。
@Published var wroteMessage = ""
// Centralからの書き込みを受信したとき
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
print("[BLE] didReceiveWrite")
let req = requests.first
let centralCount = req!.value!.withUnsafeBytes { $0.load( as: UInt32.self ) }
wroteMessage = "\(centralCount)"
}
データはCBTTRequestのvalueにData型として格納されています。今回通信のやりとりで使用するデータはUInt32型とします。ですので、UInt32型に変換して取り出しています。取り出したデータはSwiftUIにPublishする変数に格納しました。
次に通知を行う部分を実装します。
func notify() {
print("[BLE] notify")
let data = Data(bytes: &count, count: MemoryLayout.size(ofValue: count))
peripheralManager.updateValue(data, for: notifyCharacteristic!, onSubscribedCentrals: nil)
count += 1
}
通知を行うには、CBPeripheralManagerのupdateValue()メソッドを使います。書き込み受信と同様に、データはData型で扱います。このnotify()メソッドはSwiftUIから利用します。
これでPeripheralは実装できたので、SwiftUIで簡単なUIを作ります。
struct ContentView: View {
@ObservedObject var peripheral = MyPeripheral()
var body: some View {
VStack(spacing: 50) {
HStack(spacing: 20) {
Text("アドバタイズ")
Button(action: { self.peripheral.startRegistAndAdvertise() }) { Text("スタート") }
Button(action: { self.peripheral.stopAdvertise() }) { Text("ストップ") }
}
HStack(spacing: 20) {
Text("Centralからの書き込み")
Text("\(self.peripheral.wroteMessage)")
}
HStack(spacing: 20) {
Text("Centralへ通知")
Button(action: { self.peripheral.notify() }) { Text("通知") }
}
}
}
}
Apple Watch側(Central)を作る
Peripheralが作れたのでCentralを作ります。Core BluetoothでCentralを作る際には、下記の4つがキーとなります。
- CBCentralManager
- CBCentralManagerDelegate
- CBPeripheral
- CBPeripheralDelegate
CBCentralManagerを使って、Peripheralのスキャンや接続を行います。
CBCentralManagerDelegateは、CBCentralManagerにおける各種イベントを受けるためのプロトコルです。
CBPeripheralは、PeripheralのServiceやCharacteristicのディスカバー、Characteristicの値への書き込みといった操作を行います。
CBPeripheralDelegateは、ディスカバーしたイベントや、通知の受信イベントを受けるためのプロトコルです。
これら4つを使って、次の手順でCentralの動作を実現していきます。
- Peripheralをスキャン
- 見つけたPeripheralに接続
- PeripheralのServiceとCharacteristicをディスカバー
- 通信のやりとり
Peripheralをスキャン
まずはCentralとしてMyCentralクラスを用意します。
class MyCentral: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
let centralManager = CBCentralManager()
override init() {
super.init()
centralManager.delegate = self
}
// Centralの状態が変化したとき
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOff:
print("BLE PoweredOff")
break
case .poweredOn:
print("BLE poweredOn")
break
case .resetting:
print("BLE resetting")
break
case .unauthorized:
print("BLE unauthorized")
break
case .unknown:
print("BLE unknown")
break
case .unsupported:
print("BLE unsupported")
break
@unknown default:
print("BLE illegalstate \(central.state)")
}
}
}
ここはPeripheralの時と同様ですね。
このクラスにスキャンするためのscan()メソッドを追加します。
var candidates: [CBPeripheral] = []
@Published var candidateNames: [String] = []
func startScan() {
print("start scan")
candidateNames.removeAll()
candidates.removeAll()
if !centralManager.isScanning {
centralManager.scanForPeripherals(withServices: [Const.SERVICE_UUID], options: nil)
}
}
candidatesはスキャンして見つけたPeripheralを保存しておくための配列です。
candidateNamesは見つけたPeripheralをSwiftUIで表示するための配列です。
PeripheralのスキャンにはCBCentralManagerのscanForPeripherals()メソッドを使います。この時、withServices引数でServiceのUUIDを指定すると、そのServiceを持ったPeripheralだけスキャンされるので便利です。(nilだと全部スキャンします。)
スキャンを開始して、Peripheralを見つけたら配列に格納しておきます。
// Peripheralを見つけたとき
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
let name = peripheral.name
let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String
print("BLE discovered \(String(describing: name)) LocalName=\(String(describing: localName))")
if localName != nil && localName == "sample" {
candidateNames.append(localName!)
candidates.append(peripheral)
}
}
ここではアドバタイズに入っている名前をSwiftUIの表示用に使いました。(ここは本当はperipheral.nameで取れるPeripheralの名前を使った方が良かったと思っています。)
見つけたPeripheralに接続
見つけたPeripheralに接続するconnect()メソッドを作ります。
func connect(index: Int) {
print("BLE start connect")
self.peripheral = candidates[index]
centralManager.connect(self.peripheral!, options: nil)
}
接続するにはCBCentralManagerのconnect()メソッドを使います。
接続は非同期で行われます。接続完了は、CBCentralManagerDelegateの接続完了時に呼び出されるメソッドで待ちます。
PeripheralのServiceとCharacteristicをディスカバー
接続しただけでは、通信を始めることはできなくて、そのPeripheralのServiceとCharacteristicをディスカバーする必要があります。
接続完了時に呼び出されるメソッドで次のようなコードを書きます。
// 接続したとき
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("BLE connected")
peripheral.delegate = self
peripheral.discoverServices([Const.SERVICE_UUID])
}
ディスカバーや値の書き込みイベントを受け取るために、Peripheralのdelegateに自分自身を登録しておき、Serviceのディスカバーを開始します。
次にServiceのディスカバーが完了したときに呼び出されるコードを書きましょう。
// Serviceを見つけたとき
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
print("BLE discovered services")
service = peripheral.services?.first
peripheral.discoverCharacteristics([Const.WRITE_CHARACTERISTIC_UUID, Const.NOTIFY_CHARACTERISTIC_UUID], for: service!)
}
ここではCharacteristicのディスカバーを開始しています。
さらにCharacteristicのディスカバーが完了したときに呼び出されるコードにいきましょう。
// Characteristicを見つけたとき
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
print("BLE discovered characteristics")
for characteristic in service.characteristics! {
if characteristic.uuid == Const.WRITE_CHARACTERISTIC_UUID {
writeCharacteristic = characteristic
} else if characteristic.uuid == Const.NOTIFY_CHARACTERISTIC_UUID {
// 通知の受け取りを開始する
notifyCharacteristic = characteristic
peripheral.setNotifyValue(true, for: notifyCharacteristic!)
}
}
}
今回は通知を受けたいので、PeripheralのsetNotifyValue()で通知の受け取りを開始しています。第1引数にtrueを指定すると通知が開始されます。
通信のやりとり
ついにCentral側での通信のやりとりです。ここまでくればもう何も怖いものはありません。
まずは書き込みを行うwrite()関数を作ります。
var count: UInt32 = 0
func write() {
let data = Data(bytes: &count, count: MemoryLayout.size(ofValue: count))
peripheral?.writeValue(data, for: (self.service?.characteristics?.first)!, type: .withoutResponse)
count += 1
}
書き込みはPeripheralのwriteValue()メソッドを使います。
次は、通知を受け取るところを書いていきます。
@Published var notifiedMessage = "未受信"
// 通知を受け取ったとき
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
print("BLE updated characteristis: \(characteristic)")
let peripheralCount = characteristic.value!.withUnsafeBytes { $0.load( as: UInt32.self ) }
notifiedMessage = "\(peripheralCount)"
}
Peripheral側の書き込みを受け取ったところと同様です。通知を受けたデータをData型からUInt32型に変換してから、SwiftUIにPublishする変数に格納しているだけです。
最後にCentralのSwiftUIの画面を作りましょう。
struct ContentView: View {
@ObservedObject var central = MyCentral()
var body: some View {
ScrollView {
VStack {
Text("スキャン")
HStack {
Button(action: { self.central.startScan() }) { Text("開始") }
Button(action: { self.central.stopScan() }) { Text("停止") }
}
Text("見つけたiPhone")
ForEach(0..<self.central.candidateNames.count, id: \.self) { i in
Button(action: {
self.central.connect(index: i)
}) {
Text(self.central.candidateNames[i])
}
}
Text("メッセージ")
Button(action: { self.central.write() }) { Text("書き込み") }
Text("通知の受信: \(self.central.notifiedMessage)")
}
}
}
}
これでコーディングは完了です。全てのソースコードはGitHubに上げておきましたので、必要に応じてご利用ください。
ビルドして実行する
全てコードが書き終わったら、ビルドして実行してみましょう。

Apple Watch→iPhoneの書き込み、iPhone→Apple Watchの通知が動くこと確認してください。
終わりに
本記事では、iPhoneとApple WatchでBLE通信する方法を解説しました。かなり長くなってしまいましたが、1つ1つはそんなに難しいものではありません。焦らず順番に少しずつやれば大丈夫です。
注意点としては、Apple WatchはCentralとしてしか動作できない点ですね。アプリ設計時に考慮に入れておいてください。
本記事で実際に通信できるアプリを作りました。このアプリを改造して、発展的な内容に挑戦してみてください。
以上です、お疲れ様でした!