iOSでBeaconの振る舞いを確認する

確認には、以下のリポジトリにあるソースで行いました。
また、iOS10.3端末の実機にて評価試験をおこなっております。

https://github.com/nosaka/ios_playground

# Beacon Peripheralを用意する

iOSでBeaconとしてアドバタイズを行う場合、どうもバックグラウンドでアドバタイズすることができないようです。また、検証用のアプリでは、自身のアドバタイズは検知できません。
iOSをBeacon Peripheral側の端末として用いる場合は、端末を2台用意し、Beacon Peripheral側の端末の設定＞一般＞自動ロックを「しない」に設定してください。

検証用のアプリはデフォルトでは以下の仕様としています。

|UUID|
|:---|
|36E54BC0-AA81-4D4B-A3C9-B0FF983D24E2|

UUIDなど変更したい場合はuuidgenで別途生成して次のクラスを変更してください。

```swift:AppBeacon.swift
class AppBeacon {
    
    static let proximityUUID: UUID          = UUID(uuidString: "36E54BC0-AA81-4D4B-A3C9-B0FF983D24E2")!
    static let identifier: String           = "ns.me.region"
    static let localName: String            = "NS example beacon"
    static var beaconRegion: CLBeaconRegion {
        return CLBeaconRegion(proximityUUID: self.proximityUUID, identifier: self.identifier)
    }
    static var advertisingData:[String : Any]? {
        // iBeaconフォーマット
        let peripheralData = self.beaconRegion.peripheralData(withMeasuredPower: nil)
        peripheralData.setValue(self.localName, forKey: CBAdvertisementDataLocalNameKey)
        return NSDictionary(dictionary: peripheralData) as? [String : Any]
    }
}
```

検証用のアプリでを開き、一覧内の「Beacon発信」を選択すると、「Beacon発信」画面が表示されます。
本画面を表示中のあいだ、上記で指定された内容でBeaconアドバタイズを行います。
なお、主なBeaconのPeripheral側の処理は ```BeaconPeripheralManager``` で行っています。

![IMG_0053.png](https://qiita-image-store.s3.amazonaws.com/0/125724/14edc336-4aa4-feae-6ac8-9cd3b8c09211.png)


もちろん、別途Beaconを購入すれば、それを利用することもできます。
色々見ましたが、LightBlueというアプリで簡単にUUID、Major、Minorが設定できる[こちら](http://www.products.braveridge.com/beacon/)が良さそう。(わたしは購入していませんが……)


# Beacon Centralを用意する

検証用のアプリでを開き、一覧内の「Beacon受信」を選択すると、「Beacon受信」画面が表示されます。
画面内のスイッチ内を切り替えると、明示的にタスクキルを行わない限り、バックグラウンド状態でもリージョンの出入を監視し、主要なイベント系は当画面の一覧に表示される実装としています。
なお、主なBeaconのCentral側の処理は ```BeaconCentralManager``` で行っています。

![IMG_0054.png](https://qiita-image-store.s3.amazonaws.com/0/125724/296106fb-1980-745e-931b-2c9bb9cda3d9.png)

ここでの「リージョン」はBeaconとは似て異なり、検証用のアプリではBeaconが発信するアドバタイズの範囲であるリージョンの監視を行います。
たとえば、サービスの内容にもよりますが、異なるBeacon端末でも同じアドバタイズを行う場合があります。
この場合は、このリージョンは監視対象として定めた複数のBeaconのアドバタイズの範囲の和集合を示すことになります。

![Group@2x.png](https://qiita-image-store.s3.amazonaws.com/0/125724/d724d605-3b16-17d8-2a1a-5fd1e7b8775b.png)

iOSでのBeacon Centralの実装はまず、このリージョンを監視を行い、つぎに範囲内のBeaconを検知するという順に処理する必要があります。

なお、別途、Beacon機器を持っており、そちらを利用する場合、Beacon Central側も
 ```AppBeacon``` のUUIDを監視対象とする実装となっておりますので、 ```AppBeacon``` をBeacon機器の仕様にあわせて編集する必要があります。

## Suspended状態のアプリケーションプロセスの発火

アプリケーションはバックグラウンドに移行後、一定時間経過したり、メモリが不足すると[Suspended](https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/TheAppLifeCycle/TheAppLifeCycle.html)状態となり、アプリケーションのプロセスが消失します。
この状態からアプリケーションのプロセスを発火させるためには条件があり、Beaconの出入りもそのうちのひとつです。

ただし、アプリケーションのプロセスが発火されたかといって、リージョンの監視をしてくれるかというとそうではなく、 ```CLLocationManager``` のインスタンスを生成する必要があります。
UIApplicationLaunchOptionsKeyによって起動後、 ```CLLocationManager``` のインスタンスを生成すると、 ```CLLocationManager:monitoredRegions``` に監視対象のリージョンが維持されていることがわかります。

検証用アプリでは ```AppDelegate``` 内に次のように ```BeaconCentralManager``` の初期化処理時に ```CLLocationManager``` のインスタンスを生成しています。

```swift:AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    
    if let value = launchOptions?[UIApplicationLaunchOptionsKey.location] as? Bool, value {
        BeaconCentralManager.default.initLaunch()
        return true
    }
    
    // 通知権限要求
    application.registerUserNotificationSettings(UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil))
    
    // View初期化処理
    self.initializeViews()
    
    return true
}
```


# 計測する

検証用のアプリは、Beacon Centralのイベントが発生した際にアプリ内DBに記録し、記録内容を「Beacon受信」画面内の一覧に時系列で表示するようになっています。

たとえば、Beacon Central側の端末をBeacon Peripheralに近づくと、「リージョンに入りました」とメッセージが記録されます。
逆に、Beacon Peripheralのアドバタイズを受信できなくなると「リージョンから出ました」とメッセージが記録されます。

検証用のアプリのメッセージの詳細な意味は以下のとおりです。

|メッセージ|説明|
|:---|:---|
|位置情報による起動|アプリケーションがBeaconの入出により発火した場合に記録されます。<br/>アプリケーションが[Suspended](https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/TheAppLifeCycle/TheAppLifeCycle.html)の場合でもBeaconの監視が行われるように実装されています。|
|プロセス終了|アプリケーションがタスクキルした場合などに記録されます。<br/>注意点として、アプリケーションの[Suspended](https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/TheAppLifeCycle/TheAppLifeCycle.html)を検知する手段はないため、あくまでユーザが明示的にタスクキルした場合などの証跡ログの目安としてご確認ください。|
|監視開始|「Beacon受信」画面のスイッチをオンに設定し、Beaconの監視を開始したときに記録されます。|
|監視停止|「Beacon受信」画面のスイッチをオフに設定し、Beaconの監視を停止したときに記録されます。|
|検知 圏内|リージョンの圏内にいることが決定された場合に記録されます。|
|検知 圏外|リージョンの圏外にいることが決定された場合に記録されます。|
|リージョンに入りました|リージョンに入った場合に記録されます。|
|リージョンから出ました|リージョンから出た場合に記録されます。|
|レンジングのBeacon数が変わりました|リージョン内のBeacn数が変化した場合に記録されます。(フォアグラウンドのみ)|

## リージョンに入った場合

![Region_01@2x.png](https://qiita-image-store.s3.amazonaws.com/0/125724/a4ddc489-02d5-8d3e-fac3-163a7fd06014.png)

### フォアグラウンド

アプリケーションがフォアグラウンド時のイベントは次のように通知されます。
リージョン範囲に入った時点のタイミングを起点とした数回試験時の平均経過秒数になります。

|項目|タイミング
|:---|:---|
|locationManager(_:didEnterRegion:)|約0秒|
|locationManager(_:didDetermineState:)|約0秒|
|locationManager(_:didRangeBeacons:)|約1秒|

**locationManager(_:didRangeBeacons:)について**

検知されたBeacon数が1として通知され、以降１秒間隔で通知されます。
1時間ほど放置しましたが、フォアグラウンド時は１秒間隔で通知され続けるようです。

### バックグラウンド

アプリケーションがバックグラウンド時のイベントは次のように通知されます。
リージョン範囲に入った時点のタイミングを起点とした数回試験時の平均経過秒数になります。

|項目|タイミング|
|:---|:---|:---|
|locationManager(_:didEnterRegion:)|0秒|
|locationManager(_:didDetermineState:)|0秒|
|locationManager(_:didRangeBeacons:)|約1秒|

**locationManager(_:didRangeBeacons:)について**

フォアグラウンド時とは異なり、 ```locationManager(_:didEnterRegion:)``` や、
 ```|locationManager(_:didDetermineState:)``` は通知されますが、フォアグラウンド時は通知されていた ```locationManager(_:didRangeBeacons:)``` は通知されなくなります。

ただし、リージョン圏外から圏内に移動した場合、
 ```locationManager(_:didDetermineState:)``` 内の処理で
 ```startRangingBeacons(in:)``` を呼び出していることもあり、約10秒ほど検知されたBeacon数が1として ```locationManager(_:didRangeBeacons:)``` が1秒間隔で通知されます。


このため、通常の実装(※)では、バックグラウンド状態でリージョン内のBeacon数を常に検知するのは不可能です。
※ たとえば、一定間隔でUUIDを切り替えることで別Beaconに切り替わるBeaconデバイスなら可能です。

## Suspended

アプリケーションがSuspended時のイベントは次のように通知されます。
リージョン範囲に入った時点のタイミングを起点とした数回試験時の平均経過秒数になります。

|項目|タイミング|
|:---|:---|
|application(_:didFinishLaunchingWithOptions:)|0秒|
|locationManager(_:didEnterRegion:)|0秒|
|locationManager(_:didDetermineState:)|0秒|
|locationManager(_:didRangeBeacons:)|1秒|

**application(_:didFinishLaunchingWithOptions:)について**
アプリケーションがSuspended状態から発火した場合、
 ```application(_:didFinishLaunchingWithOptions:)``` が
 ```UIApplicationLaunchOptionsKey``` を含む ```launchOptions``` で起動されます。

**locationManager(_:didRangeBeacons:)について**

バックグラウンド時と同様に約10秒ほど検知されたBeacon数が1として
 ```locationManager(_:didRangeBeacons:)``` が1秒間隔で通知された後、通知が停止します。

## リージョンから出た場合

![Region_02@2x.png](https://qiita-image-store.s3.amazonaws.com/0/125724/3c2bbbd9-69f8-4006-6e7c-c3b6fd43b1f2.png)

### フォアグラウンド

アプリケーションがフォアグラウンド時のイベントは次のように通知されます。
リージョン範囲から出た時点のタイミングを起点とした数回試験時の平均経過秒数になります。

|項目|タイミング|
|:---|:---|
|locationManager(_:didRangeBeacons:)|約10秒|
|locationManager(_:didExitRegion:)|約40秒|
|locationManager(_:didDetermineState:)|約40秒|

**locationManager(_:didRangeBeacons:)について**

リージョン範囲から出てから約10秒後に検知されたBeacon数が1から0に変化、
 ```locationManager(_:didExitRegion:)``` にて
 ```CLLocationManager:stopRangingBeacons(in:)``` が呼ばれるまで、Beacon数が0の状態で通知されます。

### バックグラウンド

アプリケーションがバックグラウンド時のイベントは次のように通知されます。
リージョン範囲から出た時点のタイミングを起点とした数回試験時の平均経過秒数になります。

|項目|タイミング|
|:---|:---|:---|
|locationManager(_:didRangeBeacons:)|約1秒|
|locationManager(_:didExitRegion:)|約40秒|
|locationManager(_:didDetermineState:)|約40秒|

## Suspended

アプリケーションがSuspended時のイベントは次のように通知されます。
リージョン範囲から出た時点のタイミングを起点とした数回試験時の平均経過秒数になります。

|項目|タイミング|
|:---|:---|
|application(_:didFinishLaunchingWithOptions:)|40秒|
|locationManager(_:didExitRegion:)|約40秒|
|locationManager(_:didDetermineState:)|約40秒|

**application(_:didFinishLaunchingWithOptions:)について**
iOSがリージョン範囲から出たと判断する場合は約40秒ほど必要とするようです。
iOSがリージョン範囲から出たと判断されると、アプリケーションがSuspended状態から
 ```application(_:didFinishLaunchingWithOptions:)``` が 
 ```UIApplicationLaunchOptionsKey``` を含む ```launchOptions``` で起動されます。

## 別Beaconのアドバタイズ検知に範囲に移動

![Region_03@2x.png](https://qiita-image-store.s3.amazonaws.com/0/125724/819bddac-5c64-32cc-9181-d792160428d6.png)

リージョンから出ずに同じリージョンを定める別Beacon端末のアドバタイズ検知に範囲に移動した場合、イベントは通知されません。

同じリージョンを定めるBeacon端末の数が増えた場合(図の差集合から積集合に移動した場合)でも、アプリケーションがSuspended時からプロセスを発火する契機とはなりませんでした。

つまり、各Beacon単位でiOSで処理したい場合は別リージョンとして取り扱う必要が出てきます。
逆を言えば、同じUUIDであれば同一リージョンとして取り扱われるため、たとえばイベント会場で入場者にBeaconをもたせた上で入出管理をする場合などは現在の仕様で問題なく使用できます。

# Tips

## UUID、Major、Minorでグループ化、区別化できる

 ```CLBeaconRegion``` のイニシャライザは以下にある通り、proximityUUIDのみの指定や、proximityUUIDとMajorのみの指定が可能です。

* [init(proximityUUID:identifier:)](https://developer.apple.com/documentation/corelocation/clbeaconregion/1621534-init)
* [init(proximityUUID:major:identifier:)](https://developer.apple.com/documentation/corelocation/clbeaconregion/1621475-init)
* [init(proximityUUID:major:minor:identifier:)](https://developer.apple.com/documentation/corelocation/clbeaconregion/1621392-init)

検証用のアプリではUUIDのみを指定していましたが、Beacon Peripheral側の端末を2台用意し、UUID、Major、Minorを次のように設定した場合は次のような振る舞いをします。

**Beacon Peripheral端末(A)**

|UUID|Major|Minor|
|:---|:---|:---|
|36E54BC0-AA81-4D4B-A3C9-B0FF983D24E2|0|1|

**Beacon Peripheral端末(B)**

|UUID|Major|Minor|
|:---|:---|:---|
|36E54BC0-AA81-4D4B-A3C9-B0FF983D24E2|0|2|

## Beacon Central側の端末でUUIDのみを監視対象とした場合

 ```AppBeacon``` の ```CLBeaconRegion``` をproximityUUIDのみ指定して監視します。

```swift:AppBeacon.swift
class AppBeacon {
    
    static let proximityUUID: UUID          = UUID(uuidString: "36E54BC0-AA81-4D4B-A3C9-B0FF983D24E2")!
    static let identifier: String           = "ns.me.region"
    static let localName: String            = "NS example beacon"
    static var beaconRegion: CLBeaconRegion {
        return CLBeaconRegion(proximityUUID: self.proximityUUID,  identifier: self.identifier)
    }
}
```

この場合、端末(A)、端末(B)ともに同一リージョンとして取り扱われ、[Suspended](https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/TheAppLifeCycle/TheAppLifeCycle.html)状態でも、圏外からどちらか片方のアドバタイズ受信可能な範囲に入れば、
 ```locationManager(_:didEnterRegion:)``` が通知されます。(もちろん、圏外に移動すれば ```locationManager(_:didExitRegion:)``` が通知されます)

このため、別のBeacon端末でもUUIDが同じであれば、グループ化して管理することができます。

## Beacon Central側の端末でUUID、Major、Minorを個別に監視対象とした場合


 ```AppBeacon``` の ```CLBeaconRegion``` を端末(A)、端末(B)のUUID、Major、Minorを個別に別リージョンとして定義し、 ```BeaconCentralManager:startMonitoring``` 内で監視対象としてそれぞれ追加します。

```swift:AppBeacon.swift
class AppBeacon {
    
    static let proximityUUID: UUID          = UUID(uuidString: "36E54BC0-AA81-4D4B-A3C9-B0FF983D24E2")!
    static let identifier: String           = "ns.me.region"
    static let localName: String            = "NS example beacon"
    static var beaconRegion: CLBeaconRegion {
        return CLBeaconRegion(proximityUUID: self.proximityUUID,  identifier: self.identifier)
    }
    static var beaconRegionA: CLBeaconRegion {
        return CLBeaconRegion(proximityUUID: self.proximityUUID, major: 0, minor: 1, identifier: "ns.me.region.A")
    }
    static var beaconRegionB: CLBeaconRegion {
        return CLBeaconRegion(proximityUUID: self.proximityUUID, major: 0, minor: 2, identifier: "ns.me.region.B")
    }
}
```

```swift:BeaconCentralManager.swift
class BeaconCentralManager: NSObject {
    /// モニタリング開始
    func startMonitoring() {
        let state = CLLocationManager.authorizationStatus()
        guard state == .authorizedAlways else {
            switch state {
            case .notDetermined:
                self.locationManager.requestAlwaysAuthorization()
            default:
                self.delegate?.requestLocationAlways()
                break
            }
            UserDefaultsUtil.monitoring = false
            return
        }
        UserDefaultsUtil.monitoring = true
        if self.isMonitoring() {
            // 既に監視開始中の場合は以降の処理を実施しない
            return
        }
        self.locationManager.startMonitoring(for: AppBeacon.beaconRegionA)
        self.locationManager.startMonitoring(for: AppBeacon.beaconRegionB)
        realmHelper.log(beaconCentralManager: .startMonitoring)
    }
    
    /// モニタリング停止
    func stopMonitoring() {
        UserDefaultsUtil.monitoring = false
        if !self.isMonitoring() {
            // 既に監視停止済の場合は以降の処理を実施しない
            return
        }
        self.locationManager.stopMonitoring(for: AppBeacon.beaconRegionA)
        self.locationManager.stopMonitoring(for: AppBeacon.beaconRegionB)
        realmHelper.log(beaconCentralManager: .stopMonitoring)
    }
}
```

この場合、端末(A)、端末(B)は異なるリージョンとして取り扱われ、端末(A)、端末(B)へのリージョン入出はそれぞれ個別に```locationManager(_:didEnterRegion:)``` 、
 ```locationManager(_:didExitRegion:)``` が通知されます。