React Native+Expoでジオフェンシングを使ったリマインドアプリを作って忘れっぽい自分を救う！

## 背景
突然ですが私は忘れっぽいです。
忘れっぽいので3歩も歩けばやるべきことを忘却します。

どれだけ忘れっぽいかというと、

- 出勤・退勤時間を勤怠アプリ記録するのを忘れ、後日まとめて入れるも正しい実績が思い出せない
- トイレットペーパーを買いにスーパーに行ったが、なぜかお菓子のみを片手にスキップで帰宅する
- コンビニ弁当を温めてもらっているのを精算中に忘却、何も持たずに晴れやかな顔でコンビニを後にする
- 揚げ物を作った油をお茶のペットボトルに入れて退避していたら、後日お茶と間違えて一気に飲み干す

こんなこと日常茶飯事です。

ポンコツと言わないでください。
シンプルに傷つきます。

## この記事の目的
さて、今回はそんなポンコツのために「ジオフェンシング」という仕組みを使って自分がある特定の地点へ近づいたらローカル通知でやるべきことを表示してくれるアプリを自作してみます。ジオフェンシングを利用すればある地点に近づいた or 離れたことをトリガーにアプリにアクションさせることができるので、そいつを使ってみようという記事になります。

まず、記事の前半で今回目的のアプリを作るにあたって最低限知っておくべき技術の概要について説明します。後半で、ステップバイステップでコードを紹介しながら、実際に動くアプリを作ってみます。

※ 実を言うとiPhoneには標準でリマインダというアプリがあって、この記事の成果物と全く同じことが実現できます。が、そんなことは忘れて自分で作ることにします

## 対象読者
- React Nativeはある程度わかる人
- React Nativeを使って何でもいいから動くアプリを作ってみたい人
- 位置情報・ローカル通知を利用した機能に興味がある人
- 忘れっぽい人

## 用語の整理
### React Native
[React Native](https://facebook.github.io/react-native/)はJaveScriptを使ってネイティブアプリを開発できるフレームワークです。

記法はReact.jsと同じなので、React Native用のコンポーネントの使い方をちょこっと勉強すれば、React.jsを経験しているWeb開発者ならすぐにでもモバイルアプリを開発できるようになります。

開発したアプリはiOS/Androidの両プラットフォーム用にビルドすることができます。WebViewベースで動作するクロスプラットフォームなフレームワークでは動作がもっさりすることがデメリットとして挙げられがちですが、React Nativeの場合JavaScriptからネイティブのUIをコントロールしているのでWebViewベースのものと比べてハイパフォーマンスなのだそう。

なんて便利なフレームワークなんだ...。

### Expo
[Expo](https://expo.io/)はReact Nativeによるネイティブアプリケーションの開発を手助けしてくれるオープンソースのプラットフォームです。

Expoが提供する開発ワークフローに則ることで、開発者は複雑なビルド時の設定を意識せず、**JavaScriptのみでネイティブアプリ開発を完結できる**というWeb開発者としては嬉しいプラットフォームです。

しかも、PCにエミュレータを入れたり、Xcode/Android Studioといった開発環境を用意せずとも、Expo Clientをストア（[iOS](https://apps.apple.com/jp/app/expo-client/id982107779)/[Android](https://play.google.com/store/apps/details?id=host.exp.exponent&hl=ja)）から実機にインストールしておけば簡単に動作確認ができてしまいます。

なんて便利なプラットフォームなんだ...。

<br/>

注意点として、Expoのワークフローに沿って開発する場合、JavaScript以外（ネイティブの言語）で書かれたライブラリは利用できません。そのようなライブラリを取り入れる必要がある程度に複雑なアプリを構築する際はExpoを採用するかどうか検討の必要はありそうです。

とはいえ、どうしてもネイティブのライブラリが使いたくなったらコマンド一発でExpoのワークフローから抜け出すこともできます。（それ以後は自分でプロジェクトのビルドまわりをコントロールことになります）

始めはExpoを利用して開発していき、要件によって途中でワークフローを切り替える、というのも選択肢の一つだと思います。

### ローカル通知
ローカル通知とは外部のサーバを介さずデバイスの内部で関係するプッシュ通知です（外部のサーバを経由する場合はリモート通知と呼ばれます）。

外部のサーバを介さないため、リモート通知と比べて以下のようなメリットがあります。

- オフラインでも通知を送信できる
- デバイストークンの管理が不要

リモート通知の場合、通知を送る側のサーバがどの端末に通知するか識別する必要があるのでデバイス識別のためのトークンを管理する必要がありますが、ローカル通知の場合は自端末内で完結するのでトークンの管理は不要です。

なんて便利な機能なんだ...。

### ジオフェンシング
ジオフェンシングとは特定のエリアに仮想的な柵を作る仕組みです。ユーザがその特定の場所に入ったことをトリガーにしてシステムが何らかのアクションを起こすといった機能を実現できます。

軽く調べてみると、例えば以下のようなシーンで使われているようです。

- 店舗の近くを通った顧客に販促情報を送る
- 店舗に来店した顧客に自動的にスタンプが送られるスタンプラリーキャンペーン
- 観光スポットの近くに来たユーザにそのスポットの説明を表示する
- 自転車の盗難防止（フェンスの外に出たら通知）

なんて便利な柵なんだ...。

## 位置情報に基づいたリマインドアプリを作る
実際に位置情報を使ったリマインドアプリを作っていきます。

### 完成イメージ
リマインドアプリとして以下の機能を実装します。

- 緯度・経度と紐づけてタスクを登録できる
- 登録された緯度・経度に近づく（もしくは離れる）と登録したタスクをローカル通知でリマインドする
- ローカル通知からアプリを起動するとポップアップでタスクの内容を表示する

### 利用したライブラリのバージョン
今回利用した環境・ライブラリは以下のバージョンです。記事の通りにやってもうまくいかない場合はバージョンを確認してみてください。

| ライブラリ | バージョン |
| --- | --- |
| Node.js | v12.3.1 |
| expo-cli | v3.9.0 |
| expo-permissions | v7.0.0 |
| expo-location | v7.0.0 |
| expo-task-manager | v7.0.0 |

### STEP1：プロジェクトを作る
Expo CLIを使ってプロジェクトを作成します。

```
expo init <プロジェクト名>
```

今回は「blank（TypeScript）」のテンプレートを選びました。

`expo init`でプロジェクトを初期化すると`App.tsx`（メインのコンポーネント）は関数コンポーネントになっていますが、今回はクラスコンポーネントとして画面を作っていこうと思います。

`App.tsx`をクラスコンポーネントに直したものが以下です。この後の話は以下のコードに変更を加えていく形で進めます。

```react:App.tsx
import React from 'react'
import { 
  StyleSheet, 
  Text, 
  View, 
  Platform 
} from 'react-native'

export default class App extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <Text>Hello, React Native!</Text>
      </View>
    )  
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
})
```



### STEP2：権限を取得しよう
モバイルアプリの一部の機能では、ユーザの許可のもと機能にアクセスする権限を得ておく必要があります。普段何気なくアプリを使っているとポップアップで「このアプリに位置情報を許可しますか？」などと表示されるアレだと思っていただけると想像しやすいと思います。

今回のアプリを作るに当たって必要な権限は以下の2つです。

- 通知を送る権限
- ユーザの位置情報を取得する権限

#### 権限を取得するAPI
Expoの[Permissions](https://docs.expo.io/versions/latest/sdk/permissions/)で権限を取得するAPIが用意されているので、今回はそちらを使ってアプリ起動時に権限を求めるようにしてみます。

実装の前に、権限取得用のAPIを使えるようにするため`expo-permissions`をインストールしておきます。

```sh
expo install expo-permissions
```
権限の確認・取得には以下の2つのAPIを使います。

```typescript
// APIを使うためにインポートが必要
import * as Permissions from 'expo-permissions'

// 権限の有無を確認する（permissionTypeは複数指定可能）
Permissions.getAsync(...types)

// 権限を取得する許可をユーザに求める（permissionTypeは複数指定可能）
Permissions.askAsync(...types)
```
`types`には取得したい権限の種類を入れます。指定できる値は`Permissions.${権限取得対象}`としてアクセスできるよう定数が定義されているのでそちらを使います。例えば、`Permissions.NOTIFICATION`なら通知の権限、`Permissions.LOCATION`なら位置情報となります。

戻り値の型はこの記事では必要最低限の要素のみ解説します。詳しくは「[公式ドキュメント - Permissions.getAsync](https://docs.expo.io/versions/latest/sdk/permissions/#returns)」を参照してください。

#### 実装コードと実行結果
全体のコードを示します。

```react:App.tsx
import React from 'react'
import { StyleSheet, Text, View, Platform } from 'react-native'
import * as Permissions from 'expo-permissions'

export default class App extends React.Component {
  state = {
    isNotificationPermitted: false,
    isLocationPermitted: false,
  }

  async componentDidMount () {
    this.setState({
      isNotificationPermitted: await this._confirmNotificationPermission(),
      isLocationPermitted: await this._confirmLocationPermission()
    })
  }

  render () {
    return (
      <View style={styles.container}>
        <View>
          <Text>Notification Permission: { this.state.isNotificationPermitted ? '○' : '×' }</Text>
          <Text>Location Permission: { this.state.isLocationPermitted ? '○' : '×' }</Text>
        </View>
      </View>
    )
  }

  async _confirmNotificationPermission () {
    const permission = await Permissions.getAsync(Permissions.NOTIFICATIONS)
    if (permission.status === 'granted') return true
    const askResult = await Permissions.askAsync(Permissions.NOTIFICATIONS)
    return askResult.status === 'granted'
  }

  async _confirmLocationPermission () {
    const permissionIsValid = (permission: Permissions.PermissionResponse) => {
      if (permission.status !== 'granted') return false
      if (Platform.OS !== 'ios') return true
      return permission.permissions.location.ios.scope === 'always'
    }
    const permission = await Permissions.getAsync(Permissions.LOCATION)
    if (permissionIsValid(permission)) return true
    const askResult = await Permissions.askAsync(Permissions.LOCATION)
    return permissionIsValid(askResult)
  }
}

const styles = StyleSheet.create({
  permissions: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
})
```

上記のコードを実行すると、以下のように起動時に位置情報・通知の許可を求めるポップアップが現れます。

<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216033/fc8b1a12-9f4e-2d45-85f7-aeff8c8b80b0.png">

権限の取得状態が両方「○」になれば権限の取得完了です！
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216033/52be6353-9737-b33e-d133-7a12ddf3ffbb.png">

#### 解説：通知権限の取得
それではコードの解説です。

まずは通知の許可を確認・取得している以下の関数から。この関数は戻り値として、権限が取得できたら`true`、できなければ`false`を返します。

```typescript
  async _confirmNotificationPermission () {
    const permission = await Permissions.getAsync(Permissions.NOTIFICATIONS)
    if (permission.status === 'granted') return true
    const askResult = await Permissions.askAsync(Permissions.NOTIFICATIONS)
    return askResult.status === 'granted'
  }
```

はじめの行で`Permissions.getAsync`で現在の権限に関する情報を取得しています。戻り値としてオブジェクトが返ってくるのですが、その中の`status`で権限が許可されているかどうか判断できます。`status`は以下の3通りです。

| status | 意味 |
| --- | --- |
| undetermined | まだユーザに許可を求めたことがないため未決定 |
| granted | ユーザが権限を許可した |
| denied | ユーザが権限を拒否した |

現在の権限を確認した結果、許可されていなかった場合は`Permissions.askAsync`でユーザに許可を求めます。`getAsync`と`askAsync`の戻り値は同じ形式なので、こちらも`status`を元に結果を判断しています。

な、なんて実装が簡単なんだ...。

#### 解説：位置情報取得権限の取得
通知の時と流れはほとんど一緒ですが、一部特殊なことをしています。

```typescript
import { StyleSheet, Text, View, Platform } from 'react-native' // Platformをインポートしておく

// ...（中略）...

  async _confirmLocationPermission () {
    const permissionIsValid = (permission: Permissions.PermissionResponse) => {
      if (permission.status !== 'granted') return false
      if (Platform.OS !== 'ios') return true
      return permission.permissions.location.ios.scope === 'always'
    }
    const permission = await Permissions.getAsync(Permissions.LOCATION)
    if (permissionIsValid(permission)) return true
    const askResult = await Permissions.askAsync(Permissions.LOCATION)
    return permissionIsValid(askResult)
  }
```

通知の時と違うのは「権限が取得できたかどうか」の判断部分です。通知では単に`status`の値だけを見ておけばよかったものの、位置情報では何やら`permissionIsValid`というローカル関数を使って判断しています。

実はiOSでの位置情報の取得権限は、同じ権限でも以下のような種別に分かれているのです。そして、今回使いたいジオフェンシング機能を利用するには`always`の権限を取得しておく必要があります。常にデバイスの位置をトラッキングして、柵の中に入ったかどうかを判断するためです。

| 種別 | 意味 |
| --- | --- |
| none | 取得の許可なし |
| whenInUse | アプリを使っている時のみ取得可 |
| always | 常に許可 |

iOSのみに存在するこの種別は`戻り値オブジェクト.permissions.location.ios.scope`で確認できます。種別について、詳しくは[公式ドキュメント - Permissions.LOCATION](https://docs.expo.io/versions/latest/sdk/permissions/#permissionslocation)をご覧ください。

#### 解説：残りの部分
残りは、今まで解説してきた2つの関数（`_confirmNotificationPermission`と`_confirmLocationPermission`）を使って、`componentDidMount`でコンポーネント初期化後に権限の確認・取得をして画面に結果を`○` or `×`で表示しています。

#### 注意：ポップアップが出るのは1度だけ
アプリが起動するために毎回毎回権限を求められるのはUXが悪いですよね。そのため、2回目の実行以降の`askAsync`呼び出しでは権限取得のポップアップは表示されません。1回目にユーザが選択した結果がそのまま返ってきます。

テストなどのためにもう1回ポップアップを出したい場合、Expo Clientを再インストールするか、設定から権限の設定を初期化する必要があります。iOSなら設定から「一般 > リセット > 位置情報とプライバシーをリセット」、Androidなら「アプリと通知 > Expo > 権限 > リセットしたい権限を選択 > 許可しないに変更」とすると再度ポップアップが出るようです。

#### 注意：シミュレータ・Expo Client・実機による動作の違い
シミュレータ・Expo Clientでは取得できない権限があります。時間がなく全てをまとめきることはできませんがポップアップが出ない場合は実機でないと取得できない権限の可能性があることを頭の片隅に入れておくとハマりにくいと思います。


### STEP3：ローカル通知を送ってみよう
権限が取得できたところで、早速ローカル通知を送ってみましょう。

#### ローカル通知のAPI
こちらもExpoの[Notifications](https://docs.expo.io/versions/v35.0.0/sdk/notifications/)で用意してくれているAPIを使います。

使うのは以下の2つです。

```typescript
// APIを使うためにインポートが必要
import { Notifications } from 'expo'

// ローカル通知を送信
presentLocalNotificationAsync(notification)

// 通知を受け取ったときのコールバックを登録
Notifications.addListener(listener)
```

それぞれのAPIの概要はコメントで書いた通りです。
詳細はコードと共に説明します。

#### 実装コードと実行結果
実装の全体像はこんなかんじ（差分のある箇所以外は省略します）。

```diff_tsx:App.tsx
import React from 'react'
import { 
  StyleSheet, 
  Text,   
  View,
+  Button,
  Platform
} from 'react-native'
import * as Permissions from 'expo-permissions'
+import { Notifications } from "expo"
+import { Notification } from 'expo/build/Notifications/Notifications.types'

export default class App extends React.Component {
  // ...（中略）...
  
  render () {
    return (
      <View style={styles.container}>
        <View>
          <Text>Notification Permission: { this.state.isNotificationPermitted ? '○' : '×' }</Text>
          <Text>Location Permission: { this.state.isLocationPermitted ? '○' : '×' }</Text>
        </View>
        <View>
+          <Button title="Send local notification" onPress={ this._sendLocalNotification } />
        </View>
      </View>
    )
  }

  async componentDidMount () {
    this.setState({
      isNotificationPermitted: await this._confirmNotificationPermission(),
      isLocationPermitted: await this._confirmLocationPermission()
    })
+    Notifications.addListener(this._onReceiveNotification)
  }

  // ...（中略）...
+  async _sendLocalNotification () {
+    await Notifications.presentLocalNotificationAsync({
+      title: 'テストローカル通知',
+      body: 'これはテスト用のローカル通知です',
+      data: {
+        message: 'テストローカル通知を受け取りました'
+      },
+      ios: {
+        _displayInForeground: true
+      }
+    })
+  }
+
+  _onReceiveNotification (notification: Notification) {
+    alert(notification.data.message)
+  }
}

// ...（スタイルは省略）...
```

実行すると画面上に通知を送るためのボタンが出ます。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216033/ea7044d3-31bb-5010-3197-048688cbae82.png">

ボタンを押して通知センターを見てみると通知が届いていることが確認できます。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216033/ea36eef2-de10-0b35-0e44-0d9681849399.png">

通知をクリックすると以下のようにポップアップでメッセージが表示されます。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216033/43cbc287-3198-a4be-c56a-a7d834baa162.png">

#### 解説：通知の送信
通知を送信しているのは以下のメソッドで、ボタンが押された時に実行されるようにしています。

```typescript
  async _sendLocalNotification () {
    await Notifications.presentLocalNotificationAsync({
      title: 'テストローカル通知',
      body: 'これはテスト用のローカル通知です',
      data: {
        message: 'テストローカル通知を受け取りました'
      },
      ios: {
        _displayInForeground: false

      }
    })
  }
```

実装はシンプルで`presentLocalNotificationAsync`メソッドに通知の情報を詰め込んだオブジェクトを渡すだけです。`title`と`body`は表示される通知文言の設定、`data`には通知を受け取ったときにアプリが受け取るデータを指定します。

アプリを開いている時に画面上に通知を出したい場合は、iOSの場合のみ`ios._displayInForeground`に`true`を設定することで実現できます。（アプリを閉じている場合は設定しなくとも以下の画像のような通知が現れます）

<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216033/e630da80-7ea5-8c25-b1ba-0fdfe047e2ae.png">

その他、通知のオブジェクトの詳細な仕様は「[公式ドキュメント - Local Notification](https://docs.expo.io/versions/latest/sdk/notifications/#localnotification)」を参照してください。

#### 解説：通知を受け取ったとき
以下の部分で通知を受け取ったときのコールバック関数を登録しています。今回は受け取った値をポップアップで表示するだけの関数をコールバックにしました。

```typescript
  async componentDidMount () {
     // ...（省略）...
     Notifications.addListener(this._onReceiveNotification)
  }

  _onReceiveNotification (notification: Notification) {
    alert(notification.data.message)
  }
```

このコールバックでは、以下のようなオブジェクトを受け取れます。

```json
{
  origin: "通知をどのように受け取ったかを表す文字列",
  data: "presentLocalNotificationAsyncで指定したdata",
  remote: "リモート通知ならtrue、ローカル通知ならfalse"
}
```

`origin`は`select`または`received`のどちらかが入ります。具体的には以下のようになります。

| origin | 意味 |
| --- | --- |
| select | アプリがバックグラウンドになっているときに通知からアプリを起動した |
| received | アプリを開いている状態で通知を受け取った |

今回はローカル通知をテストしたいだけなので、コールバックは`presentLocalNotificationAsync`で`data`に詰め込んだメッセージをそのままポップアップ表示するだけの処理にしています。

※ 実際に動かしてみるとiOSで通知オブジェクトの`ios._displayInForeground`を`true`している場合、コールバックが呼ばれるのは画面上に現れた通知をクリックした時のみのようです



### STEP4：ジオフェンシングをトリガーにしよう
最後にジオフェンシングをトリガーにしてローカル通知を送信してみます。

#### ジオフェンシングAPI
こちらもExpoの[Location API](https://docs.expo.io/versions/v35.0.0/sdk/location/#locationgeofencingregionstate)で実現できます

実装の前に、位置情報取得用のAPIを使えるようにするための`expo-permissions`をインストールしておきます。

```sh
expo install expo-location
```

以下のAPIを使うとジオフェンシングを開始できます。

```typescript
import * as Location from 'expo-location'

Location.startGeofencingAsync(taskName, regions)
```

regionsには柵をどこに作るかを指定します。例えば弊社の所在地と最寄駅である勝どき駅を中心にそれぞれ半径300mの策を作る場合のregionsの例が以下です。ちなみにiOSでは`regions`を20個までしか登録できない制限があるようですのでご注意を。

```typescript
[
        {
          identifier: '晴海トリトンスクエア', // 柵のID
          latitude: 35.657413, // 緯度
          longitude: 139.782514, // 経度
          radius: 300 // 柵の半径
        },
        {
          identifier: '勝どき駅',
          latitude: 35.658979,
          longitude: 139.777149,
          radius: 300
        }
]
```

柵に出入りした際に`taskName`に指定した名前のタスクが発生します。発生したタスクをもとに処理を行うのが、次に紹介するタスクマネージャーです。

#### タスクマネージャー
タスクマネージャーを利用するために以下のパッケージをインストールします。

```sh
expo install expo-task-manager
```

以下のAPIで発生したタスクを処理します。

```typescript
TaskManager.defineTask(taskName, task)
```

`taskName`にはジオフェンシングを開始する時に指定したものと同じ名前を指定します。

`task`はジオフェンシング開始時に渡した柵のリスト（`regions`）のうち、実際に出入りが発生した柵1つを引数に取る関数を指定します。例えば以下のようなタスクを定義すると、出入りが発生した柵の情報をコンソールに出力します。

```typescript
TaskManager.defineTask(taskName, (input: any) =>   
  console.log(region)
})
```

#### 実装コードと実行結果
実装コードは以下のとおり。差分で示します。

```diff_tsx
// ...（省略）...
import * as Permissions from 'expo-permissions'
import { Notifications } from "expo"
import { Notification } from 'expo/build/Notifications/Notifications.types'
+import * as Location from 'expo-location'
+import * as TaskManager from 'expo-task-manager'
+
+const TASK_NAME = 'GEOFENCE_TASK'
+
+TaskManager.defineTask(TASK_NAME, ({ data, error }: any) => {
+  if (error) throw new Error()
+
+  const message = data.eventType === +Location.GeofencingEventType.Enter ?
+    '出勤時間を入力してください' :
+    '退勤時間を入力してください'
+  Notifications.presentLocalNotificationAsync({
+    title: `現在${data.region.identifier}付近`,
+    body: message,
+    data: {
+      message: message
+    },
+    ios: {
+      _displayInForeground: false
+    }
+  })
+})

export default class App extends React.Component {
  // ...（省略）...

  async componentDidMount () {
    this.setState({
      isNotificationPermitted: await this._confirmNotificationPermission(),
      isLocationPermitted: await this._confirmLocationPermission()
    })

    Notifications.addListener(this._onReceiveNotification)
+    
+    await Location.startGeofencingAsync(TASK_NAME, [
+      {
+        identifier: '晴海トリトンスクエア',
+        latitude: 35.657413,
+        longitude: 139.782514,
+        radius: 300
+      }
+    ])
  }

  async _confirmNotificationPermission () {
    const permission = await Permissions.getAsync(Permissions.NOTIFICATIONS)
    if (permission.status === 'granted') return true
    const askResult = await Permissions.askAsync(Permissions.NOTIFICATIONS)
    return askResult.status === 'granted'
  }

  // ...（省略）...

  _onReceiveNotification (notification: Notification) {
    alert(notification.data.message)
  }
}
```

これで弊社の所在地、晴海トリトンスクエアに近づくと以下のような通知が出ます。左がアプリを閉じている状態で柵に入った場合、右はアプリを開いた状態で柵に入った場合です。なお、左の画像の通知をタップするとアプリが自動的に起動し、右の画像と同じ状態となります。

<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216033/1aa46f0a-ca25-fa02-773b-885c8925dca0.png">　<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216033/360d5b91-d6d0-1901-b2fa-6bc2e235876c.png">

晴海トリトンスクエアから離れると以下のような通知が出ます。文言以外は近づくときと同じです。

<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216033/a8939ed7-8811-92e3-5573-4ca20f7d2ace.png">　<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216033/91041cdc-2a08-8724-fe2a-fe12ff12839e.png">

#### 解説：ジオフェンシング開始
ジオフェンシングを開始しているのは以下の部分です。

```typescript
  async componentDidMount () {
    // ...（省略）...
    
    await Location.startGeofencingAsync(TASK_NAME, [
      {
        identifier: '晴海トリトンスクエア',
        latitude: 35.657413,
        longitude: 139.782514,
        radius: 300
      }
    ])
  }
```

`componentDidMount`の中なので初期化時に実行されます。第2引数に指定する`region`の配列は前述の通りなのであまり解説することがありません。

#### 解説：柵に出入りした時の処理
以下の部分です。

```typescript
TaskManager.defineTask(TASK_NAME, ({ data, error }: any) => {
  if (error) throw new Error()

  const message = data.eventType === 
  Location.GeofencingEventType.Enter ?
    '出勤時間を入力してください' :
    '退勤時間を入力してください'
  Notifications.presentLocalNotificationAsync({
    title: `現在${data.region.identifier}付近`,
    body: message,
    data: {
      message: message
    },
    ios: {
      _displayInForeground: false
    }
  })
})
```

柵に出入りした際のコールバックの引数の形式は以下の形式です。詳細は[Task parameters](https://docs.expo.io/versions/v35.0.0/sdk/location/#task-parameters-1)を参照してください。

```json
{
  "data": {
    "eventType": "柵を出たか/柵に入ったか",
    "region": "出入りした柵の情報"
  },
  "error": "エラー発生時にエラー内容が入る"
}
```

`eventType`には以下のような定数が定義されています。

| eventType | 意味 |
| --- | --- |
| Location.GeofencingEventType.Enter | 柵に入った |
| Location.GeofencingEventType.Exit  | 柵から出た |

#### 注意：defineTaskはグローバルスコープに定義する
注意として`TaskManager.defineTask`による柵出入り時のコールバックの登録は、グローバルスコープで行う必要があります。そのため、コンポーネントの処理に含めることができません（筆者はこれに気づかずかなりハマりました）

詳しくは[TaskManager.defineTask](https://docs.expo.io/versions/v35.0.0/sdk/task-manager/#taskmanagerdefinetasktaskname-task)を参照してください。

#### 注意：スタンドアロンアプリでジオフェンシングする場合
[Configuration for standalone apps](https://docs.expo.io/versions/latest/sdk/task-manager/#configuration-for-standalone-apps)を参考に`app.json`に追加設定が必要です。

## まとめ
この記事ではジオフェンシングを使って位置情報に基づいてローカル通知を送信する方法を紹介しました。

執筆前はReact Nativeをちょっと触ったことがあるを程度の知識だったので、「位置情報をバックグラウンドで取得して通知送信」とネイティブ機能満載の今回のアイデアの実現は大変そうと思っていたものの、蓋を開けてみれば通知や位置情報などネイティブならではの機能の実装はほぼExpoのAPIだけで実現できました。

今回使っていないネイティブの機能へアクセスするAPIは豊富に揃っていそうだったので、React Native+Expoは簡単な機能を持つアプリやプロトタイプ目的ならiOS/Android両プラットフォームを（ほぼ）ワンソースで開発できてスピード感を持ってモノを作っていけそうだなと感じました。

作ったアプリについて、本来はタスクを画面から登録し、その内容に応じてリマインダすることを考えていたのですが、今回は記事が長くなってしまったこととアドベントカレンダーの期日が迫っていることから断念しました。タスクを固定で登録する作りになっているのがちょっとカッコ悪いですがご愛嬌ということで。

そのうち時間ができたらUIを整えて、よく通っているコンビニを出るときに「温めてもらった弁当忘れてない？」ってリマインドする用途で使います（本当によく忘れるので）。

## 参考URL
[React Native公式ドキュメント](https://facebook.github.io/react-native/)
[Learning React Native by Bonnie Eisenman](https://www.oreilly.com/library/view/learning-react-native/9781491929049/ch01.html)
[Expo Permissions API Reference](https://docs.expo.io/versions/latest/sdk/permissions/)
[Expo Location API Reference](https://docs.expo.io/versions/latest/sdk/location/)
[Expo Notifications API Reference](https://docs.expo.io/versions/v35.0.0/sdk/notifications/)
[Expo TaskManager API Reference](https://docs.expo.io/versions/latest/sdk/task-manager/#taskmanagerdefinetasktaskname-task)