🕊️

【SwiftUI】CoreBluetooth、BLE

2023/01/08に公開

今回はiPhone間でのBLE通信について解説していきます。
あまりiPhone間でBLE通信をする場面はありませんが、セントラル、ペリフェラルどちらも解説しているのでiPhone間だけでなく、他デバイスとのBLE通信の参考になればと思います。

軽く用語解説

BLEとは、

Bluetooth Low Energy の略で、消費電力の低さ、障害物に強いのが特徴です。

セントラル(マスタ)

通信の制御を行う役割を持ちます。
BLEではセントラルがペリフェラルに要求を出すことでデータ通信が行われます。

ペリフェラル(スレーブ)

通信制御機能は持っていません。
セントラルからの要求に応えることで通信を行います。
ペリフェラルにはサービス、キャラリスティックが定義されおり、UUIDで管理されています。

サービス 機能の単位を表します。
キャラリスティック サービスの中に存在し、データが含まれています。

アドバタイズ

ペリフェラルがセントラルにブロードキャスト通信を使用して、「ここにいるよ」と伝えています。 接続待ちの仕組みをアドバタイズと呼びます。
ブロードキャスト通信とは、不特定多数にデータを送信する、一方通行通信のことです。

実装

ここからは実際に実装をしていきます。
今回は、Appleが用意してくれているサンプルをSwiftUIに書き換えていく形で解説していきます。
接続手順通りにプログラムを書いていく人もいれば、自分のようにセントラルとペリフェラルを分けて書いていく人もいるので、やりやすい方法をとってください。

その前に、BLEの接続手順について軽く解説します。
実装手順ではないです。

接続手順

  1. ペリフェラルからアドバタイズを送信。
  2. セントラルでアドバタイズを受け取る。
  3. セントラルが接続を要求する。
  4. 接続完了!

ここから実装手順です。

フレームワークの導入と使用許可

フレームワークを導入

import CoreBluetooth

使用許可

info.plistにKeyとValueを追加

// Key
Privacy - Bluetooth Always Usage Description
// Value
他デバイスとのメッセージでBluetoothを使用します。

Central

1. CBCentralManagerのインスタンスを生成

var centralManager: CBCentralManager!
centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true])

options の CBCentralManagerOptionShowPowerAlertKey は、peripheralManager をインスタンス化する際に、 Bluetooth がオフ状態あるかどうかをシステムに警告させるかどうかを指定します。
デフォルトは false です。
nil でも動きますが、指定しておいて損はないと思います。

2. Delegateを追加

CentralViewModel.swift
extension CentralViewModel: CBCentralManagerDelegate {

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOn:
            print(".powerOn")
            return
	    
        case .poweredOff :
            print(".powerOff")
            return
            
        case .resetting:
            print(".restting")
            return
            
        case .unauthorized:
            print(".unauthorized")
            return
            
        case .unknown:
            print(".unknown")
            return
        
        case .unsupported:
            print(".unsupported")
            return
            
        @unknown default:
            print("A previously unknown central manager state occurred")
            return
        }
    }
}

extension CentralViewModel: CBPeriPheralDelegate {

}

使用するクラスには CBCentralManagerDelegate CBPeriPheralDelegate が必要です。
また、CBCentralManagerDelegateには centralManagerDidUpdateState(_:) を記述する必要があります。
centralManagerDidUpdateState(_:) はBluetoothの状態を示すメソッドです。
例として、Bluetoothが使える状態の時は、.poweredOnを示します。

3. ペリフェラルを検索

CentralViewModel.swift
let serviceUUID = CBUUID(string: "12345678-abcd-qwer-asdf-1234567890ab")

private func retrievePeripheral() {
        
    let connectedPeripherals: [CBPeripheral] = (centralManager.retrieveConnectedPeripherals(withServices: [serviceUUID]))
        
     os_log("Found connected Peripherals with transfer service: %@", connectedPeripherals)
        
     if let connectedPeripheral = connectedPeripherals.last {
         os_log("Connecting to peripheral %@", connectedPeripheral)
         self.discoveredPeripheral = connectedPeripheral
         centralManager.connect(connectedPeripheral, options: nil)

     } else {
         // ペリフェラルを検索 
         centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil)
     }
}

func stopAction() {
    centralManager.stopScan()
}

サンプルでは、centralManagerDidUpdateState(_:) が .poweredOn になったタイミング実行されています。
scanForPeripherals(withServices:options:) を使用するとペリフェラルを検索でき、stopScan() を使用するとペリフェラル検索を停止させることができます。
サンプルでは接続されているかどうか確認し、されていなければペリフェラルを検索しています。
scanForPeripherals の withServices にUUIDを指定してあげれば特定のペリフェラルに絞ることができ、nil を指定してしてあげればすべてのペリフェラルを検索することができます。
また、Appleのサンプルで options に書かれている、CBCentralManagerScanOptionAllowDuplicatesKey は、スキャンを重複フィルタリングなしで実行させるかどうかを指定するものです。
true を指定するとフィルタリングを無効にし、ペリフェラルから受信するたびに、イベントを発生させます。 指定しない、か false を指定すると同じペリフェラルの複数の検出を1つのイベントに結合させます。

4. 検索したペリフェラルに接続

CentralViewModel.swift
var discoveredPeripheral: CBPeripheral? 
extension CentralViewModel: CBCentralManagerDelegate {

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
	// 略
    }

+   func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
+	if discoveredPeripheral != peripheral {
+	    discoveredPeripheral = peripheral
+           centralManager.connect(peripheral, options: nil)
+	}
+   }
}

一致する serviceUUID を持ったペリフェラルを検索すると、 centralManager(_:didDiscover:advertisementData:rssi:) を呼び出します。
サンプルでは、discoveredPeripheral プロパティにペリフェラルを保存し、 connect(_:options:) を呼び出してペリフェラルに接続します。

RSSI パラメータを使用し、信号がデータ転送に十分な強度であるかを判断することもできます。

CentralViewModel.swift
var discoveredPeripheral: CBPeripheral? 
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
      
+   guard RSSI.intValue >= -50 else {
+       return
+   }
        
    if discoveredPeripheral != peripheral {
        discoveredPeripheral = peripheral
        centralManager.connect(peripheral, options: nil)
    }
}

5. サービスを検索

CentralViewModel.swift
extension CentralViewModel: CBCentralManagerDelegate {

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
	// 略
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { 
        // 略
    }  

+   func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {                                     
+       // スキャン停止            
+       centralManager.stopScan()
+ 
+       connectionIterationsComplete += 1
+       writeIterationsComplete = 0
+
+       // すでに情報を持っているかもしれないので削除
+       data.removeAll(keepingCapacity: false)
+
+       peripheral.delegate = self
+       peripheral.discoverServices([serviceUUID])
+   }            
}

ペリフェラルに接続ができたら、 centralManager(_:didConnect:) が呼ばれます。
ごちゃごちゃ書いてありますが、重要なのは最後の2行で、delegate を指定し、サービスを検索、取得しています。serviceUUID を nil にすることですべてのサービスを検索することもできます。
その前の数行は、スキャンを停止させ、すでに情報を持っていたら削除しています。

6. Characteristicを検索

CentralViewModel.swift
let characteristicUUID = CBUUID(string: "09876543-asdf-qwer-zxcv-1234567890cd")
extension CentralViewModel: CBPeriPheralDelegate {

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {   
        if let error = error {       
            print("Error discovering services: \(error.localizedDescription)")         
            cleanup()
             return 
        } 
   
        guard let peripheralServices = peripheral.services else {
            return
        } 
	
        for service in peripheralServices {
             peripheral.discoverCharacteristics([characteristicUUID], for: service)     
        }
     }     
}

サービスを取得すると、peripheral(_:didDiscoverServices:)が呼ばれます。
サービスに紐づくCharacteristic を検索、取得します。
characteristicUUID を nil にすることですべてを検索対象にできます。

7. データを受け取る

CentralViewModel.swift
@Published var message: String = ""
var data: Data = Data()

extension CentralViewModel: CBPeriPheralDelegate {

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
	// 略
    }

+    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 
+        if let error = error {         
+            print("Error discovering characteristics: \(error.localizedDescription)")                                
+            cleanup()                   
+            return                    
+        }                               
+                                       
+        guard let characteristicData = characteristic.value,
+            let stringFromData = String(data: characteristicData, encoding: .utf8) else {                            
+            return                    
+        }                               
+                                      
+        print("Received \(characteristicData.count) bytes: \(stringFromData)")
+                                      
+        if stringFromData == "EOM" {  
+            message = String(data: data, encoding: .utf8) ?? ""
+                                      
+        } else {                      
+            data.append(characteristicData)
+        }                             
+    }                                  
}

ペリフェラルからデータが届いたことを知らせるメソッドです。
ペリフェラルから “EOM” が送られてきたら、message プロパティに data プロパティをエンコードをして代入しています。 “EOM” が送られてくるまでは、data プロパティに送られてきたデータを入れておきます。

Centralの全体コード

Central
CentralViewModel.swift
import Foundation
import CoreBluetooth
import os

class CentralViewModel: NSObject, ObservableObject {
    
    @Published var message: String = ""
    var centralManager: CBCentralManager!
    var discoveredPeripheral: CBPeripheral?
    var transferCharacteristic: CBCharacteristic?
    var writeIterationsComplete = 0
    var connectionIterationsComplete = 0
    let defaultIterations = 5
    var data: Data = Data()
    
    
    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true])
    }
    
    func stopAction() {
        centralManager.stopScan()
    }
    
    private func cleanup() {
        guard let discoveredPeripheral = discoveredPeripheral,
              case .connected = discoveredPeripheral.state else { return }
        
        for service in (discoveredPeripheral.services ?? [] as [CBService]) {
            for characteristic in (service.characteristics ?? [] as [CBCharacteristic]) {
                if characteristic.uuid == TransferService.characteristicUUID && characteristic.isNotifying {
                    self.discoveredPeripheral?.setNotifyValue(false, for: characteristic)
                }
            }
        }
        
        centralManager.cancelPeripheralConnection(discoveredPeripheral)
    }
    
    private func writeData() {
        guard let discoveredPeripheral = discoveredPeripheral, let transferCharacteristic = transferCharacteristic
        else {
            return
        }
        
        while writeIterationsComplete < defaultIterations && discoveredPeripheral.canSendWriteWithoutResponse {
            writeIterationsComplete += 1
        }
        
        if writeIterationsComplete == defaultIterations {
            discoveredPeripheral.setNotifyValue(false, for: transferCharacteristic)
        }
    }
    
    private func retrievePeripheral() {
        
        let connectedPeripherals: [CBPeripheral] = (centralManager.retrieveConnectedPeripherals(withServices: [TransferService.serviceUUID]))
        
        os_log("Found connected Peripherals with transfer service: %@", connectedPeripherals)
        
        if let connectedPeripheral = connectedPeripherals.last {
            os_log("Connecting to peripheral %@", connectedPeripheral)
            self.discoveredPeripheral = connectedPeripheral
            centralManager.connect(connectedPeripheral, options: nil)
        } else {
            centralManager.scanForPeripherals(withServices: [TransferService.serviceUUID], options: nil)
        }
    }
}


extension CentralViewModel: CBCentralManagerDelegate {
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOn:
            print(".powerOn")
            retrievePeripheral()
            return
            
        case .poweredOff :
            print(".powerOff")
            return
            
        case .resetting:
            print(".restting")
            return
            
        case .unauthorized:
            print(".unauthorized")
            return
            
        case .unknown:
            print(".unknown")
            return
            
        case .unsupported:
            print(".unsupported")
            return
            
        @unknown default:
            print("A previously unknown central manager state occurred")
            return
        }
    }
    
    // 検索したペリフェラルに接続
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        
        guard RSSI.intValue >= -50 else {
            return
        }
        
        if discoveredPeripheral != peripheral {
            discoveredPeripheral = peripheral
            centralManager.connect(peripheral, options: nil)
        }
    }
    
    // サービスを検索
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        centralManager.stopScan()
        
        connectionIterationsComplete += 1
        writeIterationsComplete = 0
        
        data.removeAll(keepingCapacity: false)
        
        peripheral.delegate = self
        peripheral.discoverServices([TransferService.serviceUUID])
    }
    
    // ペリフェラルとの接続に失敗したとき
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        cleanup()
    }
    
    // ペリフェラルから切断されたとき
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        discoveredPeripheral = nil
        
        if connectionIterationsComplete < defaultIterations {
            retrievePeripheral()
        } else {
            print("Connection iterations completed")
        }
    }
}


extension CentralViewModel: CBPeripheralDelegate {
    
    // Characteristicを検索
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if error != nil {
            cleanup()
            return
        }
        
        guard let peripheralServices = peripheral.services else {
            return
        }
        
        for service in peripheralServices {
            peripheral.discoverCharacteristics([TransferService.characteristicUUID], for: service)
        }
    }
    
    // ペリフェラルがcharacteristicsを見つけたことを知らせる
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        
        if let error = error {
            print("Error discovering characteristics: \(error.localizedDescription)")
            cleanup()
            return
        }
        
        guard let serviceCharacteristics = service.characteristics else {
            return
        }
        
        for characteristic in serviceCharacteristics where characteristic.uuid == TransferService.characteristicUUID {
            transferCharacteristic = characteristic
            peripheral.setNotifyValue(true, for: characteristic)
        }
    }
    
    // ペリフェラルからデータが届いたことを知らせる
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if let error = error {
            print("Error discovering characteristics: \(error.localizedDescription)")
            cleanup()
            return
        }
        
        guard let characteristicData = characteristic.value,
            let stringFromData = String(data: characteristicData, encoding: .utf8) else {
            return
        }
        
        print("Received \(characteristicData.count) bytes: \(stringFromData)")
        
        if stringFromData == "EOM" {
            message = String(data: data, encoding: .utf8) ?? ""
            writeData()
            
        } else {
            data.append(characteristicData)
        }
    }

    // 指定されたcharacteristicの通知の要求をペリフェラルが受信したことを通知
    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        
        if let error = error {
            print("Error changing notification state: \(error.localizedDescription)")
            return
        }
       
        guard characteristic.uuid == TransferService.characteristicUUID else {
            return
        }
        
        if characteristic.isNotifying {
            // 通知開始
            print("Notification began on \(characteristic)")
        } else {
            // 通知が停止してるからペリフェラルとの接続を解除
            print("Notification stopped on \(characteristic). Disconnecting")
            cleanup()
        }
    }
    
    // ペリフェラルがcharacteristicのアップデートを送信する準備が整ったことを通知
    func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
        print("Peripheral is ready, send data")
        writeData()
    }
}


Peripheral

1. CBPeripheralManger のインスタンスを生成

var pripheralManager: CBPripheralManager!
peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true])

optionsのCBPeripheralManagerOptionShowPowerAlertKeyはCBCentralManagerOptionShowPowerAlertKey と同様のシステムです。

2. Delegateの追加

PeripheralViewModel.swift
extension PeripheralViewModel: CBPeripheralManagerDelegate {

    func peripheralManagerDidUpdateState(_ central: CBPeripheralManager) {
        switch peripheral.state {
        case .poweredOn:
            print(".powerOn")
            return
            
        case .poweredOff :
            print(".powerOff")
            return
            
        case .resetting:
            print(".restting")
            return
            
        case .unauthorized:
            print(".unauthorized")
            return
            
        case .unknown:
            print(".unknown")
            return
        
        case .unsupported:
            print(".unsupported")
            return
            
        @unknown default:
            print("A previously unknown peripheral manager state occurred")
            return
        }
    }
}

クラスには、 CBPeripheralManagerDelegate が必要です。
また、セントラルと同様に peripheralManagerDidUpdateState(_:) を記述する必要があります。

3. サービスを追加

PeripheralViewModel.swift
let characteristicUUID = CBUUID(string: "09876543-asdf-qwer-zxcv-1234567890cd")
private func setupPeripheral() {
    let transferCharacteristic = CBMutableCharacteristic(type: characteristicUUID,
                                                         properties: [.notify, .writeWithoutResponse],
                                                         value: nil,
                                                         permissions: [.readable, .writeable])
    // サービスを作成    
    let transferService = CBMutableService(type: TransferService.serviceUUID, primary: true)
    // サービスにcharacteristicsを追加
    transferService.characteristics = [transferCharacteristic]
    // peripharalManagerに追加
    peripheralManager.add(transferService)
        
    self.transferCharacteristic = transferCharacteristic
}

peripheralManagerDidUpdateState(_:) が .poweredOn を示すとサンプルでは、setupPeripheral() を呼び出して transferCharacteristic という CBMutableCharacteristic を生成しています。
その characteristic から CBMutableServiceを作成し、そのサービスをperipheralManagerに追加しています。

4. アドバタイズを開始

PeripheralViewModel.swift
let serviceUUID = CBUUID(string: "12345678-abcd-qwer-asdf-1234567890ab")

func switchChanged() {
    if toggleFrag {
        peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: serviceUUID]])
            
     } else {
        stopAction() 
     }
 }

func stopAction() {
    peripheralManager.stopAdvertising()
}

サービスの追加が完了したら、アドバタイズを開始します。
アドバタイズの開始には、startAdvertising(_ advertisementData: [String : Any]?) を使用します。
引数には、CBAdvertisementDataServiceUUIDsKey を指定します。 このキーはサービスUUIDを配列で指定します。
stopAdvertising() を使用すれば、アドバタイズを停止させることもできます。

Peripheralの全体コード

Peripheral
PeripheralViewModel
import Foundation
import CoreBluetooth
import os

class PeripheralViewModel: NSObject, ObservableObject {
    
    @Published var message: String = ""
    @Published var toggleFrag: Bool = false
    var peripheralManager: CBPeripheralManager!
    var transferCharacteristic: CBMutableCharacteristic?
    var connectedCentral: CBCentral?
    var dataToSend = Data()
    var sendDataIndex: Int = 0
    
    
    override init() {
        super.init()
        peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true])
    }
    
    func switchChanged() {
        if toggleFrag {
            peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [TransferService.serviceUUID]])
            
        } else {
            stopAction()
        }
    }
    
    func stopAction() {
        peripheralManager.stopAdvertising()
    }
    
    private func setUpPeripheral() {
        let transferCharacteristic = CBMutableCharacteristic(type: TransferService.characteristicUUID,
                                                             properties: [.notify, .writeWithoutResponse],
                                                             value: nil,
                                                             permissions: [.readable, .writeable])
        // サービスの作成
        let transferService = CBMutableService(type: TransferService.serviceUUID, primary: true)
        // サービスにcharacteristicsを追加
        transferService.characteristics = [transferCharacteristic]
        // periphralManagerに追加
        peripheralManager.add(transferService)
        
        self.transferCharacteristic = transferCharacteristic
    }
    
    static var sendingEOM = false
    
    private func sendData() {
        
        guard let transferCharacteristic = transferCharacteristic else {
            return
        }
        
        // EOMを送信する必要があるかどうかを確認
        if PeripheralViewModel.sendingEOM {
            let didSend = peripheralManager.updateValue("EOM".data(using: .utf8)!, for: transferCharacteristic, onSubscribedCentrals: nil)
            
            if didSend {
                PeripheralViewModel.sendingEOM = false
                print("Sent: EOM")
            }
            return
        }
        
        if sendDataIndex >= dataToSend.count {
            return
        }
        
        var didSend = true
        while didSend {
            
            var amountToSend = dataToSend.count - sendDataIndex
            if let mtu = connectedCentral?.maximumUpdateValueLength {
                amountToSend = min(amountToSend, mtu)
            }
           
            let chunk = dataToSend.subdata(in: sendDataIndex..<(sendDataIndex + amountToSend))
            
            didSend = peripheralManager.updateValue(chunk, for: transferCharacteristic, onSubscribedCentrals: nil)
            
            if !didSend {
                return
            }
            
            let stringFromData = String(data: chunk, encoding: .utf8)
            print("Sent \(chunk.count) bytes: \(String(describing: stringFromData))")
            
            sendDataIndex += amountToSend
           
            if sendDataIndex >= dataToSend.count {
                
                // 送信に失敗した場合、次回送信できるように設定
                PeripheralViewModel.sendingEOM = true
                
                // 送信
                let eomSent = peripheralManager.updateValue("EOM".data(using: .utf8)!, for: transferCharacteristic, onSubscribedCentrals: nil)
                if eomSent {
                    PeripheralViewModel.sendingEOM = false
                    print("Sent: EOM")
                }
                return
            }
        }
    }
}


extension PeripheralViewModel: CBPeripheralManagerDelegate {
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        switch peripheral.state {
        case .poweredOn:
            print(".powerOn")
            setUpPeripheral()
            return
            
        case .poweredOff :
            print(".powerOff")
            return
            
        case .resetting:
            print(".restting")
            return
            
        case .unauthorized:
            print(".unauthorized")
            return
            
        case .unknown:
            print(".unknown")
            return
            
        case .unsupported:
            print(".unsupported")
            return
            
        default:
            print("A previously unknown central manager state occurred")
            return
        }
    }
    
    // characteristicが読み込まれたときにキャッチし、データ送信を開始
    func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
        print("Central subscribed to characteristic")
        
        if let message = message.data(using: .utf8) {
            dataToSend = message
        }
     
        sendDataIndex = 0
        
        connectedCentral = central
        
        // 送信開始
        sendData()
    }
    
    // セントラルが停止したときに呼び出される
    func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
        print("Central unsubscribed from characteristic")
        connectedCentral = nil
    }
    
    // peripheralManagerが次のデータを送信する準備ができたときに呼び出される
    func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
        sendData()
    }
    
    // peripheralManagerがcharacteristicsへの書き込みを受信したときに呼び出される
    func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
        for aRequest in requests {
            guard let requestValue = aRequest.value,
                  let stringFromData = String(data: requestValue, encoding: .utf8) else {
                continue
            }
            
            print("Received write request of \(requestValue.count) bytes: \(stringFromData)")
            message = stringFromData
        }
    }
}

View

struct ContentView: View {
    var body: some View {
        
        NavigationStack {
            VStack {
                NavigationLink(destination: CentralView()) {
                    Text("Central")
                }
                .buttonStyle(.borderedProminent)
                .padding()
                
                NavigationLink(destination: PeripheralView()) {
                    Text("Peripharal")
                }
                .buttonStyle(.borderedProminent)
                .padding()
            }
        }
    }
}

struct CentralView: View {
    @StateObject var central: CentralViewModel = CentralViewModel()
    
    var body: some View {
        
        Text(central.message)
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
	    .padding(20)
	    .onDisappear {
		central.stopAction()
	    }
    }
}

struct PeripheralView: View {
    @StateObject var peripheral: PeripheralViewModel = PeripheralViewModel()
    
    var body: some View {
        
        VStack {
            TextEditor(text: $peripheral.message)
                .padding(20)
            
            Toggle("Advertising", isOn: $peripheral.toggleFrag)
                .padding(20)
                .onChange(of: peripheral.toggleFrag) { _ in
                    peripheral.switchChanged()
                }
        }
	.onDisappear {
	    peripheral.stopAction()
        }
    }
}

Storyboard で作られていたものを SwiftUI に直しただけです。
少し私好みのViewになっていますが、大体同じViewになっていると思います。
.onDisappear はViewが消えるタイミングで実行されるコールバックメソッドです。
CentralView では stopScan() を実行し、PeripheralView では stopAdvertising() を実行しています。

最後に

今回は、Appleが用意してくれているサンプルをSwiftUIに書き換える形で解説させていただきました。
GitHubでSwiftUIに書き換えたサンプルを公開しています。
Appleのサンプルとともに参考にしてください。AppleのサンプルはUIKitで作られていますが、BLEの基本的な使い方は変わらないので、参考になると思います。

https://developer.apple.com/documentation/corebluetooth/transferring_data_between_bluetooth_low_energy_devices

https://github.com/inotomo06/CorebluetoothBLESwiftUI

Discussion