iOS 端末での Bluetooth 接続(Background)について

# はじめに
はじめまして。
リンクラフト株式会社で開発業務をしている akaishi といいます。
アプリ開発をしたり、社内のツールを作ったり色々と雑多にエンジニアをやっております。

この記事は [LINCRAFT Advent Calendar 2023](https://qiita.com/advent-calendar/2023/lincraft) の21日目の記事となります。

どうでもいい情報ですが、自分はリンクラフトのバイオハザード部の部長も務めております。
部員は自分1人だけですが！
皆さんもバイオやりましょう！！

# iOS 端末での Bluetooth 接続(Background)について🤔？
iOS での Bluetooth(BLE) の調査をする機会があったので備忘録も兼ねて記事にします。
本当は社内の Wiki にまとめようと思ってたのですが、色々あって書けていなかったので Advent Calendar で書いちゃえば一石二鳥なのでは！？と思い立った次第です。


### 調査する前の自分
以前(2016年ごろ)にも Swift で Bluetooth 案件(iOS アプリ開発)を対応したことがあるのですが…
- Bluetooth Basic Rate/Enhanced Data Rate (BR/EDR) であまり参考にならない！
    - いわゆる Bluetooth Classic の規格だった！！
- 7年以上も昔の業務のためびっくりするほど色々覚えてない！
    - ビーコンとかって単語だけはおぼろげに覚えているレベル！！

こんな状況でして、以前 Bluetooth の作業をしたといってもほとんど役に立たない状況でした。

### 前提
本記事ではセントラルが iOS、ペリフェラルが周辺機器(イヤホン、スマートキー等)のケースでの話になります。
BLE についてやセントラル・ペリフェラルについての詳細は解説しませんので、気になる方はご自身でググってみたりしていただくようにお願いします。

あくまでも iOS での Background 時の Bluetooth についての内容をメインで解説します。

## Background での Bluetooth 接続🤔？
全然気にしていなかったのですが、この辺って一体どうなっているんでしょうか？
世の中にはバックグラウンドで Bluetooth 接続しているアプリって存在していますよね…！？
[スマートキーアプリ](https://apps.apple.com/jp/app/my-entrance/id1414794680)とか、一々スマホ開いてアプリ立ち上げとかしなくても鍵の開け閉め出来ている気が…

あれ、確か **iOS アプリは Background へ遷移したあとは自動的に Suspended 状態に移行**したような記憶。

**Suspended 状態 = アプリは動いていない = コード実行されない**

………？

## 🤔？？？
なんで動くのかよく分からない！

## iOS のライフサイクルを確認
まずはバックグラウンド時のアプリの挙動について把握する必要がありますね。
iOS のライフサイクルを確認してみましょう。
ということなので公式サイトを見てみます。

https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle

### アプリのライフサイクルに関連するデリゲートを抜粋
アプリのライフサイクルが変化する際は以下のデリゲートが実行されるようです。

**UIApplicationDelegate**
| デリゲート | 補足 |
|:-:|:-:|
| [application(_:didFinishLaunchingWithOptions:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622921-application) | 起動プロセスがほぼ完了し、アプリを実行する準備がほぼ整ったことを通知 |
| [applicationWillTerminate(_:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623111-applicationwillterminate) | アプリが終了するときに通知 |

**UISceneDelegate**
| デリゲート | 補足 |
|:-:|:-:|
| [sceneDidDisconnect(_:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3197916-scenediddisconnect) | アプリからシーンを削除したことを通知 |
| [sceneDidBecomeActive(_:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3197915-scenedidbecomeactive) | シーンがアクティブになり、ユーザーイベントに応答するようになったことを通知 |
| [sceneWillResignActive(_:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3197919-scenewillresignactive) | シーンがアクティブ状態を終了し、ユーザーイベントへの応答を停止しようとしていることを通知 |
| [sceneWillEnterForeground(_:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3197918-scenewillenterforeground) | シーンがフォアグラウンドで実行を開始し、ユーザーに見えるようになることを通知 |
| [sceneDidEnterBackground(_:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3197917-scenedidenterbackground) | シーンがバックグラウンドで実行され、画面上にないことを通知 |

### 実際にアプリを作って確認してみる
今回検証する環境はこちらです。
| 項目 | 値 |
|:-:|:-:|
| Xcode | Xcode 14.2(Build version 14C18) |
| 検証端末 | iPhone 12(iOS 15.1) |

まずは Xcode で検証用プロジェクトを作ります。
「iOS」の「App」で作成しましょう。
![P1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624911/fd088006-6233-e3ef-b148-8c6cd5d60bd8.png)

プロジェクト名称は「BleSample」とでもしておきます。
![P2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624911/83026ef0-5585-6d34-d154-b235f4d15944.png)

### ソースコード
では自動生成されたファイルの内容を書き換えてみましょう。
以下のコードを対象のファイルにコピペして早速 Run してみます。
```swift:AppDelegate.swift
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        Logger.printLog("application(_:didFinishLaunchingWithOptions:)")
        
        return true
    }
    
    func applicationWillTerminate(_ application: UIApplication) {
        Logger.printLog("applicationWillTerminate(_:)")
    }
}

class Logger {
    static func printLog(_ log: String) {
        print("\(log) - \(Logger.getApplicationState())")
    }
    
    static private func getApplicationState() -> String {
        switch UIApplication.shared.applicationState {
        case .active:
            return "[active]"
        case .inactive:
            return "[inactive]"
        case .background:
            return "[background]"
        default:
            return "[unknown]"
        }
    }
}
```
```swift:SceneDelegate.swift
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func sceneDidDisconnect(_ scene: UIScene) {
        Logger.printLog("sceneDidDisconnect(_:)")
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        Logger.printLog("sceneDidBecomeActive(_:)")
    }

    func sceneWillResignActive(_ scene: UIScene) {
        Logger.printLog("sceneWillResignActive(_:)")
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        Logger.printLog("sceneWillEnterForeground(_:)")
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        Logger.printLog("sceneDidEnterBackground(_:)")
    }
}
```

<details><summary>iOS 13未満のチェックコード</summary><div>

| デリゲート | 補足 |
|:-:|:-:|
| [applicationDidBecomeActive(_:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622956-applicationdidbecomeactive) | アプリがActiveになった場合に通知 |
| [applicationWillResignActive(_:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622950-applicationwillresignactive) | アプリが非アクティブになった場合に通知 |
| [applicationDidEnterBackground(_:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground) | アプリがバックグラウンドになったことを通知 |
| [applicationWillEnterForeground(_:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623076-applicationwillenterforeground) | アプリがフォアグラウンドに入ろうとしていることを通知 |

iOS13 以降でもこのコードは使えなくはないのですが、info.plist を編集したりと少し手を加える必要があります。
```swift:AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        Logger.printLog("application(_:didFinishLaunchingWithOptions:)")
        
        return true
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        Logger.printLog("applicationDidBecomeActive(_:)")
    }

    func applicationWillResignActive(_ application: UIApplication) {
        Logger.printLog("applicationWillResignActive(_:)")
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        Logger.printLog("applicationDidEnterBackground(_:)")
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        Logger.printLog("applicationWillEnterForeground(_:)")
    }

    func applicationWillTerminate(_ application: UIApplication) {
        Logger.printLog("applicationWillTerminate(_:)")
    }
}

class Logger {
    static func printLog(_ log: String) {
        print("\(log) - \(Logger.getApplicationState())")
    }
    
    static private func getApplicationState() -> String {
        switch UIApplication.shared.applicationState {
        case .active:
            return "[active]"
        case .inactive:
            return "[inactive]"
        case .background:
            return "[background]"
        default:
            return "[unknown]"
        }
    }
}
```
</div></details>

### 実機で動作確認してみた結果

まずはアプリを起動してみます。
> application(_:didFinishLaunchingWithOptions:) - [background]
> sceneWillEnterForeground(\_:) - [background]
> sceneDidBecomeActive(\_:) - [inactive]

ふむふむ、以下のように遷移しているようです。

:::note info
**[Not Running] → [Background] → [Inactive] → [Active]**
:::

ではスワイプしてアプリをバックグラウンドへと追いやってみましょう。
> sceneWillResignActive(\_:) - [active]
> sceneDidEnterBackground(\_:) - [background]

ふむふむ、以下のように遷移しているようですね。

:::note info
**[Active] → [Background](その後 OS 側のタイミイングで [Suspended]に自動的に移行)**
:::

では次にアプリをバックグラウンドから復帰させてみましょう。
> sceneWillEnterForeground(\_:) - [background]
> sceneDidBecomeActive(\_:) - [inactive]

ふむふむ、以下のように遷移しています。

:::note info
**[Background] → [Inactive] → [Active]**
もしくは
**[Suspended] → [Background] → [Inactive] → [Active]**
:::

最後にアプリを終了させてみます。
> sceneWillResignActive(\_:) - [active]
> sceneDidDisconnect(\_:) - [background]
> applicationWillTerminate(\_:) - [background]

ふむふむ、以下のように遷移していますね。

:::note info
**[Active] → [Background] → [Not Running]**
:::

## 結論
![G1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624911/3da23641-b0ea-3c66-3bb0-9e46a8c5a5a3.png)
こちらの公式の図にある通りの通りの挙動になっていることが確認できました。
(Suspended・Not Running 状態はコード実行ができないのでログ出力がありません)

# バックグラウンド
iOS のライフサイクルを確認したので、次はバックグラウンドについて調べてみます。
iOS は通常、バックグラウンドでのアプリ実行を許可してくれません!
確かに、バックグラウンドでもアプリが好き勝手に動いたらセキュリティ的にもバッテリー的にも宜しくないのが予想できます。
でも GPS とか音楽再生とか、バックグラウンドでも色々と動かしたい機能もありますよね！
そんな場合に使用出来るのが Background Modes です！！

https://developer.apple.com/documentation/xcode/configuring-background-execution-modes

今回は iOS をセントラルとして動作させるため「Uses Bluetooth LE accessories」を有効にすることで Bluetooth 関連の処理がバックグラウンドでも実行可能になるようです。

## Background での Bluetooth 接続🤔？?
これでバックグラウンドで Bluetooth の機能が実行可能になりました、やったぜ！
…と思ったんですが、あれ、Suspended 状態になった場合はどうなるんでしょうか…？

**Suspended 状態 = アプリは動いていない = コード実行されない**

………？

## 🤔？？？？？
なんで動くのか全然分からない！

# iOS での Bluetooth
とりあえず iOS のアプリライフサイクル、Background Modes は最低限の知識ですが把握出来ましたね、多分！
次は iOS での Bluetooth について公式の情報を確認してみましょう。
とりあえず困ったら公式の情報を見るのが良しです、多分！

https://developer.apple.com/bluetooth/

Bluetoothの詳細については[こちら](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/AboutCoreBluetooth/Introduction.html)に記載があります。
Background 時の Bluetooth についての詳細は[こちら](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html#//apple_ref/doc/uid/TP40013257-CH7-SW1)にあります。

### 公式ドキュメントをチェック
重要なところを引用してみます、が、長いので折りたたみしてます。
<details><summary>公式ドキュメント</summary><div>

(翻訳は全てDeepLにて行なっております。)

> By default, many of the common Core Bluetooth tasks—on both the central and peripheral side—are disabled while your app is in the background or in a suspended state. That said, <font color="red">you can declare your app to support the Core Bluetooth background execution modes to allow your app to be woken up from a suspended state to process certain Bluetooth-related events.</font> Even if your app doesn’t need the full range of background processing support, it can still ask to be alerted by the system when important events occur.
>
> デフォルトでは、アプリがバックグラウンドまたはサスペンド状態にある間は、セントラル側とペリフェラル側の両方で一般的なCore Bluetoothタスクの多くが無効になります。とはいえ、<font color="red">Core Bluetoothのバックグラウンド実行モードをサポートするようにアプリを宣言することで、特定のBluetooth関連イベントを処理するためにアプリをサスペンド状態から呼び出すことができます。</font>アプリがバックグラウンド処理サポートの全範囲を必要としない場合でも、重要なイベントが発生したときにシステムから警告を受けるように要求することができます。

:::note info
アプリが Suspended 状態にあったとしてもアプリを呼び出す事ができる
:::

> When an app that implements the central role includes the UIBackgroundModes key with the bluetooth-central value in its Info.plist file, the Core Bluetooth framework allows your app to run in the background to perform certain Bluetooth-related tasks. While your app is in the background you can still discover and connect to peripherals, and explore and interact with peripheral data. In addition, <font color="red">the system wakes up your app when any of the CBCentralManagerDelegate or CBPeripheralDelegate delegate methods are invoked, allowing your app to handle important central role events, such as when a connection is established or torn down, when a peripheral sends updated characteristic values, and when a central manager’s state changes.</font>
>
> centralの役割を実装したアプリが、Info.plistファイルにbluetooth-centralの値を持つUIBackgroundModesキーを含めると、Core Bluetoothフレームワークは、アプリがバックグラウンドで実行され、特定のBluetooth関連タスクを実行できるようにします。アプリがバックグラウンドで動作している間も、周辺機器の検出や接続、周辺機器のデータの探索や操作が可能です。<font color="red">さらに、CBCentralManagerDelegate または CBPeripheralDelegate デリゲートメソッドが呼び出されると、システムはアプリをウェイクアップし、接続が確立または切断されたとき、ペリフェラルが更新された特性値を送信したとき、セントラルマネージャの状態が変化したときのような重要なセントラルロールイベントをアプリが処理できるようにします。</font>

:::note info
アプリが Suspended 状態でも Bluetooth 関連のイベントを検知した場合、アプリは Suspended から Background へと遷移してコード実行が可能になる
:::

> <font color="red">Upon being woken up, an app has around 10 seconds to complete a task. Ideally,</font> it should complete the task as fast as possible and allow itself to be suspended again. <font color="red">Apps that spend too much time executing in the background can be throttled back by the system or killed.</font>
>
> <font color="red">スリープ解除後、アプリは約10秒でタスクを完了する。</font>理想的なのは、できるだけ早くタスクを完了し、再びサスペンドできるようにすることだ。<font color="red">バックグラウンドでの実行時間が長すぎるアプリは、システムによってスロットルバックされるか、強制終了される可能性がある。</font>

:::note info
Suspended からアプリが起動され Background へと移行した際、10秒程度コード実行が可能になる
:::
:::note warn
ただしなるべく早く処理を終了させることが望ましい
:::

> <font color="red">Even if your app supports one or both of the Core Bluetooth background execution modes, it can’t run forever. At some point, the system may need to terminate your app to free up memory for the current foreground app—causing any active or pending connections to be lost, for instance.</font>
>
> <font color="red">アプリがCore Bluetoothのバックグラウンド実行モードの1つまたは両方をサポートしていても、永遠に実行できるわけではありません。ある時点で、システムは現在のフォアグラウンドアプリのメモリを解放するためにアプリを終了する必要があるかもしれません。</font>

:::note warn
Bluetooth バックグラウンドをサポートしたアプリでも、OS 側の都合でアプリが終了(Not Running)させられてしまう可能性を考慮する
:::
</div></details>

## 公式ドキュメントからの情報を要約！
:::note info
- Core Bluetooth のバックグラウンド実行モードをサポートするようにアプリを宣言すればバックグラウンドでもコード実行が可能になる
    - Background 状態でもコード実行が可能になる
    - Suspended 状態でもアプリが OS 側から呼び出され、コード実行が可能になる
        - その際は Suspended 状態から Background 状態へと遷移する
        - 10秒程度のコード実行が可能になる
:::
:::note info
- バックグラウンド実行モードをサポートしているアプリでも必ずバックグラウンド処理が行われるわけではない
    - OS 都合でアプリが終了させられる可能性を考慮する
:::

# 本当にそうなのか🤔？
気になりますよね。
何事も実験してエビデンスを確認してみないといけませんね。
公式の情報が間違うこともあるので！！

# 実際にアプリを作ってみて確認！！

今回は家にあったこのデバイス(ネックスピーカー)をペリフェラルとして検索してみます。
バイオする時にめっちゃ使ってます。
まさに「そこを歩く、という恐怖。」を体感できる素晴らしいガジェットです！
皆さんぜひ購入してバイオしましょう！

https://www.sony.jp/active-speaker/products/SRS-NS7/

## プロジェクト設定

まずは Background Modes を有効にしましょう。
①「Signing & Capabilities」の②「+ Capability」ボタンをクリックします。
![S1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624911/65965e68-a4e6-b819-9d6e-ea5518287e4e.png)

Capabilities 追加ダイアログが表示されるので「Background Modes」を選択します。
![S2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624911/ce40b667-429c-84f8-87ff-9643139f05cc.png)

「Background Modes」が追加されます。
今回は iOS デバイスをセントラル化するので「Uses Bluetooth LE accessories」にチェックを入れます。
![S3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624911/6b53c386-031c-8025-eb14-2d1788274ea8.png)
これでバックグラウンドで Bluetooth 関連のイベントが処理出来るようになりました。
簡単ですね。

### iOS13以降
iOS13 以降では info.plist に「NSBluetoothAlwaysUsageDescription」を追加しないと実行時エラーとなってしまいます。
iOS13 以降であれば「NSBluetoothAlwaysUsageDescription」を追加しておきましょう。
```info.plist
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth使います</string>
```
![S4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624911/495e3227-006a-8051-38cd-a6e610e4c9f9.png)

## デザイン
UI は適当にボタンだけ配置します。
システムはシンプルなのが最高だとどこかで聞きました！
![U1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624911/ab1eea17-cf9b-5d66-f0d1-2a79875e7c1a.png)
これはシンプルですね。

## Bluetooth 実装に必要な機能
### Bluetooth に関連するデリゲートを抜粋
Bluetooth 関連の処理は以下のデリゲートが使用可能です。

**CBCentralManagerDelegate**
| デリゲート | 補足 |
|:-:|:-:|
| [centralManager(_:didDiscover:advertisementData:rssi:)](https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518937-centralmanager) | セントラルマネージャがデバイスをスキャンしている間に周辺機器を発見したことを通知 |
| [centralManager(_:didConnect:)](https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518969-centralmanager) | セントラルマネージャが周辺機器に接続していることを通知 |
| [centralManager(_:didDisconnectPeripheral:error:)](https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518791-centralmanager) | セントラルマネージャが周辺機器から切断したことを通知 |
| [centralManager(_:didFailToConnect:error:)](https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518988-centralmanager) | セントラルマネージャが周辺機器との接続の作成に失敗したことを通知 |
| [centralManagerDidUpdateState(_:)](https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518888-centralmanagerdidupdatestate) | セントラルマネージャーの状態が更新されたことを通知 |

**CBPeripheralDelegate**
| デリゲート | 補足 |
|:-:|:-:|
| [peripheral(_:didDiscoverServices:)](https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate/1518744-peripheral) | 周辺サービスの発見に成功したことを通知 |
| [peripheral(_:didDiscoverCharacteristicsFor:error:)](https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate/1518821-peripheral) | ペリフェラルがキャラクタリスティック(ペリフェラル機器がセントラル機器に対して公開している共有データ構造)を見つけたことを通知 |

## ソースコード
では実際に↑各種デリゲートを使用してバックグラウンドでの Bluetooth の動作確認を行ってみましょう。
とりあえず必要最低限の機能で確認してみます！

```swift:ViewController.swift
import CoreBluetooth
import UIKit

class ViewController: UIViewController {
    var centralManager: CBCentralManager!
    var peripheral: CBPeripheral!

    override func viewDidLoad() {
        Logger.printLog("viewDidLoad()")
        
        super.viewDidLoad()
        
        self.centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
    }

    @IBAction func onClickScanBluetoothDeviceButton(_ sender: Any) {
        Logger.printLog("onClickScanBluetoothDeviceButton(_:)")

        // withServicesを指定して対象のデバイス(ペリフェラル機)のみ検索
        let service = CBUUID(string: "検索対象ペリフェラル機のUUID")
        self.centralManager.scanForPeripherals(withServices: [service], options: nil)
        
        Logger.printLog("scanForPeripherals()")
    }
}

extension ViewController: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        var state = "centralManagerDidUpdateState(_:) : "
        
        switch central.state {
        case .poweredOn:
            state += "[poweredOn]"
        case .poweredOff:
            state += "[poweredOff]"
        case .unauthorized:
            state += "[unauthorized]"
        case .unsupported:
            state += "[unsupported]"
        case .resetting:
            state += "[resetting]"
        default:
            state += "[unknown]"
        }
        
        Logger.printLog(state)
    }

    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String: Any],
                        rssi RSSI: NSNumber) {
        Logger.printLog("centralManager(_:didDiscover:advertisementData:rssi:)")
        
        print(peripheral.debugDescription)
        
        // 本来は想定通りのペリフェラルが検索されたかどうかチェックするのが良い
        self.centralManager.connect(peripheral, options: nil)
        self.peripheral = peripheral
        self.centralManager.stopScan()
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        Logger.printLog("centralManager(_:didConnect:)")
        
        self.peripheral.delegate = self
        self.peripheral.discoverServices(nil)
    }
    
    func centralManager(_ central: CBCentralManager,
                        didDisconnectPeripheral peripheral: CBPeripheral,
                        error: Error?) {
        Logger.printLog("centralManager(_:didDisconnectPeripheral:error:)")
    }
    
    func centralManager(_ central: CBCentralManager,
                        didFailToConnect peripheral: CBPeripheral, error: Error?) {
        Logger.printLog("centralManager(_:didFailToConnect:error:)")
    }
}

extension ViewController: CBPeripheralDelegate {
    func peripheral(_ peripheral: CBPeripheral,
                    didDiscoverServices error: Error?) {
        Logger.printLog("peripheral(_:didDiscoverServices:error:)")

        self.peripheral.discoverCharacteristics(nil, for: (peripheral.services?.first)!)
    }

    func peripheral(_ peripheral: CBPeripheral,
                    didDiscoverCharacteristicsFor service: CBService,
                    error: Error?) {
        Logger.printLog("peripheral(_:didDiscoverCharacteristicsFor:error:)")
    }
}
```
ボタンをクリックしたらデバイスの検索を開始し、見つかり次第接続を行うだけの機能になります。

また先ほどの「SceneDelegate.swift」に以下の要素を追加します。
Background 中にログを出力させるコードになります。
```diff_swift:SceneDelegate.swift
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
+   var backgroundProcessing: Bool = false

    func sceneDidDisconnect(_ scene: UIScene) {
        Logger.printLog("sceneDidDisconnect(_:)")
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        Logger.printLog("sceneDidBecomeActive(_:)")
    }

    func sceneWillResignActive(_ scene: UIScene) {
        Logger.printLog("sceneWillResignActive(_:)")
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        Logger.printLog("sceneWillEnterForeground(_:)")
+
+       self.backgroundProcessing = false
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        Logger.printLog("sceneDidEnterBackground(_:)")
+
+       print("Execute background processing")
+
+       self.backgroundProcessing = true
+       DispatchQueue.global(qos: .background).async {
+           var i = 0
+           while self.backgroundProcessing {
+               print("Background process...[\(i)]")
+
+               i += 1
+               Thread.sleep(forTimeInterval: 1)
+           }
+
+           print("Background process complete")
+       }
    }
}
```
これで Background 状態の時に約1秒間隔でログが表示されるようになります。


## 実機で実践

まずはアプリを起動します。
> application(\_:didFinishLaunchingWithOptions:) - [background]
> viewDidLoad() - [background]
> sceneWillEnterForeground(\_:) - [background]
> sceneDidBecomeActive(\_:) - [inactive]
> centralManagerDidUpdateState(_:) : [poweredOn] - [active]

ボタンをクリックして Bluetooth デバイスの検索を開始します。
> onClickScanBluetoothDeviceButton(\_:) - [active]
> scanForPeripherals() - [active]

そのままスワイプして Background 状態へとアプリを移行します。
> sceneWillResignActive(\_:) - [active]
> sceneDidEnterBackground(\_:) - [background]
> Execute background processing
> Background process...[0]

ログ出力が止まりました。
Suspended 状態へと遷移したと判断します。

ここでスピーカーの電源を入れて Bluetooth 接続モードにすると…

> Background process...[1]
> centralManager(\_:didDiscover:advertisementData:rssi:) - [background]
> \<CBPeripheral: 0x283058000, identifier = {検索されたペリフェラル機のID}, name = (null), mtu = 0, state = disconnected\>
> centralManager(_:didConnect:) - [background]
> Background process...[2]
> peripheral(\_:didDiscoverServices:error:) - [background]
> peripheral(\_:didDiscoverCharacteristicsFor:error:) - [background]

おお！
**Suspended 状態からアプリが復帰し Background 状態で処理が継続されていることが確認できました。**
ペリフェラル機が見つかったので「centralManager(\_:didDiscover:advertisementData:rssi:)」デリゲートが実行され、ペリフェラル機に接続を行います。
その後ペリフェラル機に接続出来たので「centralManager(\_:didConnect:)」デリゲートが呼ばれていますね、やったぜ！

そのままログ出力がしばらく続きます。

> Background process...[3]
> Background process...[4]
> Background process...[5]
> Background process...[6]
> Background process...[7]
> Background process...[8]
> Background process...[9]
> Background process...[10]
> Background process...[11]

ここでログの出力が止まりました。
Suspended 状態へ移行したようですね。

今度はこの状態でスピーカーの電源をオフにしてみます。
> Background Process...[12]
> centralManager(\_:didDisconnectPeripheral:error:) - [background]

Suspended 状態でもちゃんと「centralManager(\_:didDisconnectPeripheral:error:)」デリゲートが呼ばれペリフェラル機の切断を検知出来ています。

> Background process...[13]
> Background process...[14]
> Background process...[15]
> Background process...[16]
> Background process...[17]
> Background process...[18]
> Background process...[19]
> Background process...[20]

その後しばらくしてログの出力が止まりました。
確かに Background 状態へ遷移した際に10秒程度のコード実行が可能になっていますね。

## 検証結果
Suspended 状態にあるアプリでも Background Modes を宣言することで対象のデリゲートが検知された場合にコード実行が可能になることが確認できました！！

# iOS 端末での Bluetooth 接続(Background)について😉！
色々と検証してみた結果をまとめます！

## 結論！
:::note info
- Background Modes を設定すれば Background 状態でもコード実行が可能になる
- アプリが Suspended 状態でも特定のイベントが通知された場合、アプリは Background 状態へと自動的に移行しイベントを処理する事が可能になる
- Suspended から Background へと移行した際に10秒程度コード実行が可能になる
    - ただし OS 制御のためなるべく短時間で完結させる
:::
:::note warn
- Background Modes を設定したとしても必ずバックグラウンド時にデリゲートが呼ばれる保証はない
    - OS 側の都合等でアプリが終了させられてしまう可能性があるのを考慮する
:::

## +α
アプリが Suspended 状態 ではなく Not Running 状態になったとしても Bluetooth デバイスとのやり取りを継続させることが可能です。
実際にはアプリは Not Running 状態になり終了しているが、自動的にアプリ起動時に設定を引き継げる **state preservation and restoration 機能**については、また時間のある時に解説できればと思います。

https://developer.apple.com/documentation/uikit/view_controllers/preserving_your_app_s_ui_across_launches/


# 一緒に働く仲間を募集中です！
リンクラフト株式会社では、組織拡大に伴い積極的な採用活動を行っています。
少しでも興味がある方はぜひご連絡ください。

▽会社ホームページ
https://lincraft.co.jp/
▽Instagram
https://www.instagram.com/lincraft.inc/
▽ご応募はこちらより
https://lincraft.co.jp/recruit

※カジュアル面談も受付中です。ご希望の方はHPのお問い合わせフォームよりご連絡ください。
※もちろんバイオ部員も随時募集中です。