Webエンジニアが「バックグラウンドで位置情報をFirebaseに保存するアプリ」を最速でベータテストする方法

**2019/01/25 追記**: [Expo v32.0.0 でバックグラウンド位置情報使えるようになった](https://blog.expo.io/expo-sdk-v32-0-0-is-now-available-6b78f92a6c52 )っぽいですね。暇があればExpoでの検証記事も書こうかなと思います。

とある事情でバックグラウンドで位置情報を保存する機能が必要なアプリのお手伝いをするようになったので、[React Native](https://facebook.github.io/react-native/) でその機能を実装した過程を書こうかと思います。書いてる途中でアホほど長くなってしまってもはや最速ではないなと思いました。

# スムーズに進めたい方

ベータテスト配布のために [Apple Developer Program](https://developer.apple.com/jp/programs/) に登録（11,800円/年）する必要があります。

登録からアクティベーションに約1日かかるので、待つのが面倒な方は先に登録しておくと良いかと思います。


# 対象

- iOS アプリ作りたい人
- Web エンジニア
- 趣味以上
- App Store 配布未満

今回は、ベータテスト配布やそれなりに将来を見据えた CI による自動化も含めた方法になるので、どうしてもお金がかかります。なので、「ちょっと勉強したいなー」くらいのモチベーションの人には不向きかもしれません。

あとは、単純に今回手伝う範囲に App Store へのリリースが含まれていないので、経験していないものを想像で書くのも良くないと思いベータ版配布までの方法にしました。ただ、リリースもすぐにできる環境は整えているような気がします。

Web エンジニアが Web エンジニア向けに書いているので、間違っているところやアプリ開発経験がある方には少し丁寧すぎるところがあるかもしれません。

# 実装前の調査

そもそも僕は Web エンジニアで「こういうアプリ作りたいです」と言われて、浅知恵で「[PWA](https://developers.google.com/web/fundamentals/codelabs/your-first-pwapp/?hl=ja) とかでできるかなあ」というところから始まって、結局 React Native で [react-native-background-geolocation](https://github.com/transistorsoft/react-native-background-geolocation) というプラグインを使うに至ったので、その経緯を一応書いておきます。

## PWAでは無理 

PWA の説明で「バックグラウンドで〜」「位置情報が〜」みたいなリストや表が出てくるせいで何でもできる気がしてくるんですが、思ったよりそんなことないみたいです。

### PWAはバックグラウンドでずっと動くわけではない

PWA で言われるバックグラウンド云々は[サービスワーカー](https://developers.google.com/web/fundamentals/primers/service-workers/?hl=ja)の機能を指していますが、サービスワーカーは[基本何かを契機に動くもの](https://stackoverflow.com/a/35840961/8054959)で、その処理はブラウザを閉じた状態では数分程度で止まってしまいます。

試しにサービスワーカーの [waitUntil()](https://stackoverflow.com/a/37906330/8054959) に何時間単位のクソ長い `setTimeout` の Promise を渡して、ブラウザを閉じてどのくらい動き続けるか試したところちょうど5分くらいで止まりました（10秒おきにFirebaseに保存する処理を実行していました）。

[![Image from Gyazo](https://i.gyazo.com/9c6d8deb09da7689cb559f83cf8ee923.png)](https://gyazo.com/9c6d8deb09da7689cb559f83cf8ee923)

### そもそもサービスワーカー内で位置情報取得できない

PWA！バックグラウンド！位置情報！という並び的に、バックグラウンドでも位置情報アクセスできそうな気がしますが、今の所そんなことはないです。

- https://github.com/w3c/ServiceWorker/issues/745
- https://github.com/w3c/geolocation-sensor/pull/30 

とかを見る感じ、いつか使えるようになるといいね、くらいにしか思えない雰囲気です。

## では React Native でやる、ただ Expo では無理

そうなるともうアプリを作るしかなくなってしまいますが、 アプリ開発経験がない僕が短時間で学べそうなのは React Native だったのでとりあえず React Native にしました。

React Native 開発には [Expo](https://expo.io/) という超便利ツール（QRコード読み込んでアプリをテストできたりする）の利用が推奨されているのですが、バックグラウンド処理にはまだ対応していませんでした。

~~ただ、[今年の8月にバックグラウンドでの位置情報トラッキング](https://expo.canny.io/feature-requests/p/background-location-tracking)、つい[先月にバックグラウンド処理](https://expo.canny.io/feature-requests/p/background-tasks-support) の Feature Request が `IN PROGRESS` になったので、どれくらい先かは不明ですが Expo で作れるようになる未来はあるっぽいです。~~

**2019/01/25 追記**: 使えるようになりました。

- https://blog.expo.io/expo-sdk-v32-0-0-is-now-available-6b78f92a6c52

---

こんな感じの過程を経て、Expo なしの React Native で開発をすることが決まりました。


# 機能

- Google アカウントでログインできる
- 位置情報を移動をしている間だけログインしたアカウントの ID を key に保存する（Firebase）
- バックグラウンドでも動く

# 環境・利用するもの

- Xcode: Version 10.1 (10B61)
- Node.js: 10.14.0 (11.0.* では出来ませんでした)
- npm: 6.4.1
- React Native: 0.57.7
- [React Native Firebase](https://rnfirebase.io/): ^5.1.1
- [react-native-background-geolocation](https://github.com/transistorsoft/react-native-background-geolocation): ^2.14.2
- [Cloud Functions for Firebase](https://firebase.google.com/docs/functions): ^2.1.0 (Node v8 の方で利用)
- [Bitrise](https://www.bitrise.io/): CI サービス
- [HockeyApp](https://hockeyapp.net/): アプリ配布サービス

# React Native プロジェクトの開始

[公式のGetting Started](https://facebook.github.io/react-native/docs/getting-started) の通りに進めます。 

ただ最後の `react-native run-ios` はビルドが走ってしまい、あとでビルドターゲットを変える段階でビルドエラーが出たりした気がしなくもないので、やらないほうがいいかなあと思います。

やってて思いましたが React Native 開発で総じて言えることは**「余計なことはしない」**です。何かの拍子で歯車が噛み合わなくなるとビルドエラーから抜け出せなくなります。。こまめに git にコミットしてエラーが出たらすぐに戻れるようにしておいておくと良いです。

Expo は利用しないので `Quick Start` ではなく、 `Building Projects with Native Code` の方で始めます。
![](https://i.gyazo.com/c68e0367f3995f25e224d5ebcdd2e81a.png)

最終的に `App.js` やら `ios/` やら `android/` やらが入ったプロジェクトができると思います。

`ios/プロジェクト名.xcodeproj` が Xcode で開くファイルになります（あとで `ios/プロジェクト名.xcworkspace` を開くように切り替えますが）

# iOS アプリ開発の基礎知識

実機で動作確認するステップに入りたいのですが、その前に iOS アプリ開発をやる上で最低限理解しておきたい **Provisioning Profile** というものがあります。実機でアプリを起動するためのビルドにはこの Provisioning Profile が必須になります。

そもそもなぜこれが必要かというと、要は Apple がやりたいことは

- アプリを正しく識別したい
- アプリの開発元を明らかにしたい
- アプリを起動できる端末を限定したい

かと思います。そのため Provisioning Profile には主に以下の3つが含まれます。

- **App Id**: そのアプリで利用可能な識別子。ここで指定した ID だけがビルド時の Bundle Identifier に利用できます。同じアプリでも別のアプリとして端末にインストールしたい場合は、Bundle Identifier の末尾を変えてビルドすることがあるので、テスト用途でワイルドカードで指定することもあります。
- **Certificates**: 証明書。要は「Apple に登録された誰それのアカウントが開発している」ことを証明するもの。
- **Devices**: デバイスのUUID一覧。App Store からダウンロードされる場合を除き、ここに登録されたデバイスでしか起動できません。

Provisioning Profile は3種類（エンプラは除く）あって用途によって使い分けます。

## Development Provisioning Profile: 開発用

主に USB で繋いだデバイスでテストをする場合に使います。Xcode に Apple アカウントでログインしたら自動生成してくれるので、USB 実機テストのときはこの Provisioning Profile を意識することはないと思います。
![develop.png](https://qiita-image-store.s3.amazonaws.com/0/94526/0b8fce9a-2a8d-498b-3655-789a255ed169.png)

Development Provisioning Profile の場合、端末側がデベロッパを「信頼」をした場合のみ利用することができます（多分）。信頼は、開発用 Mac と同じネットワークにつないだ状態（？）で iPhone の「設定」 > 「一般」 > 「デバイス管理」から出来ます。


## Ad Hoc Provisioning Profile: ベータテスト用

こちらはベータテスト用で

- Certificates
- Devices
- App Id
- ↑の3つを使った Ad Hoc Provisioning Profiles

を Apple Developer の管理画面で作ってダウンロードする必要があります。
![adhoc.png](https://qiita-image-store.s3.amazonaws.com/0/94526/cbcbdee4-ddd2-c1b2-eb74-06861a1c76b8.png)

ダウンロードした Provisioning Profile でビルド、アーカイブすることで、 .ipa ファイルが生成され任意のデバイスに配布ができるようになりますが、Devices で指定したデバイスでしか起動できません。

※厳密な図は[こちらの記事](https://qiita.com/fujisan3/items/d037e3c40a0acc46f618#%E6%A6%82%E5%BF%B5%E5%9B%B3)をご覧ください（正確な動作条件も記載されています、とてもわかりやすい）。


## App Store Provisioning Profile: App Store 公開用

Ad Hoc と同じような手順で App Store 用の Provisioning Profile を作成します。

Provisioning Profile　をダウンロードして同じようにビルドをして、App Store に登録やら申請やらをして無事配信が開始されれば、すべてのデバイスが App Store からダウンロードして起動できるようになります。


# 実機でまずは動作確認

Provisioning Profile の説明が終わったので、今の状態の React Native プロジェクトを実機で起動してみましょう。

`ios/プロジェクト名.xcodeproj` を開いて [Running On Device・React Native](https://facebook.github.io/react-native/docs/running-on-device) の通りにやればできるかと思います。以下のような状態になって左上の再生マークのボタンを押すと起動できるかと思います。

> ![](https://facebook.github.io/react-native/docs/assets/RunningOnDeviceCodeSigning.png)
> –− [Running On Device・React Native](https://facebook.github.io/react-native/docs/running-on-device)

「Appが検証できません」と出た場合は、[Development Provisioning Profileの項](#development-provisioning-profile-開発用)でも書いたように iPhone の「設定」 > 「一般」 > 「デバイス管理」から信頼してください。

成功すると、実機で以下のような画面が出ます。

> ![](https://facebook.github.io/react-native/docs/assets/GettingStartediOSSuccess.png)
> –− [Getting Started | React Native](https://facebook.github.io/react-native/docs/getting-started.html)

# React Native Firebase の導入

実機確認ができたので、必要なライブラリを導入していきます。やってみた感じ React Native Firebase を最初に入れないとエラーが出てしまいがちだったので、最初に入れます。

## Bundle Identifier の設定

その前に Firebase のセットアップで Bundle Identifier を指定するステップがあるので、Xcode で設定しておきます。サイドバーの最上位の「プロジェクト名」を選択して、General タブを押すと出てきます。
![](https://i.gyazo.com/235188ddc67912088ba8f8871e4703a8.png)

ID は[こちらのスライド](https://www.slideshare.net/Nkzn/react-native8-80596018)を参考につけます。

> ![](https://image.slidesharecdn.com/devfesttokyo2017-171009065157/95/react-native8-52-638.jpg?cb=1507532038)
> ![](https://image.slidesharecdn.com/devfesttokyo2017-171009065157/95/react-native8-55-638.jpg?cb=1507532038)
> –− [React Nativeアプリをリリースし続けるために、最初に行う8つの取り組み](https://www.slideshare.net/Nkzn/react-native8-80596018)

## Firebase のセットアップ

[Firebase](https://console.firebase.google.com) でプロジェクトを作成します。作成が終わったら、**Add Firebase to your iOS App** でセットアップを始めます。

> ![](https://i.imgur.com/vebPTf1.png)
> –− [React Native Firebase - Simple Firebase integration for React Native](https://rnfirebase.io/docs/v5.x.x/installation/initial-setup)

**iOS バンドル ID** を入力して、設定ファイル（`GoogleService-Info.plist`）をダウンロードすれば良いので、ステップ2まで大丈夫です。
![](https://i.gyazo.com/7f0ee83af204f34dc371ababc067ddc9.png)

## react-native-firebase のインストール

ダウンロードが終わったらプロジェクトに戻って `react-native-firebase` をインストールします。

```
$ npm install --save react-native-firebase
```

あとは、 [iOS Installation | React Native Firebase](https://rnfirebase.io/docs/v5.x.x/installation/ios) を進めます。注意点は以下です。

- **1.1 Setup GoogleService-Info.plist** では、Finder でファイルをフォルダに入れるのではなくちゃんと Xcode 上で追加します
- **1.3 Install Firebase Library** では Cocoapods を使います
    - どうせ使うので `Podfile` に `pod 'Firebase/Auth', '~> 5.11.0'` も追加しておきましょう
    - `pod install` したあとは `.xcodeproj` ではなく `.xcworkspace` を開いて Xcode を起動するようにしましょう
- **2. React Native Firebase Installation Recommended installation** では `react-native link react-native-firebase` を使います

ちなみに `ios/Podfile` は以下のようになります。

```ruby:Podfile
platform :ios, '9.0'

target 'プロジェクト名' do
  pod 'Firebase/Core', '~> 5.11.0'
  pod 'Firebase/Auth', '~> 5.11.0'

  target 'プロジェクト名Tests' do
    inherit! :search_paths
  end
end

target 'プロジェクト名-tvOS' do
  target 'プロジェクト名-tvOSTests' do
    inherit! :search_paths
  end
end

```

## 確認

インストールが終わらせたら、 `App.js` で匿名認証を試してみます。

```js:App.js
import React from 'react';
import { View, Text } from 'react-native';
import firebase from 'react-native-firebase';

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      isAuthenticated: false,
    };
  }

  componentDidMount() {
    firebase.auth().signInAnonymously()
      .then(() => {
        this.setState({
          isAuthenticated: true,
        });
      });
  }

  render() {
    if (!this.state.isAuthenticated) {
      return null;
    }
    return (
      <View>
        <Text>Welcome to my awesome app!</Text>
      </View>
    );
  }
}

export default App;
```

実行前に Firebase コンソールで匿名認証を有効化しておく必要があります。
![](https://i.gyazo.com/88a05828b8c9733097a16c4bd0f178b5.png)

# Google ログインできるようにする

匿名認証が正常に動作したら、Google ログインを実装します。

[React Native Firebase のドキュメントの Google ログインの部分](https://rnfirebase.io/docs/v5.x.x/auth/social-auth#Google) をそのままやります。

まず、[react-native-google-signin](https://github.com/react-native-community/react-native-google-signin) をインストールします。

```
npm install --save react-native-google-signin 
```

そして、[iOS guide](https://github.com/react-native-community/react-native-google-signin/blob/master/docs/ios-guide.md) を参考にインストールを完了します。

冒頭に書いてあるように `node_modules/react-native-google-signin` の中にある `RNGoogleSignin.podspec` を削除してから

```
react-native link react-native-google-signin
```

をしましょう。あとは書いてある通りに進めたら大丈夫です。ちなみに **Install Google Sign In SDK** は **With Cocoapods** の方でやりました。

書いてあるステップがすべて終わったら、JavaScript のコードに追加しましょう。`GoogleSignIn.js` はドキュメントにあるやつをそのままコピペしてます。

```js:GoogleSignIn.js
import { GoogleSignin } from "react-native-google-signin";
import firebase from "react-native-firebase";

export const googleLogin = async () => {
  try {
    await GoogleSignin.configure();
    const data = await GoogleSignin.signIn();

    const credential = firebase.auth.GoogleAuthProvider.credential(
      data.idToken,
      data.accessToken
    );
    const currentUser = await firebase
      .auth()
      .signInAndRetrieveDataWithCredential(credential);

    console.info(JSON.stringify(currentUser.user.toJSON()));
  } catch (e) {
    console.error(e);
  }
};
```

`App.js` でボタンを押してログインできるようにします。

```js:App.js
import React from 'react';
import { View, Button } from 'react-native';
import firebase from 'react-native-firebase';
import { googleLogin } from "./GoogleSignIn";

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      isAuthenticated: false,
    };
    
  }

  componentDidMount() {
    this.unsubscriber = firebase.auth().onAuthStateChanged(user => {
     if (!user) {
       this.setState({
         isAuthenticated: false
       });
       return;
     }
    this.setState({
      isAuthenticated: true
    });
  }

  componentWillUnmount() {
    if (this.unsubscriber) {
      this.unsubscriber();
    }
  }

  _signIn() {
    googleLogin();
  }

  _signOut() {
    firebase.auth().signOut();
  }

  render() {
    return (
      <View>
        {!this.state.isAuthenticated && <Button onPress={this._signIn} title="Sign in" />}
        {this.state.isAuthenticated && (
          <Button onPress={this._signOut} title="Sign out" />
        )}
      </View>
    );
  }
}

export default App;
```

ボタンを押すと Google アカウントでログインする画面が出てきて、ログインができるようになります。

実行する前に、Firebase のコンソールから Google 認証を有効にしておく必要があります。

![](https://i.gyazo.com/22a60ab8cb9d132a5c42937b8b6d2b37.png)

# react-native-background-geolocation を導入する

[react-native-background-geolocation](https://github.com/transistorsoft/react-native-background-geolocation) というバックグラウンドで位置情報を送信してくれるプラグインをインストールします。1点制限があって、 Android でリリースビルドするには $299 かかります。Android 版を出さないなら関係ないです。

これとは別に、個人？が作っている [react-native-mauron85-background-geolocation](https://github.com/mauron85/react-native-background-geolocation) というライブラリもあります。ただ、試してみましたがやはり前者が安定している、というかこっちは使い物にならなかったです。

では、インストールをしていきます。

```
npm install --save react-native-background-geolocation 
```

そして、[iOS Manual Instllation](https://github.com/transistorsoft/react-native-background-geolocation/blob/master/help/INSTALL-IOS.md) を進めます（[iOS Installation with react-native link](https://github.com/transistorsoft/react-native-background-geolocation/blob/master/help/INSTALL-IOS-RNPM.md) でも良いと思いますが、`react-native-google-signin` と同じように `node_modules` 内にある link するモジュールの `.podspec` ファイルを削除してから `react-native link` するようにしましょう）。

インストールが完了したら JavaScript のコードを変更します。

```js:App.js
import React, { Component } from "react";
import { View, Button } from "react-native";
import firebase from "react-native-firebase";
import BackgroundGeolocation from "react-native-background-geolocation";
import { googleLogin } from "./GoogleSignIn";

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      user: null,
      inited: false
    };
  }
  componentDidMount() {
    this.unsubscriber = firebase.auth().onAuthStateChanged(user => {
      if (!user || !user.uid) {
        this.setState({ user: null });
        BackgroundGeolocation.stop(); // トラッキングを停止する
        return;
      }
      this.setState({ user });
      if (this.inited) {
        BackgroundGeolocation.start(); // トラッキングを開始する
        return;
      }
      BackgroundGeolocation.ready( // トラッキングの設定をする
        {
          desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH, // 正確さの度合い
          distanceFilter: 10, // 10m以上移動してトラッキングされる
          debug: false, // false だとデバッグ用の通知を OFF にする
          stopOnTerminate: false, // false だとアプリを終了させても動くようになる
          url: "...あとで Cloud Functions のURLを入れる...",
          autoSync: true, // あとで説明
          autoSyncThreshold: 5, // あとで説明
          batchSync: true, // あとで説明
          params: { // POST リクエストに付け足したいパラメータ
            userId: user.uid 
          }
        },
        state => {
          this.setState({ inited: true });
          BackgroundGeolocation.start(); // トラッキングを開始する
        }
      );
    });
  }
  componentWillUnmount() {
    if (this.unsubscriber) {
      this.unsubscriber();
    }
  }
  _signIn() {
    googleLogin();
  }
  _signOut() {
    firebase.auth().signOut();
  }
  render() {
    return (
      <View>
        {!this.state.user && <Button onPress={this._signIn} title="Sign in" />}
        {!!this.state.user && (
          <Button onPress={this._signOut} title="Sign out" />
        )}
      </View>
    );
  }
}
```

## 注意点

[このライブラリの思想](https://github.com/transistorsoft/react-native-background-geolocation/wiki/Philosophy-of-Operation)として、できるだけバッテリーをセーブするために不必要なトラッキングはしないように作られています。

とてもありがたい設計ですが、デバッグをするときに少し困ることがときがあるので注意点をいくつか紹介します。

### stationary モードについて

iOS だと同じところにとどまっていると **stationary** というモードになります。このモードになるとこのモードを抜け出すまでトラッキングが開始しません。

[ドキュメント](https://transistorsoft.github.io/react-native-background-geolocation/interfaces/_react_native_background_geolocation_.config.html#stationaryradius)を見る限り、**stationaryRadius** が最低値の **25** の場合、**200m** 以上動かないとモードから抜け出さないようです。

### トラッキングする間隔

トラッキングする間隔は **distanceFilter** で指定できます。distanceFilter は距離をメートルで指定できて、指定した距離以上移動しない限りトラッキングしません。

ただ、デフォルトで「移動速度によって distanceFilter を変化させる」設定になっているので、自転車や車に乗ると指定した distanceFilter より長くなることに注意しましょう。

### HTTP リクエストを送るタイミング

**autoSync** を **true** にすると（デフォルトで true です）トラッキングするたびに位置情報を POST リクエストしてくれます。

ただこれだと結構頻繁にリクエストを送ってしまうので **autoSyncThreshold** で何回に一回リクエストするかを指定できます。例えば autoSyncThreshold を5に設定すると5回トラッキングされて初めてリクエストします。

もうひとつ、このままだと5回トラッキングされたあと、一気に5回リクエストされます。

そこで、**batchSync** を **true** にすると、5つの位置情報を配列にして1回だけリクエストしてくれるようになります。

これまでの話をざっくり図にすると以下のようになります。

![locations.png](https://qiita-image-store.s3.amazonaws.com/0/94526/d829eddb-de82-26fa-f472-81958787ca3f.png)

# Cloud Functions の導入

良い感じに位置情報をリクエストする仕組みまで作ったので、リクエスト先を作ります。

Cloud Functions で HTTPS リクエストを受け付けるようにして、Firestore に保存するようにします。

## Cloud Functions のセットアップ

[Firebase CLI](https://firebase.google.com/docs/cli/?hl=ja) を入れてセットアップしましょう。

```
npm install -g firebase-tools
firebase login
```

プロジェクトにて

```
firebase init
```

をやると

```
❯◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◯ Functions: Configure and deploy Cloud Functions
 ◯ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
```

と出てくるので Firestore と Functions を選択して進めましょう。最終的に色々ファイルが生成されます。

## Firestore に保存するコードを書く

`functions/index.js` で受け取った配列を Firestore に保存するコードを書きます（今回の制約上 userId をトークン化せずに生で渡して処理しているので、userId まわりのセキュリティルールは慎重に設定しましょう）。

なお、[`location` データのスキーマはこちら](https://github.com/transistorsoft/react-native-background-geolocation/wiki/Location-Data-Schema)にあります。

```js:functions/index.js
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const serviceAccount = require("./serviceAccount.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://__YOUR_FIREBASE_APPID__.firebaseio.com"
});

const db = admin.firestore();
const settings = { timestampsInSnapshots: true };
db.settings(settings);

exports.saveLocation = functions.https.onRequest(async (req, res) => {
  if (!req.body.location || req.body.location.length < 1 || !req.body.userId) {
    res.status(400).send("Bad Request");
    return;
  }
  const { userId, location } = req.body;
  try {
    for (let i = 0; i < location.length; i++) {
      let {
        coords: { latitude, longitude },
        timestamp
      } = location[i];
      await db.collection(`users/${userId}/locations`).add({
        latitude,
        longitude,
        timestamp: new Date(timestamp)
      });
    }
    res.send("Success");
  } catch (e) {
    console.log(e);
    res.status(400).send("Bad Request");
  }
});
```

この例では async, await を使っているので、 `functions/package.json` で以下の指定をして Node v8 にします。

```json:functions/package.json
{
  ...
  "engines": {
    "node": "8"
  },
  ...
}
```

`const serviceAccount = require("./serviceAccount.json");` と呼び出しているのは admin 用の認証情報が入ったファイルです。以下でダウンロードして、`functions/` に `serviceAccount.json` にリネームして置いておく必要があります。公開不可なので `.gitignore` に忘れずに入れましょう。

![](https://i.gyazo.com/95134b8fa03ad6a492afb32621aadeb2.png)

## デプロイして確認

最後にデプロイします。

```
firebase deploy
```

デプロイが終わるとコンソールに `https://us-central1-*****.cloudfunctions.net/saveLocation` のように Functions の URL が表示されるので、[先ほどの](#react-native-background-geolocation-を導入する)React Native 側のコードの `url` に指定します。


これでビルドをしてログインすると位置情報が保存されるかと思います。

# CI の利用

とりあえずアプリは完成したので、CI の設定に入ります。
CI サービスは評判良さげな [Bitrise](https://www.bitrise.io/) を使います。

なお、このアプリだとどうしてもビルド時間が無料枠の10分を超えてしまうので、30日のトライアル後は **$40/mo** 課金して Developer プランを続ける必要があります。

## Code Signing について

CI でビルドをするにあたってコードサイニングについて知っておくと良いです。

コードサイニングとは [「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典](https://wa3.i-3-i.info/word16535.html)的に言うと

> まぁ「コードサイニング」って単語が出てきたら「コード署名（『改ざんとかされてないよ！』を証明するためにプログラムに付ける電子署名）のことなんだな～」と、お考えください。

とのことです。

iOS アプリ開発では、[こちらの記事](https://qiita.com/fujisan3/items/d037e3c40a0acc46f618#%E6%A6%82%E5%BF%B5%E5%9B%B3)の画像にもあるように、MacBook にある秘密鍵と Apple Developer で作った証明書に含まれる暗号化された公開鍵を付き合わせてその「改ざんされているかチェック」を行います。

> ![](https://camo.qiitausercontent.com/876674b1ba1921954d253a43ad575145ba79ea58/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f35333832332f63353766396136382d393533662d383762392d343035352d6365333839373665613966632e706e67)
> –– [iOSアプリのプロビジョニング周りを図にしてみる](https://qiita.com/fujisan3/items/d037e3c40a0acc46f618)

つまり、CI でビルドするとなると CI 上に秘密鍵が必要になります。

[こちらの記事](https://dev.classmethod.jp/smartphone/iphone/apple-certificates-summary-and-etc/)を見るとわかりやすいのですが、Certificates から生成できる .p12 ファイルが秘密鍵を含んだものになるので、これを CI にアップロードすることになります。

> Certificatesから出力できるファイル。Certificatesは証明書のみ（証明書と、必要な秘密鍵が書かれているのみ。実際の鍵は入っていない。）で、p12ファイルは秘密鍵を含めた証明書なので管理は厳重に。Admin権限をもつことと等価。プッシュサービス（Repro, Parse）などのMBaaSを利用する際にはPush通知用のこのファイルを書き出してサービス側に登録したりすることもある。
> ![](https://cdn-ssl-devio-img.classmethod.jp/wp-content/uploads/2016/08/8ef77e52-e771-d6ef-3a35-b3d3edb58200.png)
> [Appleの証明書とかまとめ！](https://dev.classmethod.jp/smartphone/iphone/apple-certificates-summary-and-etc/)

## Bitrise のセットアップ

新規登録して、アプリを作成しようとするとウィザードが出てくるので進めます。自動でファイルを解析してくれるので[こちらの記事](https://blog.bitrise.io/how-to-set-up-a-react-native-app-on-bitrise)を参考によしなに進めます。

iOS のプロジェクトパスは `.xcodeproj` ではなく `.xcworkspace` になることに注意しましょう。

セットアップが終わったらテストビルドが走って失敗しますが無視して、アプリの管理画面から Workflow Editor を開きます。

![](https://i.gyazo.com/f9d3cc5365adb6a71268546257bfb666.png)

## Workflows

**Workflows** は以下のようにしました。

![bitrise-workflow.png](https://qiita-image-store.s3.amazonaws.com/0/94526/9e43497c-5ed5-c637-a839-b5c1e20db6f1.png)


### Activate SSH Key

git にアクセスする ssh key を利用できるようにします。git 最初のセットアップのウィザードで自動で設定でき、デフォルトのままで大丈夫です。

### Git Clone Repository

最初のセットアップのウィザードで指定したブランチを取ってきます。こちらもデフォルトのままで大丈夫です。

### Set iOS Info.plist - Bundle Version and Bundle Short Version String

アプリのバージョンを更新するステップです。

アプリのバージョンは2種類あって、公開されるバージョン（ユーザーが使う上で機能が変わったときに変える）と内部のバージョン（コードをいじってビルドするたびに変える）があります。**バージョン番号**と**ビルド番号**という言われ方もするようです。

僕の場合は **Git のタグ**（`$BITRISE_GIT_TAG`で呼び出せます）をバージョン番号、**Bitrise のビルドナンバー**（`$BITRISE_BUILD_NUMBER`で呼び出せます）をビルド番号にしています。

![](https://i.gyazo.com/c6f9533c1572941ec087b9fb7db40cd3.png)

2つあるのは **~~Tests** の方のバージョンも上げているからです。もうひとつの **Info.plist file path** は以下になります。

![](https://i.gyazo.com/7cc5bbf847e56d25c2d5ef8e5f956fb8.png)

### Stamp AppIcon with version number

アプリのアイコンに今のバージョン番号とビルド番号を乗っけてくれるやつです。`バージョン番号(ビルド番号)`みたいに表示されます。

<img width="88" alt="スクリーンショット 2018-12-14 3.43.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/94526/789b4175-4715-8b06-8a41-08c18ec79f56.png">

Info.plist と同様に `$BITRISE_GIT_TAG` と `$BITRISE_BUILD_NUMBER` を使用します。

![](https://i.gyazo.com/bbb9063cbf9ebcca1cd6b1a8ef14999a.png)

### Script 

任意のスクリプトを実行できるステップです。ここで git のコミットとプッシュをします。

```
#!/usr/bin/env bash
# fail if any commands fails
set -e
# debug log
set -x

# write your script here
git checkout master
git add ios/__YOUR_PROJECT_NAME__/Info.plist ios/__YOUR_TEST_PROJECT_NAME__/Info.plist
git commit -m "Update Version"
git push origin master

# or run a script from your repository, like:
# bash ./path/to/script.sh
# not just bash, e.g.:
# ruby ./path/to/script.rb
```

### Run npm command

npm コマンドを実行できるステップで React Native のセットアップをすると `npm install` するやつがデフォルトでついてくるのでそのまま使います。

### Run CocoaPods install

今回は CocoaPods を使っているので `pod install` が必要になります。**Podfile path** は `ios/Podfile` と指定する点に注意しましょう。

### Certificate and profile installer

こちらはコードサイニング用の秘密鍵と Provisioning Profile をインストールするステップです。

追加して設定は何もいじらなくても良いですが、秘密鍵と Provisioning Profile をアップロードする必要があります（あとで説明します）。

### Xcode Archive & Export for iOS

ビルドするステップです。**Select method for export** だけ、ベータテスト用の `ad-hoc` に変更しましょう。

### HockeyApp iOS Deploy

ベータテスト配信用のサービス HockeyApp にデプロイするステップになります。HockeyApp でプロジェクトを作成して API TOKEN と APP ID を控えてくる必要があります（あとで説明します）。


## Code Signing

[コードサイニングについて](#code-signing-について)で述べたように、コードサイニング用のファイルをアップロードします。

![](https://i.gyazo.com/8e7c854658a7d70764def0c6938db7cb.png)

その前に **Distribution** 用の Certificates と **Ad Hoc** Provisioning Profile を作成して Xcode に入れておく必要があります。以下の記事が簡潔でわかりやすいです。

- [[iPhone] iOS, Certificate 証明書を作ってみる](https://i-app-tec.com/ios/apply-application.html)
- [[iPhone] Provisioning Profile を作ってみる](https://i-app-tec.com/ios/provisioning-profile.html)

終わったら、Bitrise にも書いてあるように以下のコマンドで CodeSignDoc というのを使って .p12 ファイルの取り出しと Bitrise へのアップロードを行います。言われるがままに進めれば大丈夫です。

```
bash -l -c "$(curl -sfL https://raw.githubusercontent.com/bitrise-tools/codesigndoc/master/_scripts/install_wrap.sh)"
```

## HockeyApp のセットアップ

アプリをデプロイするために [HockeyApp](https://hockeyapp.net/) でセットアップしましょう。

アカウント作成して、 App を作成しようとすると「Upload しろ」みたいなこと言われますが、**Create the app manually instead.** のリンクをクリックして進めましょう。

Title や Bundle Indentifier などを決めて、作成が完了したら App Id と API KEY を探します。

作成してすぐ見れるアプリの Overview に App Id があります。

![](https://i.gyazo.com/142c290a431af9c66d2270de6801e07f.png)

API_TOKEN は右上のアカウントのドロップダウンにある Account Settings から行けます。

![](https://i.gyazo.com/d74f334d6610f57518ea468c79d7f978.png)

## Secrets

Bitrise では機密性の高い情報は **Secrets** に入れるようになっています（そうではない情報は **Env Vars** に入れます）。

ここに `$HOCKEY_API_TOKEN` と `$HOCKEY_APP_ID` を入れます。

![](https://i.gyazo.com/064da1f44e22453947b985bdfe533f40.png)


**HockeyApp iOS Deploy** のステップでプルダウンで選択できるようになるので、それぞれ指定します。

![](https://i.gyazo.com/47c08d99a7157d12586585fc1775c616.png)

---

ここまでやって git で適当にタグを打ってビルドを実行すると、うまく行けば HockeyApp にアプリがアップロードされます。


# アプリを配布

アップロードされたファイルをテスターに配布しましょう。今回は URLを共有してテスターを募る方法を採用します。

HockeyApp の App 管理画面の Users の Recruitment から設定します。

![](https://i.gyazo.com/629085d2ca092978cf4888fcb3c61819.png)

設定画面は好きなように設定します。以下は一番ガバガバな設定です。

![](https://i.gyazo.com/9a06cb1ec2cba06d15541852eb5cc752.png)

設定が終わると、URL が出てくるのでこれを共有すると招待ができます。

![](https://i.gyazo.com/ad51582de963376b023c6e673483feba.png)

## Provisioning Profile に UUID を追加

これですぐにテスターがアプリをダウンロードできるようにはなりません。

[Ad Hoc Provisioning Profile](#ad-hoc-provisioning-profile-ベータテスト用) で説明したように、Provisioning Profile にデバイスの UUID が入っていないと起動できません。

HockeyApp では追加されたユーザーのデバイスが Provisioning Profile に入ってるか自動的に判断してくれます。

![](https://i.gyazo.com/e9b089aee28b83dc28669e5797729a07.png)

App 管理画面の Users の Devices に行くと、デバイス一覧が見れます。そこにある **Export** の **Unprovisioned Devices** を押すと、Provisioning Profile に入ってないデバイス一覧をダウンロードできます。

![](https://i.gyazo.com/1101948fd6855f2105b21de2f1f78555.png)

このファイルは Apple Developer の Devices にファイルごとアップロードして一括追加することができます。

![](https://i.gyazo.com/9a92048c68171fc76f7cb565db81b21c.png)

追加が終わったら Provisioning Profile の作成を再度行います。追加したすべてのデバイスを選択して、作成します。作成した Provisioning Profile は Bitrise の Code Signing にドラックアンドドロップすれば大丈夫です。

これで再度ビルド・配布が終われば、テスターがダウンロード・起動できるようになります。

# おわりに

React Native といえば「Web エンジニアでも簡単にアプリが作れる！」というイメージがありますが、まともにやろうとすると理解しないといけないことが山ほどあって、今でも理解できているかわからないような状態で苦労しました。アプリエンジニアすごい。

[こちらの記事](https://tech.maricuru.com/entry/2018/04/09/142341)など見て Expo だとだいぶ楽になる印象があり、[最初の方に言った](#では-react-native-でやるただ-expo-では無理)ようにバックグラウンド処理も Expo で使えるようになるっぽいので、Expo に期待しているのが現状です。

## 〇〇使わないの？

選択肢にあったけど今回使わなかったものも紹介します。

### react-native-background-geolocation-firebase

react-native-background-geolocation の拡張で [react-native-background-geolocation-firebase](https://github.com/transistorsoft/react-native-background-geolocation-firebase) という、トラッキングのたびに Firestore に直で保存してくれるやつもあります。

これなら Cloud Functions 使わなくてもよくなりますが、これ使うと不要なデータも保存されてしまう上に、最新の位置情報だけ保存したくなったり、Firebase じゃないやつ使いたくなったりしたときに変更の手間が発生するので、臨機応変に対応できる functions に POST するほうがマシかなあと感じています。。

### fastlane

[fastlane](https://fastlane.tools/) はコードサイニングやビルドを Ruby で書けるやつです。今回 Bitrise でやってることをまるまる Ruby で書くかたちになります。

今回採用しなかった理由は以下です。

- Bitrise で事足りる
- fastlane を使うとなると、コードサイニングで private の Git リポジトリを使う必要があって余計に課金が必要になる
- CI 上のコードサイニングの過程で Apple ID の2段階認証を求められてハマった

fastlane を使うのは、本腰入れて複数人で開発するようになって Apple Developer の Enterprise Edition に登録してからが良いのかなと感じました。


### Bitrise Auto Provisioning

Bitrise には Auto Provisioning という Bitrise に秘密鍵さえ置いてしまえば自動的に Provisioning Profile を作ってくれる便利な機能があります。

これを使えば、テスターの端末追加時に Apple Developer にていちいち Provisioning Profile を作り直さなくてよくなるっぽいです。

ただ、今 Apple 側の仕様変更で2段階認証が有効なアカウントでの利用が実質不可能になっていて、チャットお問い合わせしたところ以下のような回答が返ってきました。

> To summarise the issue: currently the session provided for 2FA Apple Developer Accounts is only kept alive for about 8 hours(this means that you have to re-authenticate on your profile page every 8 hours)

Auto Provisioning は Bitrise に Apple Id で2段階認証後、そのセッションを保管して使うことで実現していましたが、そのセッションの有効期間が最大8時間になったため8時間ごとに Bitrise 上で2段階認証をしないと使えなくなってしまった、とのことでした。


### DeployGate / TestFlight etc.

HockeyApp の代替。単純に身近にいる人が HockeyApp 使ってて馴染みがあったので HockeyApp 使いました。特別な意味はないです。

## 参考になりすぎた記事たち

- [iOSアプリのプロビジョニング周りを図にしてみる](https://qiita.com/fujisan3/items/d037e3c40a0acc46f618)
- [個人開発だと適当になりがちなiOSの証明書とアカウントを理解する](https://qiita.com/guitar_char/items/7dbd7e2bc35a1fc9e5a0#_reference-8a77bfe3399d2fc06c3c)
- [React Nativeアプリをリリースし続けるために、最初に行う8つの取り組み](https://www.slideshare.net/Nkzn/react-native8-80596018)
- [「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典](https://wa3.i-3-i.info/word16535.html)
- [Appleの証明書とかまとめ！](https://dev.classmethod.jp/smartphone/iphone/apple-certificates-summary-and-etc/)
- [iOS アプリ開発](https://i-app-tec.com/)