【iOS】iBeacon の Region として iPhone を利用する

## 環境
- Xcode 12.3
- iPhone 6 Plus
- iOS 12.6

## 目的
- iBeacon を用いた勤怠管理システムを構築するための実験として現在利用していない試験機 (iOS 12.6)の iPhone を iBeacon の Region として動作させたい
- いずれは raspberry pi に置き換える予定なので最低限動けばOK

## ポイント

### Info.plist の設定
- iOS 13 以降では `Privacy - Bluetooth Always Usage Description` の設定が必要
- 今回は iOS 12.6 の端末で動けば良いので実は不要だが一応設定

![スクリーンショット 2021-01-26 16.06.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1045667/1b43547d-ce7d-7701-465e-e674e35f16fb.png)


### Region の作成

- Region: Central 側が出入りを確認可能な「地域」
- UUID（地域を一意に定めるもの）、major・minor（ビーコン識別用の値）、identifier（内部管理用のIDで信号には含まれない）が存在
- iOS 13前後で仕様が異なるため今回は条件分けを行った

```swift:PeripheralManager.swift
    // Region を作成する
    private func createBeaconRegion() -> CLBeaconRegion {
        
        guard let uuid = UUID(uuidString: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") else { fatalError() }
        
        let identifier = "xxx.xxxx.xxxxx.xxxxx"
        
        if #available(iOS 13.0, *) {
            
            return CLBeaconRegion(uuid: uuid, identifier: identifier)
        }
        else {
            
            return CLBeaconRegion(proximityUUID: uuid, identifier: identifier)
        }
    }
```

### CBPeripheralManagerDelegate

- `CBPeripheralManager` インスタンスを通じて通信を行うが、その際 Delegate を実装しておく必要がある
- `func peripheralManagerDidUpdateState(_:)` は `CBPeripheralManager` インスタンス の状態が変化した際に呼ばれる
    - `CBPeripheralManager` インスタンスが生成されたタイミングでも呼ばれるため初期化処理には注意が必要
    - 今回は単純に BLE ON になったらアドバタイズ開始、それ以外ではアドバタイズ停止するように
- `CBPeripheralManagerDelegate` を適用するクラスは `NSObjectProtocol` に準拠しておく必要があるため注意

```swift:PeripheralManager.swift
extension PeripheralManager: CBPeripheralManagerDelegate {
    
    // Bluetooth の電源ON/OFF または 接続状態が切り替わった場合に呼び出されるメソッド
    // CBPeripheralManager のインスタンスが生成された時点で一度呼び出される
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        
        switch peripheral.state {
        case .poweredOn: self.startAdvertising()
        default: self.stopAdvertising()
        }
    }
}
```

### その他
- `PeripheralManager` クラスをシングルトンとして保持
    - 単一の Region としての役割のみを期待
    - （ないとは思うが）拡張する際にはどの画面からでも同一のマネージャクラスにアクセスできるように
- `startAdvertising()` `stopAdvertising()` を外部から参照可能に
    - （ないとは思うが）拡張する際に利便のよいように
- `setupManager()` を公開
    - `CBPeripheralManager` インスタンスが生成されるとその時点で状態監視が始まる
    - `PeripheralManager` インスタンスと同時に `CBPeripheralManager` インスタンスを生成すると処理の順序が交錯し分かりづらい

## 実装全体

```swift:PeripheralManager.swift
import CoreLocation
import CoreBluetooth
import Foundation


// MARK: - PeripheralManager

final class PeripheralManager: NSObject {
    
    static let shared = PeripheralManager()
    
    func setupManager() {
        
        if self.manager == nil {
            
            let region = self.createBeaconRegion()
            
            self.beaconPeripheralData = region.peripheralData(withMeasuredPower: nil)
            self.manager = CBPeripheralManager(delegate: self, queue: nil)
        }
    }
    
    // アドバタイズを開始する
    func startAdvertising() {
        
        if let manager = self.manager,
           let data = self.beaconPeripheralData as? [String : Any] {
            
            manager.startAdvertising(data)
        }
    }
    
    // アドバタイズを停止する
    func stopAdvertising() {
        
        if let manager = self.manager {
            
            manager.stopAdvertising()
        }
    }
    
    
    // MARK: - Private
    
    private var beaconPeripheralData: NSDictionary?
    
    private var manager: CBPeripheralManager?
    
    // Region を作成する
    private func createBeaconRegion() -> CLBeaconRegion {
        
        guard let uuid = UUID(uuidString: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") else { fatalError() }
        
        let identifier = "xxx.xxxx.xxxxx.xxxxx"
        
        if #available(iOS 13.0, *) {
            
            return CLBeaconRegion(uuid: uuid, identifier: identifier)
        }
        else {
            
            return CLBeaconRegion(proximityUUID: uuid, identifier: identifier)
        }
    }
}


// MARK: - CBPeripheralManagerDelegate

extension PeripheralManager: CBPeripheralManagerDelegate {
    
    // Bluetooth の電源ON/OFF または 接続状態が切り替わった場合に呼び出されるメソッド
    // CBPeripheralManager のインスタンスが生成された時点で一度呼び出される
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        
        switch peripheral.state {
        case .poweredOn: self.startAdvertising()
        default: self.stopAdvertising()
        }
    }
}

```

```swift:AppDelegate.swift

import UIKit


// MARK: - AppDelegate

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    
    // MARK: - UIApplicationDelegate

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        // PeripheralManager を設定する
        PeripheralManager.shared.setupManager()
        return true
    }
}