FlutterからXcodeへ環境変数を渡す

## 概要
FlutterでiOSアプリをビルドする際には、本番環境と開発環境で`GoogleService-info.plist`や`Bundle Identifier`の切り替えが必要となるケースが多いと思います。
この切替には複数のSchemeを用意する方法、`Flavor`を使う方法、ビルド時の`--release`と`--debug`を使って切り分ける方法がありますが、Flutter 1.17からビルド引数の`--dart-define`で定数を渡せるようになりました。今回はこのビルド変数の渡し方を説明します。

Android編は[こちら](https://qiita.com/yana1316/items/9a19d0d15ff6daff99ee)

### 前提
- Flutterのサンプルプロジェクトを作成しシミュレーターで実行できる
- Flutter 1.20以降

### 環境
```
$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 1.22.4, on Mac OS X 10.15.7 19H2 darwin-x64, locale en-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 12.1)
[✓] Android Studio (version 4.1)
[✓] Connected device (1 available)      
```

### サンプルコード
https://github.com/yana1316/flutter_define

## 前半の設定
前半では、FlutterからXcodeへ変数を渡し確認してみます。
### Flutterプロジェクトの用意
既存のプロジェクトでも、新規にプロジェクトを作成しても問題ありません。
一度ビルドして、シミュレーター上で実行できることを確認しておいてください。

### Xcodeの設定
Android StudioのProject Windowから下記の`[project]/ios`を右クリックし`Flutter`内の`Open iOS module in Xcode`をクリックします。すると該当ProjectのXcodeが開きます。
![Screen_Shot_2020-12-29_at_8_30_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/f61e7cb7-64b4-6769-42b5-b2b880133c01.png)

Xcodeメニューの`Product`>`Scheme`>`Edit Scheme`をクリックします。
![Screen_Shot_2020-12-28_at_10_29_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/8a67ba49-e00b-2e6c-6ba5-f55ae4ace69c.png)

開いたウィンドウの`Build`>`Pre-actions`を開き、`Provide build setting from`を設定し、画面下部の`+`を押して`New Run Script Action`を追加します。
![Runner_xcworkspace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/34c21704-2004-e1a3-ec48-d4436e7ae49b.png)


以下のスクリプトを追加します。`urldecode()`を挟むことで、文字列がURLデコードされ見やすくなります。`${PROJECT_DIR}`はFlutterの`ios`ディレクトリです。`$DART_DEFINES`はFlutterからXcodeに渡される定数です。
そして、このスクリプトが実行されると`[project]/ios/prebuild.log`が自動的に生成され、その中にFlutterから渡したビルド引数が書き込まれます。

```sh
function urldecode() { : "${*//+/ }"; echo "${_//%/\\x}"; }

exec > ${PROJECT_DIR}/prebuild.log 2>&1
echo $(urldecode "$DART_DEFINES")
```

### Flutterの設定
Flutter側はビルド時に指定します。

```sh
flutter build ios --dart-define=DEFINE_HELLO=world --dart-define=DEFINE_FOO=bar
```

Android Studio を使っている場合`Run/Debug Configurations`にて設定可能です。
![Run_Debug_Configurations.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/6c8d3507-4cf9-b4ab-0745-cf68ded7bce3.png)



### 前半の動作確認
Flutterをビルドすると`[project]/ios/prebuild.log`が出力されていると思いますので確認してみてください。以下のように出力されていれば、ここまでは成功です。


```text
DEFINE_HELLO=world,DEFINE_FOO=bar
```

## 後半の設定
後半では、実際にXcodeへ渡した変数の値をFlutter上で確認してみます。

### Xcodeの設定
Flutterから渡された変数をXcode上で扱えるように設定していきます。
まずFlutterから渡された変数のデフォルト値を作成します。

※ このデフォルト値の設定について、私は必要性を理解できませんでした。参照元の[Denisさんの記事](https://itnext.io/flutter-1-17-no-more-flavors-no-more-ios-schemas-command-argument-that-solves-everything-8b145ed4285d)では以下のようにだけ書かれています。もしかすると、複数人で開発をしたとき、うっかりビルド引数を指定することを忘れるとエラーとなってしまうのを避けるためかもしれません。
> Consider putting Defineexample.xcconfig after Defineexample-defaults.xcconfig, so values from Flutter Run command could override default values.

XcodeのFlutterディレクトリで右クリックし`New File`を追加します。
![Screen_Shot_2020-12-28_at_13_17_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/967186fe-1c0d-a1c8-a0af-ff2e099e50b1.png)
`Configuration Settings File`を選択し`Targets`にチェックを入れ、`DartDefineDefaults.xcconfig`という名前で`Flutter`ディレクトリに保存します。（名称は何でも大丈夫です）
![Save_and_Runner_xcworkspace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/0345c631-e096-9ede-931d-dcbfde6da192.png)

次に作成した`DartDefineDefaults.xcconfig`を開きデフォルト値を設定します。
![DartDefineDefaults_xcconfig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/5309a8dc-de1b-c838-c6f5-e9887a988f82.png)


その後`Debug.xcconfig`と`Release.xcconfig`に、先程つくった`DartDefineDefaults.xcconfig`と、このあと設定する`DartDefine.xcconfig`を`include`する設定を追加していきます。
![Debug_xcconfig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/45c66157-6509-4d63-63fa-57426340fde6.png)

このとき`DartDefineDefaults.xcconfig`が先に読み込まれ、その後に`DartDefine.xcconfig`（後のスクリプトで自動生成される）が読み込まれるようにしてください。こうすることで、例えば先に`DEFINE_FOO=bar`が定義され、後から`DEFINE_FOO=hoge`が定義された場合、後からの値で上書きすることができます。後からの値が定義されていなければ、デフォルト値のみが定義されるという仕組みです。

次にPre-actionsの中身を変えていきましょう。
![Release_xcconfig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/b08a60fe-dda4-d087-9a6d-31aab316e136.png)

このような形にスクリプトを修正します。~~（[Denisさんの元記事](https://itnext.io/flutter-1-17-no-more-flavors-no-more-ios-schemas-command-argument-that-solves-everything-8b145ed4285d)ではビルド引数のPrefixでgrepし、特定の引数のみを読み込むように工夫されています）~~ [2020/12/31 下記に修正]
工夫されていますではなく、対応したほうが良さそうです。
下の例のように定数のPrefixでgrepするのが良さそうです。これをやらないと`define_items[@]`の中には`flutter.inspector.structuredErrors=true`などが入ってきます。これが原因なのかは定かではありませんが、意図しない値が`DartDefine.xcconfig`に書き込まれることで、うまくコンパイルできないケースがありました。（実際には落ちずに中途半端な形でコンパイルが通ってしまい、原因を特定するのにすごく時間がかかりました。）


```sh
function urldecode() { : "${*//+/ }"; echo "${_//%/\\x}"; }

#exec > ${PROJECT_DIR}/prebuild.log 2>&1
#echo $(urldecode "$DART_DEFINES")

IFS=',' read -r -a define_items <<< "$DART_DEFINES"

for index in "${!define_items[@]}"
do
    define_items[$index]=$(urldecode "${define_items[$index]}");
done

#printf "%s\n" "${define_items[@]}" > ${SRCROOT}/Flutter/DartDefine.xcconfig
printf "%s\n" "${define_items[@]}"|grep '^DEFINE_' > ${SRCROOT}/Flutter/DartDefine.xcconfig
```

Xcodeの設定の最後に`Bundle name`と`Bundle identifer`に定義した変数が反映するよう修正します。
![Runner_xcodeproj.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/161d8027-e6dc-4ab1-746b-86a9e66d9718.png)
これでXcode側の設定は完了です。

### Flutterの設定
基本的には、下記のサンプルコードを参考にしてもらえれば大丈夫なはずです。
https://github.com/yana1316/flutter_define

`pubspec.yaml`に追記します。

```yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.0
  package_info: ^0.4.3+2 # 追加
```

あとは`main.dart`でパッケージ名を表示させます。

```dart
Text(
  'Package Name',
  style: Theme.of(context).textTheme.headline5,
),
FutureBuilder<PackageInfo>(
    future: PackageInfo.fromPlatform(),
    builder: (context, value) {
      if (!value.hasData) {
        return Container();
      }
      return Text(
        value.data.packageName,
        style: Theme.of(context).textTheme.headline6,
      );
    }
),
```

### 後半の動作確認
うまく行けば、アプリの名前の後ろに指定した値（`DEFINE_FOO=bar`としたならbar）が付き、画面上ではパッケージ名の後ろに値がついていると思います。
![iPhone_SE_–_2nd_generation_–_14_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315258/ee61fa3a-e804-33a9-38ac-15fe29dfff92.png)

## 参考
- [Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything](https://itnext.io/flutter-1-17-no-more-flavors-no-more-ios-schemas-command-argument-that-solves-everything-8b145ed4285d)
- [Flutter 1.20 — What you should know before you upgrade your project that uses compile-time variables](https://medium.com/@tatsu.ukraine/what-you-should-know-before-you-upgrade-flutter-in-your-project-with-compile-time-variables-3ec3d2e9ba79)