iBeaconをつかって自動タイムカード登録


どこの会社でも勤務時間を把握するためにタイムカード登録をやっていると思います。

でもこのタイムカード登録って結構忘れちゃうんですよね。自分の会社の場合はWeb上からタイムカード登録ができるのですが、タイムカードのページを開くのを忘れてしまうので、もうそれならslack上からでタイムカード登録してみよう、ということでbot作ってみました。

![スクリーンショット 2016-01-31 16.09.04.png](https://qiita-image-store.s3.amazonaws.com/0/6808/c21227af-36a5-8c51-5408-841556e81267.png "スクリーンショット 2016-01-31 16.09.04.png")

ただもうこれすら面倒くさい、俺はもう「おは」と打つ時間も惜しんでプログラミングをしたいんだ、という思いに応えるべく自動でタイムカード登録できるアプリを作りました。
（完全社内用アプリなので、公開できないのですが、、、）

仕組みとしてはビーコンをつかってビーコンの領域に入ったのと、領域外に出たのを判定させてタイムカード登録させるようにしました。

![スクリーンショット 2016-01-31 15.58.06.png](https://qiita-image-store.s3.amazonaws.com/0/6808/9efe1d4e-8723-670f-64da-9b385d294ce7.png "スクリーンショット 2016-01-31 15.58.06.png")


以下ペリフェラルとかキャラクタリスティックとかBLEに関わる用語が普通にでてきます。この辺りがわからない場合は

[CoreBluetooth で出来る事](http://qiita.com/rnamiki/items/ea912393c4fb58e7a9b9)

この辺りをみて用語を確認すると良いと思います。


## 最初に注意点というか、はまったところ
今回ビーコンが手元になかったので、とりあえず最初にipod touchをビーコン代わりにするようにしました。

普通ペリフェラルを実装する場合下記のような流れになると思います。

1. サービスの作成
2. キャラクタリスティックの作成
3. サービスにキャラクタリスティックを追加
4. ペリフェラルにサービスを追加
5. アドバタイズするデータ生成
6. ペリフェラルのアドバタイズを開始

ペリフェラル側の実装をする時はサービスやキャラクタリスティックを追加する時に指定するUUIDが重要なのですが、Beaconとして機能させる場合（CLBeaconRegionを使う場合）は　 **「5. アドバタイズするデータ生成」** が結構重要です。

ここで指定したUUIDをセントラル側で指定して、Beaconの領域に入ったかどうかを判定してます。

LightBlueなどのアプリを使用して、ペリフェラルのUUIDを確認することができるのですが、その時に表示されているUUIDを指定してもうまく判定できませんでした・・・


## ペリフェラル側の実装

iOSの端末であればなんでも良いです。
ただしアドバタイズの処理は **バックグラウンドでは行われない** ので常にアクティブにする必要があり、実際に使用するには普通にビーコン端末買った方がいいですね。

CoreBluetoothを使うので最初に宣言します。
位置情報にも関わるのでCoreLocationも追加。

```
import CoreBluetooth
import CoreLocation
```


ペリフェラルにサービスを追加（CBPeripheralManagerのaddServiceメソッドを実行)した時に呼ばれるデリゲートメソッドでアドバタイズするデータを設定します。
UUIDは適当です。uuidgenコマンドなどで生成できます。

```

func peripheralManager(peripheral: CBPeripheralManager, didAddService service: CBService, error: NSError?) {
  if(error != nil) {
    print("サービスの追加失敗\(error)")
    return
  }
  
  //この値がアドバタイズされる
  let uuid: NSUUID! = NSUUID(UUIDString: "C708AA89-A0FE-47DB-A422-31621C6FC5FA")
  let beaconRegion = CLBeaconRegion(proximityUUID: uuid, identifier: "iBeacon")
  let advertisedata: NSDictionary = beaconRegion.peripheralDataWithMeasuredPower(nil)
  self.peripheralManager.startAdvertising(advertisedata as! [String : AnyObject])
  
}

```

## セントラル側の実装

メインとなるセントラル側の実装です。

### 位置情報の認証

まずビーコンの領域判定をするために位置情報を使うので、位置情報の取得許可をユーザーにしてもらう必要があります。

```
//位置情報の取得
let locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
```

位置情報を取得するために
・requestAlwaysAuthorization
・requestWhenInUseAuthorization
のメソッドを呼び出す必要があります。

さらにInfo.plistに
・NSLocationAlwaysUsageDescription
・NSLocationWhenInUseUsageDescription
を追加する必要があります。

今回は常に位置情報を取得する必要があるので　
**requestAlwaysAuthorizationt** と **NSLocationAlwaysUsageDescription** を指定します。


認証が完了すると
`locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus)`
が呼ばれます。

```
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {

  if status == CLAuthorizationStatus.AuthorizedAlways {
    //アドバタイズされているUUIDを指定
    let uuid: NSUUID! = NSUUID(UUIDString: "C708AA89-A0FE-47DB-A422-31621C6FC5FA")
    self.beaconRegion = CLBeaconRegion(proximityUUID: uuid, identifier: "iBeacon")
    
    //ビーコン領域の監視を開始する
    manager.startMonitoringForRegion(self.beaconRegion)
  }
}
```

CLLocationManagerの **startMonitoringForRegion** が呼ばれてビーコン領域の観測が開始すると`locationManager(manager: CLLocationManager, didStartMonitoringForRegion region: CLRegion)`が呼ばれます。

```
func locationManager(manager: CLLocationManager, didStartMonitoringForRegion region: CLRegion) {
  print("観測開始")
  self.locationManager.requestStateForRegion(self.beaconRegion)
}
```




### ビーコン領域判定

CLLocationManagerの **requestStateForRegion** でビーコン領域にいるかどうかのステータスを要求します。すると
`locationManager(manager: CLLocationManager, didDetermineState state: CLRegionState, forRegion region: CLRegion)`が呼ばれます。

```
    func locationManager(manager: CLLocationManager, didDetermineState state: CLRegionState, forRegion region: CLRegion) {
        
        switch(state) {
        case CLRegionState.Inside:
            print("inside")
            //既にリージョン内にいる場合にはdidEnterRegionが呼ばれないため、デリゲートメソッドを実行メソッドを実行
            if(self.beaconRegion != nil) {
                self.locationManager.startRangingBeaconsInRegion(self.beaconRegion)
            }
            
        case CLRegionState.Outside:
            print("outside")
            
        case CLRegionState.Unknown:
            print("unknown")
        }
    }
```


#### ビーコン領域に入った

ビーコン領域に入った時に呼ばれるデリゲートメソッド。
このメソッド内で、タイムカード登録処理を行います。

```
func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
}
```

#### ビーコン領域から出た

ビーコン領域に入った時に呼ばれるデリゲートメソッド。
ビーコン領域に入った時にはすぐに判定されるが、ビーコン領域から出た場合は判定されるまでに30秒ぐらいかかります。

```
func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
}
```


## おまけ
iBeaconはアプリが停止した場合どうなるかというと、それでもビーコン領域を判定してくれます。なので全くアプリを立ち上げなくてもインストールさえしてあれば問題ないはずです。