
はじめに
サブスク管理アプリ「SubTrack」(ksg-my-portfolio.com/subtrack)は買い切り・課金要素なしの無料アプリとして公開しているので、収益化手段としてバナー広告(Google AdMob)を組み込みました。実装自体は react-native-google-mobile-ads を導入するだけなので難しくはないのですが、ATT(App Tracking Transparency)対応と、アダプティブバナーが画面幅いっぱいに表示されないという地味な不具合でつまずいたので、その記録です。
実装: 初期化とバナー表示を分離する
広告関連のコードは「SDKの初期化」と「バナーの表示」で役割を分けています。
初期化は RootLayout のマウント時に一度だけ実行します。
// app/_layout.tsx
useEffect(() => {
initializeAds();
}, []);
// src/lib/adsInit.ts
export async function initializeAds(): Promise<void> {
if (Platform.OS !== "ios") {
return;
}
await requestTrackingPermissionsAsync();
await mobileAds().initialize();
}
ポイントは requestTrackingPermissionsAsync()(ATTのトラッキング許諾ダイアログ)を mobileAds().initialize() より先に await していることです。順番を逆にすると、ユーザーがダイアログに答える前に広告SDKが初期化されてしまい、パーソナライズ広告の許諾状態が正しく反映されないことがあります。地味に見落としがちなポイントだと思います。
現状Androidには対応していないので、Platform.OS !== "ios" で早期リターンして何もしないようにしています。
表示側のコンポーネントもシンプルです。
// src/components/AdBanner.tsx
export function AdBanner() {
if (Platform.OS !== "ios") {
return null;
}
return (
<BannerAd
unitId={getBannerAdUnitId()}
size={BannerAdSize.ANCHORED_ADAPTIVE_BANNER}
onAdFailedToLoad={() => {}}
/>
);
}
ANCHORED_ADAPTIVE_BANNER は画面幅に応じて高さが自動調整される、いわゆる「アダプティブバナー」のサイズ指定です。固定サイズのバナーより表示領域を無駄にしないので、これを選んでいます。
dev/prodで広告ユニットIDを出し分ける
開発中に本番の広告ユニットIDでバナーを表示し続けると、意図しないインプレッションやクリックが発生してAdMobのポリシー違反になりかねません。なので __DEV__ フラグでテスト用IDと本番IDを切り替えるだけの薄い関数を用意しています。
// src/lib/ads.ts
export const BANNER_AD_UNIT_ID_IOS = "ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx";
export const BANNER_AD_UNIT_ID_TEST = "ca-app-pub-3940256099942544/2934735716";
export function getBannerAdUnitId(): string {
return __DEV__ ? BANNER_AD_UNIT_ID_TEST : BANNER_AD_UNIT_ID_IOS;
}
BANNER_AD_UNIT_ID_TEST はGoogleが公式に配布しているテスト用IDです。ロジックとしては1行程度ですが、「開発中に本番IDを踏んでしまう」というありがちな事故を防ぐために、あえて関数として切り出してユニットテストも書いています。
// src/lib/__tests__/ads.test.ts
it("returns the test ad unit ID in development", () => {
__DEV__ = true;
expect(getBannerAdUnitId()).toBe(BANNER_AD_UNIT_ID_TEST);
});
it("returns the production ad unit ID outside development", () => {
__DEV__ = false;
expect(getBannerAdUnitId()).toBe(BANNER_AD_UNIT_ID_IOS);
});
__DEV__ の値を書き換えるだけの単純なテストですが、「本番ビルドで確実に本番IDが使われる」ことを保証できるので、広告まわりでは費用対効果の高いテストだと思っています。
ハマったところ: バナーが画面幅いっぱいに表示されない
実装自体はすんなり終わったのですが、リリース後に実機で見ると、アダプティブバナーの左端が16pxほど内側に入って見える(=画面幅いっぱいに表示されない)ことに気づきました。
原因は画面全体のコンテナに設定していた横パディングでした。
// 修正前
<View style={styles.container}>
<Text style={styles.total}>...</Text>
<FallingIconLayer ... />
<AdBanner />
</View>
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 16, // ← これがAdBannerにもかかっていた
paddingTop: 12
}
// ...
});
一覧コンテンツに余白を持たせるために container へ paddingHorizontal: 16 を設定していたのですが、AdBanner も同じ container の直下にあるため、このパディングがバナーの左右にもそのままかかってしまっていました。ANCHORED_ADAPTIVE_BANNER は「親Viewの幅」を基準に自身の幅を決めるので、パディング分だけ狭い横幅で計算され、結果として画面全体では中央に寄って見える(=左端に隙間ができる)という見た目になっていたわけです。
直し方は単純で、パディングをコンテンツ用の内側Viewに移し、AdBanner は元のパディングなしコンテナの直下に置くようにしただけです。
// 修正後
<View style={styles.container}>
<View style={styles.content}>
<Text style={styles.total}>...</Text>
<FallingIconLayer ... />
</View>
<AdBanner />
</View>
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 12
},
content: {
flex: 1,
paddingHorizontal: 16 // パディングはcontent側だけに
}
// ...
});
「広告バナーだけ別の余白ルールで扱う」という意識がないと踏みやすい罠だと思うので、他のアプリで広告を挿入するときも「バナーの親Viewにパディングやmarginがかかっていないか」は最初に確認するようにしています。
まとめ
- ATT許諾 → 広告SDK初期化、の順序を守る
- 広告ユニットIDのdev/prod切り替えはテストで担保しておくと事故防止になる
- アダプティブバナーは「親Viewの幅」に依存するので、パディング持ちのコンテナに直接置かない
広告の実装自体はSDKのドキュメント通りで難しくないものの、レイアウトまわりの落とし穴は実機で見ないと気づきにくいなと感じました。SubTrackは引き続き機能追加していく予定なので、よかったらアプリも見てみてください。


