Rork で書き出したアプリに AdMob を組み込んだ翌週、別のライブラリを足そうとして npx expo prebuild を走らせたら、前の週に手で書き込んだ Info.plist の GADApplicationIdentifier がきれいに消えていました。ios/ フォルダごと作り直されるのですから、当然です。けれど当時の私はそれを知らず、また手で書き戻して、また消して、を数回繰り返してしまいました。
2014年から個人でアプリを作ってきて、累計5,000万ダウンロードに育った壁紙アプリ群を今は6本並行で運用しています。同じ AdMob の初期設定を6回手で書くのは、間違いの温床でしかありません。1本だけ App ID を貼り間違えて、リリース後に広告が一切表示されなかったこともありました。この「手作業が native 層に残り続ける」問題を根本から無くしたのが、自作の Expo config plugin でした。
なぜ prebuild のたびに native 設定が消えるのか
Rork や Expo の開発体験は、ios/ と android/ を意識しなくて済むところに価値があります。これらのネイティブフォルダは「成果物」であって「ソース」ではありません。app.config.ts と package.json という宣言から、npx expo prebuild が毎回ネイティブプロジェクトを生成し直します。
ここを取り違えると延々とハマります。生成物である ios/MyApp/Info.plist を手で編集しても、それは砂浜に書いた文字のようなもので、次の prebuild で波にさらわれます。.gitignore に ios/ と android/ を入れる「managed 寄り」の運用をしているなら、なおさらです。
正しい考え方は「ネイティブ設定もまた宣言から生成されるべき」というものです。その宣言と生成のあいだを橋渡しするのが config plugin です。両祖父が宮大工で、図面の通りに寸分違わず木を刻む姿を見て育ったせいか、私は「同じものは同じ手順から再現できる」状態をとても大切にします。config plugin は、まさにネイティブ設定にその再現性を与えてくれます。
config plugin の仕組み — prebuild の中で何が起きているか
config plugin は、@expo/config-plugins が用意した一連の「mod」を使って、prebuild の途中でネイティブファイルを書き換える関数です。代表的な mod は次の通りです。
withInfoPlist — iOS の Info.plist を JS オブジェクトとして受け取り、書き換える
withEntitlementsPlist — iOS の entitlements(Associated Domains 等)を扱う
withAndroidManifest — Android の AndroidManifest.xml をパース済みオブジェクトで扱う
withAppBuildGradle — app/build.gradle の文字列を直接編集する
withDangerousMod — 任意のファイルに直接アクセスする最終手段
prebuild は、これらの mod を順番に適用してからネイティブプロジェクトを書き出します。つまり手で Info.plist を編集する代わりに、「prebuild のたびに必ずこの値を入れてくれ」という指示を JS で書いておくわけです。
最小の config plugin を書いてみる
まず感覚を掴むために、Info.plist に1つキーを足すだけの小さなプラグインを作ります。プロジェクト直下に plugins/withHello.js を置きます。
// plugins/withHello.js
const { withInfoPlist } = require("@expo/config-plugins");
module.exports = function withHello(config) {
return withInfoPlist(config, (config) => {
// config.modResults が Info.plist の中身(JSオブジェクト)
config.modResults.NSAppGreeting = "Hello from a config plugin";
return config;
});
};
そして app.config.ts の plugins 配列に登録します。
// app.config.ts
export default {
expo: {
name: "WallpaperApp",
plugins: [
"./plugins/withHello.js",
],
},
};
npx expo prebuild --clean を実行すると、生成された ios/WallpaperApp/Info.plist に NSAppGreeting が入っています。手で書いていないのに、です。これで「ネイティブ設定を宣言から生成する」感覚が掴めたはずです。
AdMob と ATT をまとめて差し込むプラグイン
ここからが本題です。壁紙アプリで毎回必要になるのは、AdMob の App ID、ATT(App Tracking Transparency)の説明文、そして SKAdNetwork の識別子リストです。これらを1つのプラグインに集約します。
Before の運用は、prebuild のあとに次のような編集を手でやっていました。
# 毎回 prebuild のあとに手作業(消える運命)
1. ios/WallpaperApp/Info.plist を開く
2. GADApplicationIdentifier を貼る
3. NSUserTrackingUsageDescription を書く
4. SKAdNetworkItems を延々と貼る
5. → 次の prebuild で全部消える → 1 に戻る
After は、この手順を丸ごとプラグインに移します。
// plugins/withAdConfig.js
const { withInfoPlist } = require("@expo/config-plugins");
// よく使う SKAdNetwork ID(実際には広告SDKの指定に合わせて増やします)
const SKADNETWORK_IDS = [
"cstr6suwn9.skadnetwork",
"4fzdc2evr5.skadnetwork",
"v9wttpbfk9.skadnetwork",
];
module.exports = function withAdConfig(config, props) {
const { admobAppId, trackingMessage } = props;
return withInfoPlist(config, (config) => {
const plist = config.modResults;
// 1) AdMob の App ID
plist.GADApplicationIdentifier = admobAppId;
// 2) ATT の説明文(ローカライズは別途 InfoPlist.strings で上書き)
plist.NSUserTrackingUsageDescription = trackingMessage;
// 3) SKAdNetwork のリストを冪等に組み立てる
plist.SKAdNetworkItems = SKADNETWORK_IDS.map((id) => ({
SKAdNetworkIdentifier: id,
}));
return plist && config;
});
};
呼び出し側はこう書きます。プラグインに引数を渡すときは、配列の2要素目に props を入れます。
// app.config.ts
export default {
expo: {
name: "WallpaperApp",
plugins: [
[
"./plugins/withAdConfig.js",
{
admobAppId: process.env.ADMOB_IOS_APP_ID,
trackingMessage:
"あなたに合った壁紙やおすすめを表示するために利用します。",
},
],
],
},
};
ここで process.env から App ID を読んでいるのが要点です。値をコードに直書きせず、アプリごとに .env を切り替えるだけで同じプラグインを使い回せます。私は App ID の貼り間違いで広告が出なかった失敗を踏んでいるので、「ID は1か所でだけ定義する」という設計を強く推奨します。
6本のアプリで共有する — 設定モジュール化
6本それぞれのリポジトリに withAdConfig.js をコピーすると、結局コピーの数だけ修正箇所が増えます。SKAdNetwork のリストは広告 SDK の更新でときどき変わるので、6か所を手で揃えるのは現実的ではありません。
私は共有パッケージにまとめる構成にしました。手順は次の3ステップです。
@dolice/native-config という小さな npm パッケージ(あるいは monorepo の内部パッケージ)を作り、withAdConfig.js と SKAdNetwork リストをそこに置く
- 各アプリの
package.json でそれを依存に加える
app.config.ts からはパッケージ名で参照する
// 各アプリの app.config.ts(6本とも同じ書き方)
export default {
expo: {
name: "WallpaperApp",
plugins: [
[
"@dolice/native-config/withAdConfig",
{
admobAppId: process.env.ADMOB_IOS_APP_ID,
admobAndroidId: process.env.ADMOB_ANDROID_APP_ID,
trackingMessage: process.env.ATT_MESSAGE,
},
],
],
},
};
こうしておくと、SKAdNetwork のリストが変わったときに直すのはパッケージの1ファイルだけです。各アプリは依存のバージョンを上げて prebuild するだけで、6本すべてに同じ変更が反映されます。アプリ固有の値(App ID やローカライズ文言)は .env に閉じ込めるので、共有コードとアプリ固有設定がきれいに分かれます。
Android 側も同じプラグインで賄う
iOS だけでなく Android の AndroidManifest.xml も同じ思想で扱えます。AdMob の App ID は manifest の <meta-data> に入れる必要があります。withAndroidManifest を使って、同じプラグイン関数の中で両 OS を面倒みます。
// plugins/withAdConfig.js(Android 部分を追加)
const {
withInfoPlist,
withAndroidManifest,
AndroidConfig,
} = require("@expo/config-plugins");
function withAndroidAdConfig(config, props) {
return withAndroidManifest(config, (config) => {
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(
config.modResults
);
AndroidConfig.Manifest.addMetaDataItemToMainApplication(
app,
"com.google.android.gms.ads.APPLICATION_ID",
props.admobAndroidId
);
return config;
});
}
module.exports = function withAdConfig(config, props) {
config = withIosAdConfig(config, props); // 先ほどの iOS 部分を関数化したもの
config = withAndroidAdConfig(config, props);
return config;
};
AndroidConfig.Manifest のヘルパーを使うと、XML を文字列で力技編集せずに済みます。addMetaDataItemToMainApplication は同名キーを上書きしてくれるので、prebuild を何度走らせても <meta-data> が重複しません。この冪等性が後々とても効いてきます。
本番で踏んだ落とし穴と対処
プラグインが動き始めてからも、いくつか本番でつまずきました。記録として残しておきます。
第一に、プラグインは prebuild のときにしか走りません。EAS Update(OTA 更新)では native 設定は変わらないので、App ID を直したつもりでも、新しいバイナリをビルドして配布するまで反映されません。ここを勘違いして「直したのに反映されない」と何時間か悩みました。native に関わる変更は必ずビルドを伴う、と覚えておくのが対処です。
第二に、TypeScript で書いたプラグインはそのままでは読めません。app.config.ts 自体は読めますが、plugins 配列から参照するプラグイン本体は CommonJS の .js(または事前にコンパイル)である必要があります。私は素直に .js で書くことに落ち着きました。
第三に、同じファイルを複数の mod が触ると順番が問題になります。withInfoPlist を2つ登録すると、後勝ちで上書きされることがあります。1つの plist を触る処理は1つの mod にまとめるのが安全です。
第四に、withDangerousMod を使うときは冪等性を自分で保証する必要があります。ファイルに追記する処理を書くと、prebuild のたびに行が増えていきます。追記の前に「すでに同じ行があるか」を必ず確認してください。可能な限り withDangerousMod は避け、構造化された mod(withInfoPlist 等)を使うのが私の推奨です。
最後に、変更が効かないときは npx expo prebuild --clean で ios/ と android/ を作り直すと切り分けが楽です。古い生成物が残っていると、プラグインの変更が見えにくくなります。
次の一歩
まずは手元の Rork プロジェクトで、Info.plist に1キー足すだけの最小プラグインを書いて prebuild してみてください。生成された plist にその値が入っているのを目で確認できれば、ネイティブ設定を「ソースから再現する」感覚が手に入ります。そこまで来れば、AdMob でも ATT でも、手作業で消えていた設定を1つずつプラグインへ移していけます。
私自身、最初は「ネイティブはブラックボックス」と身構えていましたが、config plugin を通すと、宮大工の図面のように「同じ入力からは必ず同じ家が建つ」状態に近づきました。同じように複数アプリを抱えている方の、設定地獄を抜ける助けになれば嬉しいです。