Apple Watch に Core Motion を使って色々なデータを取得して表示させてみる

## はじめに
行動認識とか，ライフログとか大学(+院)の学生時代に興味持っていて
Core Motion についても調べ一度ブログにまとめたことがありました。
Objective-C(今見るとツッコミどこが満載) だったので
勉強がてら Swift 3 版つくるかということで再度作り直しました。
ブログには iOS 側について，Qiita には試しに Apple Watch に
表示させた部分について書きたいと思います。

一部，[Shinagawa.swift #2](https://mokumoku-ios-at-next.connpass.com/event/48178/) で発表した内容になります。

## Core Motion とは

簡単にいえばデバイスのモーションデータを取得できる
Frameworkで，得られる色々なデータを適切に用いることで
より良いユーザ体験を提供できる。

これらのデータは目に見えないモノなので可視化して
ユーザに見える形になってようやく意味をなすという感じでしょうか。

<details><summary>**補足**</summary>iPhone 5s からモーションコプロセッサ(M7)が搭載され，
より省電力化されています。</details>


## Core Motion で取得可能な主なデータ

* CMAccelerometerData - 加速度関係
* CMAltitudeData - 相対高度。気圧関係
* CMGyroData - ジャイロ関係
* CMMagnetometerData - 磁気関係
* CMMotionActivity - 動作状態関係
* CMPedometerData - 歩数・距離関係
* CMPedometerEvent - 一時停止・再開関係

などなどでかなり多岐に渡ります。

それぞれ取得できる iOS/watchOS のバージョン，
端末があるので注意しながら実装していく必要があります。
(例として，気圧などは iPhone 6 以降の端末で取得可能)

## 今回の実装イメージ

今回は歩数を扱う **CMPedometerData** と
iOS 10から追加された **CMPedometerEvent** と
ユーザの動作情報が取得できる **CMMotionActivity** を
せっかくなので Apple Watch で取得し，
**今日歩いた歩数と歩数計イベント(停止・再開)**，
**動作状態(静止・歩行・走行・交通機関・自転車)とどのくらい信頼度あるか** を
画像やラベルで可視化してみます！
絵をもっと上手くやればもっとそれっぽくなると思います・・・

![gainenzu.png](https://qiita-image-store.s3.amazonaws.com/0/88266/133b2eba-b02e-0c44-3bb1-bfe08cbd09d1.png)


watch App の導入の仕方については説明を省きます。
これくらいの情報なら Apple Watch で閲覧できれば十分なのかなと思います。

<details><summary>**補足**</summary>
CMPedometerData は iOS 8 以上，watchOS 2以上です。
(iOS 7 では CMStepCounter というものでした)
CMPedometerEvent は iOS 10 以上，watchOS 3 以上です。
CMMotionActivity は iOS 7 以上，watchOS 2 以上です。</details>

## 今回のサンプルコード

iOS: Core Motion 全般(一部除く)		←ブログで記事書きます
watchOS: 歩数とイベントタイプ・アクティビティ　← 本記事

[GitHub: CoreMotioner](https://github.com/MilanistaDev/CoreMotioner)
ブログ: 後日

**動作環境等**
・Swift 3
・iOS 10 以上
・watchOS 3 以上
・Xcode 8.2 で動作確認

## CoreMotion 導入

簡単

### 1. CoreMotion.framework を追加

**TARGETS -> Build Phases -> Link Binary With Libraries**

![01_AddCoreMotionFramework.png](https://qiita-image-store.s3.amazonaws.com/0/88266/1d50eabe-3b64-61d8-32da-83ab40394f63.png "01_AddCoreMotionFramework.png")

### 2. Info.plist に使用目的等を記述(iOS 10 で必須に)

**TARGETS -> Info**

![02_CoreMotionUsageDescription.png](https://qiita-image-store.s3.amazonaws.com/0/88266/c13555aa-42e6-f660-9437-018392036511.png "02_CoreMotionUsageDescription.png")

あとは使いたい ファイルで ```import CoreMotion``` を追加すれば良いです。


## 実装 1 (今日歩いた歩数とイベントタイプ)

### 1. インスタンスを生成

```Swift:InterfaceController.swift
// インスタンスを生成
let pedometer: CMPedometer = CMPedometer()
```

### 2. 利用できるかを確認

Watch の場合，取得関数を呼ぶときは willActive 内に書く。
画面消えたら停止処理を書く didDeactivate が呼ばれるため。

```Swift:InterfaceController.swift
// 歩数取得可能 or not
if CMPedometer.isStepCountingAvailable() {
    self.acquirePedometerData()
}
// PedometerEvent取得可能 or not
if CMPedometer.isPedometerEventTrackingAvailable() {
    self.displayEventType()
}
```

<details><summary>**補足**</summary>
他のデータ取得に関してもだいたい用意されています。
一応書かなくても取得できない場合，
各取得データは nil になるので guard ではじけます。
それぞれ確認できないと呼べないという場合の書き方が
いまいちわからないです・・・(iOS 版で)
</details>


### 3-A. 今日歩いた歩数のデータを取得

歩数取得のメソッドは下記のようになります。
今日の 0 時を計算して，0 時を起点に
今日の歩数を取得してラベルに表示させています。

非同期なのでリアルタイムでの更新はされないです。
だいたい 4 ~ 5歩くらいで更新されます。

```swift:InterfaceController.swift

// 歩数表示のラベル
@IBOutlet var stepCountsLabel: WKInterfaceLabel!
    
let pedometer: CMPedometer = CMPedometer()

// CMPedometerData(歩数・距離)
var numberOfSteps: NSNumber = 0

...

func acquirePedometerData() {

    let now = Date()    // 現在時間
    let calendar = Calendar(identifier: .gregorian)
    let components = calendar.dateComponents([.year, .month, .day], from: now)
    let today = calendar.date(from: components) // 今日の0時

    self.pedometer.startUpdates(from: today!, withHandler: {
        (data: CMPedometerData?, error: Error?) in
        DispatchQueue.main.async(execute: { () in
            guard let exData = data, error == nil else {
                // 取得できなかった場合ここで抜ける
                return
            }
            // 歩数取得
            self.numberOfSteps = exData.numberOfSteps
            self.stepCountsLabel.setText(self.numberOfSteps.description)            
        })
    })
}
```    

<details><summary>**補足**</summary>
今回は，起点指定の ```startUpdates(from:withHandler:)``` を用いましたが，
起点と終点を指定できるものもあります。
```queryPedometerData(from:to:withHandler:)```
こちらは一度だけ呼ばれ一度だけ更新されます。
iOS 版では(ちょうど1週間分歩数・距離取得などで)実装しているので
よかったらご覧ください。
</details>


### 3-B. イベントタイプの取得(一時停止・再開)

EventType 取得メソッドは下記のようになります。
値は 2 つだけでそれぞれに該当する画像を表示させています。

```Swift:InterfaceController.swift
@IBOutlet var eventTypeImageView: WKInterfaceImage!

// CMPedometerEvent(歩行の一時停止・再開)
var pedometerEventType = ""

...

func displayEventType() {
    self.pedometer.startEventUpdates(handler: {
        (event: CMPedometerEvent?, error: Error?) in
        DispatchQueue.main.async(execute: { () in
            guard let exEvent = event, error == nil else {
                return
            }
            switch exEvent.type {
            case .pause:
                self.pedometerEventType = "PAUSE"
                self.eventTypeImageView.setImageNamed("pause")
            case .resume:
                self.pedometerEventType = "RESUME"
                self.eventTypeImageView.setImageNamed("resume")
        })
    })
}    
```

<details><summary>**補足**</summary>
[CMPedometerEvent](https://developer.apple.com/reference/coremotion/cmpedometerevent) は iOS 10 から追加された。
(No overview available. で今のところ説明がない)
[CMPedometerEventType](CMPedometerEventType) は ```.resume``` と ```.pause``` の2種類。
使ってみた感じタイミング的には結構正確なので
だるまさんが転んだゲームとかすぐ作れそう。
</details>

### 4. 取得終了メソッドを呼ぶ

スタートしたらストップもする。
画面消えたら止まっちゃいます。

```swist:InterfaceController.swift
override func didDeactivate() {
    // This method is called when watch view controller is no longer visible
    super.didDeactivate()
    // 計測終了
    self.pedometer.stopUpdates()
    self.pedometer.stopEventUpdates()
}
```

## 実行結果 (今日歩いた歩数とイベントタイプ)

* 静止時
 
![03_pause.PNG](https://qiita-image-store.s3.amazonaws.com/0/88266/e809f199-a833-03bb-1ac9-2c197941482c.png "03_pause.PNG")

* 動いたとき

![04_reseme.PNG](https://qiita-image-store.s3.amazonaws.com/0/88266/e1b12ed9-c0a0-e201-22e6-528a77bb77c0.png "04_reseme.PNG")


走っても，歩いても同じ挙動でした。
変更が多いのか画像がチラチラすることもありました。

---

## 実装 2 (動作状態を表示)

### 1. インスタンスを生成

```Swift:ActivityInterfaceController.swift
// インスタンスを生成
let activityManager = CMMotionActivityManager()
```

### 2. 利用できるかを確認

```Swift:ActivityInterfaceController.swift
// アクティビティ取得可能だったら取得する
if CMMotionActivityManager.isActivityAvailable() {
    self.displayActivityData()
}
```

### 3. アクティビティデータを取得・表示

アクティビティデータ取得のメソッドは下記になります。
信頼度(confidence)は ```.low, .medium, .high``` で
状態は5種類(stationary，walking，running, automotive, cycling)で
それぞれ Bool 値で取得可能です。

```Swift:ActivityInterfaceController.swift
func displayActivityData() {
    self.activityManager.startActivityUpdates(to: OperationQueue.current!,
        withHandler: {(data: CMMotionActivity?) in
        DispatchQueue.main.async(execute: { () in
            guard let exData = data else {
                return
            }
            // ユーザの状態(Bool)
            self.stationary = exData.stationary
            self.walking = exData.walking
            self.running = exData.running
            self.automotive = exData.automotive
            self.cycling = exData.cycling

            // confidence は CMMotionActivityConfidence 型
            // ユーザの状態の信頼度(精度)
            switch exData.confidence {
            case .low:
                self.confidence = "Low"
            case .medium:
                self.confidence = "Medium"
            case .high:
                self.confidence = "High"
            }
            self.confidenceLabel.setText(self.confidence)

            // 静止・歩行・走行が複数trueにはならないと思われ・・・
            if self.stationary {
                self.mainActivityImage.setImageNamed("stationary")
            } else if self.walking {
                 self.mainActivityImage.setImageNamed("walking")
            } else if self.running {
                 self.mainActivityImage.setImageNamed("running")
            }
                                                        
            // 交通機関で移動・自転車で移動も同時にtrueとはならないと思われ・・・
            if self.automotive {
                 self.subActivityImage.setImageNamed("automotive")
            } else if self.cycling {
                 self.subActivityImage.setImageNamed("cycling")
            }
        })
    })
}
```

それぞれの Bool 値は独立なので，この書き方は問題あるかもしれないです。
例えば，交通機関に乗っているときに静止も YES になることもあるはず。


### 4. 取得終了メソッドを呼ぶ

こちらもスタートさせたらストップさせる。

```swist:ActivityInterfaceController.swift
override func didDeactivate() {
    // This method is called when watch view controller is no longer visible
    super.didDeactivate()
    // 停止処理
    self.activityManager.stopActivityUpdates()
}
```

## 実行結果 (動作状態を表示)

* 走ったとき
![IMG_1342.PNG](https://qiita-image-store.s3.amazonaws.com/0/88266/1ebdd10d-e438-333b-e1d9-137a24f2ff1c.png)

* 歩行時
![IMG_1341.PNG](https://qiita-image-store.s3.amazonaws.com/0/88266/12e003a4-3757-a2f9-e8f6-8e50e60a053e.png)

* 静止時
![IMG_1340.PNG](https://qiita-image-store.s3.amazonaws.com/0/88266/d0030dfb-c05b-bec1-07ad-b8dbaa817384.png)

あとはまだ試してないです。
交通機関は iOS 版は YES(true) になることがありましたが，
Apple Watch だと静止だけの状態になっていました。
手首に Apple Watch を固定しているからかなーと。
例えば悪路をラリーカーなどで走れば変わってくるかもしれません。


## おわりに

Core Motion で取得できるデータは多い(取得して何に使うかが重要)。
iOS デバイスだけでなく Apple Watch でも取れる。
計算系は苦手かもしれないけど常に装着しているものだしデータ取得や
簡単なデータの閲覧には Apple Watch は向いていると思う。
ユーザとしてカラダはもちろんのこと，
iOS アプリエンジニアとしては，頭や手を動かして
Apple Watch もっと使ってみたいです。

乱文でしたが，ご覧いただきありがとうございました!!

## 参考

* [Core Motion API Reference](https://developer.apple.com/reference/coremotion)
* [CoreMotion で色々なデータを取得してみた(基礎)](http://milanista224.blogspot.jp/2015/02/coremotion-1.html)  
↑学生時代に書いた色々つっこみたくなる記事