取り組みの背景:個人アプリの運用で見えてきたCapacitorの実用範囲
2013年から個人でiOS/Androidアプリを作り続けてきました。壁紙アプリ・癒し系アプリ・引き寄せ系アプリと、ジャンルを横断してリリースしてきた中で、「Webアプリ資産をどう活かしてモバイル展開するか」は何度も悩まされてきたテーマです。Flutter で書き直すコスト、React Native のネイティブブリッジ起因のクラッシュ、そして AdMob・Firebase Crashlytics の SDK 統合の手間――いずれも個人開発の体力では重いものでした。
そこに Antigravity(Google の AI-first IDE)と Capacitor を組み合わせた構成が現れたとき、最初は半信半疑でした。WebView ベースで本当に App Store の審査を通せるのか、Crashlytics は本当に動くのか、AdMob のメディエーションは破綻しないのか。実際に既存の Next.js アプリを Capacitor で iOS/Android に展開してみると、想定よりも遥かに実用的でした。
実測でいくつかの数値を共有しておきます。Web 版で 4 ヶ月かけて作った瞑想ツールアプリを Capacitor で iOS/Android 化したケースでは、ネイティブ化の追加工数が 約 18 時間 (うち Antigravity が生成したコードに対する手直しが 5 時間程度)。App Store / Google Play 双方で初回審査を通過しました。Crashlytics に上がってきた起動時クラッシュ率は 0.04% 、リジェクト理由は「Sign in with Apple の戻り先 URL 設定の誤り」のみで、本質的な WebView 起因の問題は出ていません。
一方で、Capacitor が向かないケースもはっきりしました。フレームレートが 60fps を要求されるカジュアルゲーム、リアルタイム動画フィルタ、3D レンダリングを多用する壁紙アプリ――こうしたものは素直に SwiftUI / Jetpack Compose で書く方が結果的に早く、品質も高くなります。
本ガイドは、Capacitor を選ぶべき領域・避けるべき領域を最初に切り分けた上で、Antigravity をペアプロ相手として使いながらネイティブ化を進める実装手順をまとめたものです。各章のコード例は、実際に運用しているアプリと同じ AdMob / Crashlytics / Firebase Analytics 構成の環境で動作確認しています。読者の対象は次の通りです。
既に動く Web アプリがあり、ネイティブ化の最小コストを探っている個人開発者・スタートアップ
React/Vue で書かれた管理画面や社内ツールを社員向けモバイルアプリにしたいエンジニア
iOS/Android ネイティブ未経験で、App Store / Google Play 申請まで一気通貫で抜けたい方
Antigravity を日常開発に取り入れており、モバイル領域への適用範囲を広げたい方
第1章:Capacitorの基礎とAntigravityとの相性
Capacitorとは何か
Capacitor は Ionic チームが開発したオープンソースのクロスプラットフォームランタイムです。Web アプリ(HTML/CSS/JS)を WebView でラップし、カメラ・GPS・プッシュ通知などのネイティブ機能を JavaScript から呼び出せるようにするブリッジ層を提供します。
Cordova の後継として設計されており、以下の特徴があります。
標準的な Web API に準拠 :Cordova の独自プラグイン形式ではなく、Web 標準に近い API 設計
ネイティブプロジェクトをそのまま管理 :iOS(Xcode プロジェクト)と Android(Gradle プロジェクト)を直接編集可能
既存プロジェクトに後付け可能 :Next.js・Nuxt・SvelteKit などどんな Web フレームワークとも組み合わせられる
AntigravityとCapacitorの相性が良い理由
Antigravity の強みは、大量の API ドキュメントとコード例をコンテキストに持ち込んで、正確なコードを生成できることです。Capacitor のプラグインリストは公式に整備されており、Antigravity はその型定義を参照しながらコードを生成します。
具体的には以下のような支援が期待できます。
プラグイン呼び出しのボイラープレートコードを自動生成
iOS/Android 両プラットフォームの条件分岐コードを正確に記述
Permission 要求のタイミングと UI 処理をベストプラクティスに従って実装
Swift/Kotlin のネイティブプラグイン実装をゼロから生成(カスタムプラグインが必要な場合)
第2章:プロジェクトセットアップ
前提環境の確認
まず、Antigravity に以下の確認をさせましょう。
Antigravity プロンプト:
「私の環境でCapacitorプロジェクトを始めるために必要なツールを確認して、
不足しているものをインストールしてください。
Node.js、CocoaPods、Android Studio、Xcode CLIが対象です。」
必要なツールは以下の通りです。
Node.js 20.x 以上
Xcode 16 以上(iOS ビルド用、macOS のみ)
Android Studio Meerkat 以上
CocoaPods(iOS の依存関係管理)
# Antigravityが自動実行するセットアップコマンド例
node -v # v20以上を確認
pod --version # 1.15以上を確認
xcodebuild -version # Xcode 16以上を確認
# CocoaPodsがない場合
sudo gem install cocoapods
既存WebアプリにCapacitorを追加する
Next.js アプリを例にして進めます。
# プロジェクトルートで実行
npm install @capacitor/core @capacitor/cli
npx cap init
# 初期化時の入力
# App Name: MyApp
# App ID: com.yourcompany.myapp(逆ドメイン形式)
# Web Dir: out(Next.jsの静的出力ディレクトリ)
Antigravity に capacitor.config.ts の設定を依頼します。
// capacitor.config.ts
// AntigravityがベストプラクティスのWebViewSetting等を自動追加
import type { CapacitorConfig } from '@capacitor/cli' ;
const config : CapacitorConfig = {
appId: 'com.yourcompany.myapp' ,
appName: 'MyApp' ,
webDir: 'out' ,
server: {
// 開発時はローカルサーバーを使用(本番では削除)
// url: 'http://192.168.1.x:3000',
// cleartext: true,
},
plugins: {
SplashScreen: {
launchShowDuration: 2000 ,
launchAutoHide: true ,
backgroundColor: '#ffffff' ,
androidSplashResourceName: 'splash' ,
showSpinner: false ,
},
PushNotifications: {
presentationOptions: [ 'badge' , 'sound' , 'alert' ],
},
},
};
export default config;
iOSとAndroidプラットフォームの追加
# iOS/Androidプラットフォームを追加
npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
# プロジェクトをビルドしてネイティブに同期
npm run build # Next.jsのstatic export
npx cap sync # ネイティブプロジェクトを最新状態に更新
Antigravity へのプロンプト:
「capacitor syncを実行したら以下のエラーが出ました:[エラー内容をペースト]
このエラーの原因と修正方法を教えてください。
また、修正後に再び発生しないよう予防策も実装してください。」
第3章:主要ネイティブAPIの実装
カメラプラグインの実装
カメラは最も頻繁に使われるネイティブ機能のひとつです。Antigravity に実装を依頼する際は、権限要求・エラー処理・プレビュー表示のすべてを含めるよう指示します。
npm install @capacitor/camera
npx cap sync
// src/hooks/useCamera.ts
// Antigravityが型安全かつ権限処理を含む実装を生成
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera' ;
import { useState, useCallback } from 'react' ;
interface UseCameraReturn {
photo : Photo | null ;
takePhoto : () => Promise < void >;
pickFromGallery : () => Promise < void >;
error : string | null ;
isLoading : boolean ;
}
export const useCamera = () : UseCameraReturn => {
const [ photo , setPhoto ] = useState < Photo | null >( null );
const [ error , setError ] = useState < string | null >( null );
const [ isLoading , setIsLoading ] = useState ( false );
const requestPermissions = async () : Promise < boolean > => {
const permissions = await Camera. requestPermissions ({
permissions: [ 'camera' , 'photos' ],
});
return (
permissions.camera === 'granted' &&
permissions.photos === 'granted'
);
};
const takePhoto = useCallback ( async () => {
setIsLoading ( true );
setError ( null );
try {
const granted = await requestPermissions ();
if ( ! granted) {
setError ( 'カメラへのアクセス許可が必要です。設定から許可してください。' );
return ;
}
const result = await Camera. getPhoto ({
quality: 85 , // 画質(0-100)
allowEditing: false ,
resultType: CameraResultType.DataUrl, // base64 DataURL形式で取得
source: CameraSource.Camera,
width: 1024 ,
height: 1024 ,
correctOrientation: true , // 自動回転補正
});
setPhoto (result);
} catch (err) {
// ユーザーがキャンセルした場合はエラーとして扱わない
if (err instanceof Error && err.message. includes ( 'cancelled' )) {
return ;
}
setError ( '写真の取得中にエラーが発生しました。' );
console. error ( 'Camera error:' , err);
} finally {
setIsLoading ( false );
}
}, []);
const pickFromGallery = useCallback ( async () => {
setIsLoading ( true );
setError ( null );
try {
const result = await Camera. getPhoto ({
quality: 85 ,
allowEditing: false ,
resultType: CameraResultType.DataUrl,
source: CameraSource.Photos, // ギャラリーから選択
width: 1024 ,
height: 1024 ,
});
setPhoto (result);
} catch (err) {
if (err instanceof Error && err.message. includes ( 'cancelled' )) {
return ;
}
setError ( 'ギャラリーへのアクセス中にエラーが発生しました。' );
} finally {
setIsLoading ( false );
}
}, []);
return { photo, takePhoto, pickFromGallery, error, isLoading };
};
iOS では Info.plist に権限説明文を追加する必要があります。Antigravity に依頼します。
<!-- ios/App/App/Info.plist に追加 -->
<!-- Antigravityがこの設定を自動検出して追加を提案してくれる -->
< key >NSCameraUsageDescription</ key >
< string >プロフィール写真の撮影や商品画像のアップロードに使用します</ string >
< key >NSPhotoLibraryUsageDescription</ key >
< string >ギャラリーから画像を選択するために使用します</ string >
< key >NSPhotoLibraryAddUsageDescription</ key >
< string >撮影した写真をカメラロールに保存するために使用します</ string >
Android では AndroidManifest.xml に権限を追加します。
<!-- android/app/src/main/AndroidManifest.xml -->
< uses-permission android:name = "android.permission.CAMERA" />
< uses-permission android:name = "android.permission.READ_MEDIA_IMAGES" />
< uses-feature android:name = "android.hardware.camera" android:required = "false" />
プッシュ通知の実装
プッシュ通知はアプリのリテンション率を大きく左右する機能です。Firebase Cloud Messaging(FCM)と Apple Push Notification service(APNs)の両方に対応する実装を Antigravity で生成します。
npm install @capacitor/push-notifications
npx cap sync
// src/services/pushNotifications.ts
import { PushNotifications, PushNotificationSchema, ActionPerformed } from '@capacitor/push-notifications' ;
import { Capacitor } from '@capacitor/core' ;
export class PushNotificationService {
private static instance : PushNotificationService ;
private token : string | null = null ;
private constructor () {}
static getInstance () : PushNotificationService {
if ( ! PushNotificationService.instance) {
PushNotificationService.instance = new PushNotificationService ();
}
return PushNotificationService.instance;
}
async initialize ( onTokenReceived : ( token : string ) => void ) : Promise < void > {
// ネイティブ環境でのみ実行(Web環境はスキップ)
if ( ! Capacitor. isNativePlatform ()) {
console. log ( 'プッシュ通知はネイティブ環境でのみ利用可能です' );
return ;
}
// 通知受信時のリスナー登録
PushNotifications. addListener ( 'registration' , ( token ) => {
this .token = token.value;
console. log ( 'Push registration token: ' , token.value);
onTokenReceived (token.value);
});
PushNotifications. addListener ( 'registrationError' , ( error ) => {
console. error ( 'Push registration error: ' , error.error);
});
// フォアグラウンドでの通知受信
PushNotifications. addListener ( 'pushNotificationReceived' , ( notification : PushNotificationSchema ) => {
console. log ( 'Push notification received: ' , notification);
// アプリ内通知UIを表示する処理を追加
this . handleForegroundNotification (notification);
});
// 通知タップ時のアクション
PushNotifications. addListener ( 'pushNotificationActionPerformed' , ( action : ActionPerformed ) => {
console. log ( 'Push action performed: ' , action);
this . handleNotificationAction (action);
});
// 権限要求と登録
await this . requestPermissionAndRegister ();
}
private async requestPermissionAndRegister () : Promise < void > {
let permStatus = await PushNotifications. checkPermissions ();
if (permStatus.receive === 'prompt' ) {
permStatus = await PushNotifications. requestPermissions ();
}
if (permStatus.receive !== 'granted' ) {
console. warn ( '通知の権限が拒否されました' );
return ;
}
await PushNotifications. register ();
}
private handleForegroundNotification ( notification : PushNotificationSchema ) : void {
// フォアグラウンド通知の処理(カスタムUIを表示など)
const event = new CustomEvent ( 'push-notification-received' , {
detail: notification,
});
window. dispatchEvent (event);
}
private handleNotificationAction ( action : ActionPerformed ) : void {
// 通知のディープリンク処理
const data = action.notification.data;
if (data?.route) {
window.location.href = data.route;
}
}
getToken () : string | null {
return this .token;
}
async removeAllListeners () : Promise < void > {
await PushNotifications. removeAllListeners ();
}
}
// 使用例
// const pushService = PushNotificationService.getInstance();
// await pushService.initialize(async (token) => {
// // サーバーにトークンを登録
// await registerDeviceToken(token, userId);
// });
生体認証(Face ID / 指紋認証)の実装
セキュリティが求められるアプリには必須の機能です。
npm install @aparajita/capacitor-biometric-auth
npx cap sync
// src/hooks/useBiometricAuth.ts
import { BiometricAuth, BiometryType } from '@aparajita/capacitor-biometric-auth' ;
import { useState, useEffect } from 'react' ;
interface BiometricAuthState {
isAvailable : boolean ;
biometryType : string ;
authenticate : () => Promise < boolean >;
}
export const useBiometricAuth = () : BiometricAuthState => {
const [ isAvailable , setIsAvailable ] = useState ( false );
const [ biometryType , setBiometryType ] = useState ( '' );
useEffect (() => {
checkBiometryAvailability ();
}, []);
const checkBiometryAvailability = async () => {
try {
const result = await BiometricAuth. checkBiometry ();
setIsAvailable (result.isAvailable);
// 利用可能な生体認証のタイプを取得
if (result.biometryType === BiometryType.faceId) {
setBiometryType ( 'Face ID' );
} else if (result.biometryType === BiometryType.touchId) {
setBiometryType ( 'Touch ID' );
} else if (result.biometryType === BiometryType.fingerprint) {
setBiometryType ( '指紋認証' );
} else if (result.biometryType === BiometryType.faceAuthentication) {
setBiometryType ( '顔認証' );
}
} catch {
setIsAvailable ( false );
}
};
const authenticate = async () : Promise < boolean > => {
try {
await BiometricAuth. authenticate ({
reason: 'アカウントにアクセスするために認証が必要です' ,
iosFallbackTitle: 'パスコードを使用' ,
androidTitle: '生体認証' ,
androidSubtitle: 'アカウントへのアクセスを確認' ,
androidConfirmationRequired: false ,
});
return true ;
} catch (error) {
console. error ( 'Biometric authentication failed:' , error);
return false ;
}
};
return { isAvailable, biometryType, authenticate };
};
第4章:ストレージとデータ永続化
Capacitor Preferencesによるキー・バリューストレージ
npm install @capacitor/preferences
npx cap sync
Antigravity に型安全なラッパーの生成を依頼します。
// src/lib/storage.ts
// AntigravityがTypeScriptのGenericsを活用した型安全な実装を生成
import { Preferences } from '@capacitor/preferences' ;
export class TypedStorage {
static async set < T >( key : string , value : T ) : Promise < void > {
await Preferences. set ({
key,
value: JSON . stringify (value),
});
}
static async get < T >( key : string ) : Promise < T | null > {
const { value } = await Preferences. get ({ key });
if (value === null ) return null ;
try {
return JSON . parse (value) as T ;
} catch {
return null ;
}
}
static async remove ( key : string ) : Promise < void > {
await Preferences. remove ({ key });
}
static async clear () : Promise < void > {
await Preferences. clear ();
}
static async keys () : Promise < string []> {
const { keys } = await Preferences. keys ();
return keys;
}
}
// 使用例
interface UserSettings {
theme : 'light' | 'dark' ;
language : 'ja' | 'en' ;
notifications : boolean ;
}
// 保存
await TypedStorage. set < UserSettings >( 'userSettings' , {
theme: 'dark' ,
language: 'ja' ,
notifications: true ,
});
// 取得
const settings = await TypedStorage. get < UserSettings >( 'userSettings' );
// settings?.theme === 'dark'
SQLiteプラグインによる構造化データ管理
大量データや複雑なクエリが必要な場合は SQLite を使います。
npm install @capacitor-community/sqlite
npx cap sync
// src/lib/database.ts
import { CapacitorSQLite, SQLiteConnection, SQLiteDBConnection } from '@capacitor-community/sqlite' ;
import { Capacitor } from '@capacitor/core' ;
class DatabaseService {
private sqlite : SQLiteConnection ;
private db : SQLiteDBConnection | null = null ;
constructor () {
this .sqlite = new SQLiteConnection (CapacitorSQLite);
}
async initialize () : Promise < void > {
// Web環境ではIndexedDBを使用するためのポリフィルが必要
if (Capacitor. getPlatform () === 'web' ) {
await import ( '@capacitor-community/sqlite/electron/dist/plugin.js' );
}
this .db = await this .sqlite. createConnection (
'myapp.db' , // データベース名
false , // 暗号化なし
'no-encryption' , // 暗号化モード
1 , // バージョン
false // 読み取り専用でない
);
await this .db. open ();
await this . createTables ();
}
private async createTables () : Promise < void > {
if ( ! this .db) throw new Error ( 'Database not initialized' );
const createUsersTable = `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
` ;
const createPostsTable = `
CREATE TABLE IF NOT EXISTS posts (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT,
created_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
` ;
await this .db. execute (createUsersTable);
await this .db. execute (createPostsTable);
}
async insertUser ( user : { id : string ; name : string ; email : string }) : Promise < void > {
if ( ! this .db) throw new Error ( 'Database not initialized' );
const now = Date. now ();
const query = `
INSERT OR REPLACE INTO users (id, name, email, created_at, updated_at)
VALUES (?, ?, ?, ?, ?);
` ;
await this .db. run (query, [user.id, user.name, user.email, now, now]);
}
async getUserById ( id : string ) : Promise <{ id : string ; name : string ; email : string } | null > {
if ( ! this .db) throw new Error ( 'Database not initialized' );
const query = 'SELECT * FROM users WHERE id = ?;' ;
const result = await this .db. query (query, [id]);
if (result.values && result.values. length > 0 ) {
return result.values[ 0 ] as { id : string ; name : string ; email : string };
}
return null ;
}
async close () : Promise < void > {
if ( this .db) {
await this .sqlite. closeConnection ( 'myapp.db' , false );
this .db = null ;
}
}
}
export const database = new DatabaseService ();
第5章:パフォーマンス最適化とデバッグ
WebViewのパフォーマンス改善
ハイブリッドアプリの最大の弱点はネイティブアプリと比較したパフォーマンスです。Antigravity に最適化を依頼します。
// capacitor.config.ts に追加する最適化設定
// AntigravityがiOS/Android別の最適な設定を提案
const config : CapacitorConfig = {
// ...基本設定...
ios: {
// WKWebViewの設定
contentInset: 'automatic' ,
scrollEnabled: true ,
backgroundColor: '#ffffff' ,
preferredContentMode: 'mobile' ,
limitsNavigationsToAppBoundDomains: true ,
// ハードウェアアクセラレーションを有効化
webContentsDebuggingEnabled: false , // 本番はfalse
},
android: {
// Android WebViewの設定
backgroundColor: '#ffffff' ,
useLegacyBridge: false ,
// Android 9以降のDarkMode対応
allowMixedContent: false ,
captureInput: true ,
webContentsDebuggingEnabled: false , // 本番はfalse
},
};
デバッグ方法
Antigravity と組み合わせたデバッグワークフローは以下の通りです。
iOS デバッグ (Safari を使用):
iOS Simulator でアプリを実行
Safari → 開発 → Simulator → アプリの WebView を選択
Web Inspector でコンソール・ネットワーク・パフォーマンスを確認
Android デバッグ (Chrome を使用):
Android Emulator でアプリを実行
Chrome で chrome://inspect を開く
対象デバイスの WebView をクリックしてデバッグ開始
Antigravity でのデバッグプロンプト例:
「Androidエミュレータでchromeのinspectを使ったところ、以下のエラーが
コンソールに出ています:[エラーログをペースト]
このエラーの原因と修正コードを教えてください。」
第6章:App Store / Google Play リリース準備
App Store 申請の準備(iOS)
iOS リリースには以下の準備が必要です。Antigravity に Xcode の設定確認と Info.plist の最終チェックを依頼します。
必須設定のチェックリスト:
アプリアイコン(1024x1024 PNG)の設定
スプラッシュスクリーン(各解像度)の設定
Bundle Identifier の設定(com.yourcompany.appname)
Provisioning Profile と Signing Certificate の設定
App Privacy マニフェストの追加(iOS 17 以降必須)
# Xcodeでアーカイブを作成
cd ios/App
xcodebuild -workspace App.xcworkspace \
-scheme App \
-configuration Release \
-archivePath App.xcarchive \
archive
# IPA を作成(App Store Connect 用)
xcodebuild -exportArchive \
-archivePath App.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath ./output
Antigravity に ExportOptions.plist の生成を依頼します。
<!-- ExportOptions.plist -->
<? xml version = "1.0" encoding = "UTF-8" ?>
<! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
< plist version = "1.0" >
< dict >
< key >method</ key >
< string >app-store</ string >
< key >teamID</ key >
< string >YOUR_TEAM_ID</ string >
< key >uploadSymbols</ key >
< true />
< key >compileBitcode</ key >
< false />
</ dict >
</ plist >
Google Play 申請の準備(Android)
# Gradle でリリースビルドを作成
cd android
./gradlew bundleRelease # AAB形式(推奨)
# または
./gradlew assembleRelease # APK形式
署名の設定を Antigravity に生成してもらいます。
// android/app/build.gradle に追加
// Antigravityがセキュリティベストプラクティスに従った設定を生成
android {
signingConfigs {
release {
// キーストアはバージョン管理に含めない
// 環境変数または local.properties から読み込む
storeFile file( System. getenv( 'KEYSTORE_PATH' ) ?: '../keystore/release.keystore' )
storePassword System. getenv( 'KEYSTORE_PASSWORD' ) ?: ''
keyAlias System. getenv( 'KEY_ALIAS' ) ?: 'release'
keyPassword System. getenv( 'KEY_PASSWORD' ) ?: ''
}
}
buildTypes {
release {
signingConfig signingConfigs . release
minifyEnabled true // コードの難読化
proguardFiles getDefaultProguardFile( 'proguard-android-optimize.txt' ), 'proguard-rules.pro'
shrinkResources true // 未使用リソースの削除
}
}
}
審査通過のためのAntigravityによるコードレビュー
提出前に Antigravity に審査基準に沿ったレビューを依頼します。
Antigravity プロンプト:
「このCapacitorアプリをApp Store審査に提出する前に、
以下の観点でコードレビューを行ってください:
1. プライバシーポリシーと実際の権限要求の整合性
2. App Tracking Transparency (ATT) の実装が必要な箇所
3. iOS 17のApp Privacy Manifestで申告が必要なAPIの使用
4. クラッシュリスクの高いコード
5. Appleのヒューマンインターフェースガイドラインへの準拠
重大度を高・中・低に分類して報告してください。」
第7章:継続的デリバリーの自動化
GitHub ActionsによるCI/CDパイプライン
Antigravity に完全な GitHub Actions ワークフローを生成してもらいます。
# .github/workflows/mobile-release.yml
name : Mobile Release Pipeline
on :
push :
tags :
- 'v*.*.*'
jobs :
build-ios :
runs-on : macos-latest
steps :
- uses : actions/checkout@v4
- name : Setup Node.js
uses : actions/setup-node@v4
with :
node-version : '20'
cache : 'npm'
- name : Install dependencies
run : npm ci
- name : Build web app
run : npm run build
- name : Sync Capacitor
run : npx cap sync ios
- name : Setup Ruby & CocoaPods
uses : ruby/setup-ruby@v1
with :
ruby-version : '3.2'
bundler-cache : true
- name : Install CocoaPods
run : cd ios/App && pod install
- name : Import Certificates
env :
P12_CERTIFICATE : ${{ secrets.P12_CERTIFICATE_BASE64 }}
P12_PASSWORD : ${{ secrets.P12_PASSWORD }}
run : |
echo "$P12_CERTIFICATE" | base64 --decode > certificate.p12
security create-keychain -p "" build.keychain
security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
security set-keychain-settings -t 3600 -u build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
- name : Build & Archive iOS
run : |
cd ios/App
xcodebuild -workspace App.xcworkspace \
-scheme App \
-configuration Release \
-archivePath $GITHUB_WORKSPACE/App.xcarchive \
archive
- name : Export IPA
run : |
xcodebuild -exportArchive \
-archivePath $GITHUB_WORKSPACE/App.xcarchive \
-exportOptionsPlist $GITHUB_WORKSPACE/ExportOptions.plist \
-exportPath $GITHUB_WORKSPACE/output
- name : Upload to App Store Connect
env :
APPLE_ID : ${{ secrets.APPLE_ID }}
APPLE_APP_PASSWORD : ${{ secrets.APPLE_APP_PASSWORD }}
run : |
xcrun altool --upload-app \
-f output/App.ipa \
-t ios \
-u "$APPLE_ID" \
-p "$APPLE_APP_PASSWORD"
build-android :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Setup Node.js
uses : actions/setup-node@v4
with :
node-version : '20'
cache : 'npm'
- name : Setup Java
uses : actions/setup-java@v4
with :
distribution : 'temurin'
java-version : '17'
- name : Install dependencies
run : npm ci
- name : Build web app
run : npm run build
- name : Sync Capacitor
run : npx cap sync android
- name : Build Release AAB
env :
KEYSTORE_PATH : ${{ github.workspace }}/release.keystore
KEYSTORE_PASSWORD : ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS : ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD : ${{ secrets.KEY_PASSWORD }}
run : |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > release.keystore
cd android
./gradlew bundleRelease
- name : Upload to Google Play
uses : r0adkll/upload-google-play@v1
with :
serviceAccountJsonPlainText : ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName : com.yourcompany.myapp
releaseFiles : android/app/build/outputs/bundle/release/*.aab
track : internal # internal → alpha → beta → production
公式ドキュメントに書かれていない、運用で気づいた実装ポイント
ここからは、Capacitor 公式ドキュメントには載っていないものの、自分のアプリを運用する中で実際に踏んだ落とし穴とその回避策を、優先度順に共有します。Antigravity でコードを生成した後にチェックすべき「最後の 10%」がここに詰まっています。
1. WebView 起動直後の白画面を防ぐ Splash Screen 制御
Capacitor の @capacitor/splash-screen プラグインを「自動で隠す」設定のまま使うと、低速 Android 端末(特に RAM 2GB 以下のエントリーモデル)で WebView の初期化完了前に Splash が消え、約 300〜800ms の白画面 が発生します。実機での体感はかなり悪く、レビュー欄で「起動が重い」と書かれる典型パターンです。
回避策は「Web 側で完全に描画準備が整ってから手動で Splash を隠す」設計に変えること。capacitor.config.ts で launchAutoHide: false を設定し、React のルートコンポーネント(または Next.js の _app.tsx)で useEffect 内から SplashScreen.hide() を呼びます。
// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli' ;
const config : CapacitorConfig = {
appId: 'com.yourcompany.myapp' ,
appName: 'MyApp' ,
webDir: 'out' ,
plugins: {
SplashScreen: {
launchAutoHide: false , // 自動で隠さない
backgroundColor: '#0b1020' , // Web側の背景色と完全一致させる
androidScaleType: 'CENTER_CROP' ,
},
},
};
export default config;
// src/app/layout.tsx もしくは _app.tsx
import { Capacitor } from '@capacitor/core' ;
import { SplashScreen } from '@capacitor/splash-screen' ;
import { useEffect } from 'react' ;
export default function RootLayout ({ children } : { children : React . ReactNode }) {
useEffect (() => {
if ( ! Capacitor. isNativePlatform ()) return ;
// フォント・画像・初期API取得が終わってから呼ぶのが重要
const t = setTimeout (() => { SplashScreen. hide ({ fadeOutDuration: 200 }); }, 50 );
return () => clearTimeout (t);
}, []);
return <>{children} </> ;
}
実測では、この変更だけで Android Galaxy A05(最低スペックの主力機)における起動時の白画面が消え、Firebase Performance Monitoring の app_start メトリクスが 平均 1.8 秒 → 0.9 秒 に短縮されました。
2. AdMob プラグインを Capacitor 環境で安定動作させる設定
@capacitor-community/admob を素直に入れただけだと、iOS の Sign in with Apple や In-App Purchase と組み合わせた際に、ATT(App Tracking Transparency)プロンプトの呼び出し順序で広告 SDK が初期化失敗するケースがあります。具体的には、ATT 許可前に AdMob.initialize() を呼ぶと iOS 14+ で IDFA が取れず、eCPM が 約 35〜40% 落ちます。
正しい初期化順序は次の通り。Antigravity に依頼するときは、この順序を明示しないと生成コードが ATT を後追いにしてしまうことが多いです。
import { AdMob } from '@capacitor-community/admob' ;
import { AppTrackingTransparency } from 'capacitor-plugin-app-tracking-transparency' ;
export async function initAds () {
// 1) ATTプロンプトを先に出す(iOS 14.5+のみ実際にダイアログ表示)
const att = await AppTrackingTransparency. requestPermission ();
// 2) ステータスがauthorizedならパーソナライズ広告、それ以外は非パーソナライズ
const npa = att.status === 'authorized' ? '0' : '1' ;
// 3) ATT結果を踏まえてからAdMobを初期化
await AdMob. initialize ({
requestTrackingAuthorization: false , // 自前で先に呼んだのでfalse
testingDevices: [],
initializeForTesting: false ,
});
// 4) 広告リクエスト時にnpaパラメータを付ける
await AdMob.bannerAd. show ({
adId: 'ca-app-pub-XXXXXXXXXXXXXXXX/YYYYYYYYYY' ,
adSize: 'ADAPTIVE_BANNER' ,
position: 'BOTTOM_CENTER' ,
margin: 0 ,
npa,
} as any );
}
実運用での効果:自分が運用している壁紙系アプリで同じ初期化順序に切り替えたところ、iOS の eCPM が $1.42 → $2.11(+48%) に回復しました。
3. Crashlytics と Web エラーを橋渡しする最小実装
Capacitor 環境では Crashlytics に上がってくるのはネイティブクラッシュのみで、WebView 内で発生した JavaScript エラーは原則記録されません。これを放置すると「Crashlytics には何も上がってないのにユーザーは落ちたと言っている」現象が頻発します。
回避には Web 側で window.onerror と unhandledrejection を捕捉し、@capacitor-community/firebase-crashlytics の recordException に流す薄いブリッジを置きます。Antigravity に投げると 30 秒で生成しますが、コードは以下の形に整えてください。
import { FirebaseCrashlytics } from '@capacitor-community/firebase-crashlytics' ;
import { Capacitor } from '@capacitor/core' ;
export function installWebErrorBridge () {
if ( ! Capacitor. isNativePlatform ()) return ;
const send = ( message : string , stacktrace ?: string ) => {
FirebaseCrashlytics. recordException ({ message, stacktrace }). catch (() => {});
};
window. addEventListener ( 'error' , ( e ) => {
send ( `[JS Error] ${ e . message }` , e.error?.stack);
});
window. addEventListener ( 'unhandledrejection' , ( e ) => {
const r = e.reason;
send ( `[Unhandled Promise] ${ r ?. message ?? String ( r ) }` , r?.stack);
});
}
実測では、これを入れたことで「不可視のJavaScriptエラー」が 週 18 件 → 0 件 になり、レビュー欄に書かれていた「特定の画面で固まる」報告が 2 週間で消えました。
4. iOS の WKWebView で起きる Cookie・localStorage 消失への対策
iOS の WKWebView は OS のメモリ圧縮時に Cookie や localStorage を予告なくクリアすることがあります。ログイン状態が「数日に一度なぜか飛ぶ」典型原因です。
対策は次の 2 段構え。
認証トークンは @capacitor/preferences に保存し、起動時に localStorage に書き戻す
セッション Cookie に依存している場合は WebView の limitsNavigationsToAppBoundDomains を false に保ち、WKHTTPCookieStore の永続化を有効にする
import { Preferences } from '@capacitor/preferences' ;
const KEY = 'auth.idToken' ;
export async function persistToken ( token : string ) {
await Preferences. set ({ key: KEY , value: token });
localStorage. setItem ( KEY , token); // 同一セッション内のWeb側互換
}
export async function restoreTokenOnBoot () {
const { value } = await Preferences. get ({ key: KEY });
if (value && ! localStorage. getItem ( KEY )) localStorage. setItem ( KEY , value);
}
運用中の引き寄せ系アプリで導入後、「再ログインを求められる頻度」のサポート問い合わせが 月 24 件 → 月 3 件 に減りました。
5. App Store / Google Play の審査でハマりやすい3点の事前チェック
Antigravity が生成したコードはほぼそのまま動きますが、ストア審査の規約面までは把握していません。実際にリジェクトされた経験から、push 前に必ず確認すべき項目を 3 つに絞ります。
Sign in with Apple の戻り先 URL :Capacitor の Universal Links 設定で apple-app-site-association を配置し忘れるとリジェクト。https://yourdomain.com/.well-known/apple-app-site-association を Content-Type: application/json で配信
プライバシーマニフェスト(iOS 17+) :PrivacyInfo.xcprivacy に Capacitor の使うトラッキング API(NSPrivacyAccessedAPICategoryUserDefaults 等)を宣言。未宣言だと 2024 年以降は審査で警告 → リジェクトに進化
Google Play のデータ削除リンク :ユーザーがアプリ内とアプリ外の両方からアカウント削除できる必要があります。Capacitor の WebView 内に「アカウント削除」画面を実装し、その URL を Play Console の「データセーフティ」に登録
これら 3 つを事前にチェックしておくと、初回審査の通過率が体感で 30% → 90% 以上 に上がります。
6. リリース前チェックリスト(実運用版)
最後に、ストア提出前に必ず通すチェックリストを置いておきます。Antigravity に「このリポジトリを Capacitor のリリース基準で監査して」と投げる前の人間側チェック項目です。
[ ] capacitor.config.ts の appId が App Store Connect / Play Console の Bundle ID と完全一致
[ ] webDir の生成物に index.html が存在し、next.config.ts で output: 'export' 済み
[ ] iOS: Info.plist に NSCameraUsageDescription 等の利用理由文を全プラグイン分記述
[ ] iOS: PrivacyInfo.xcprivacy を ios/App/App/ に配置
[ ] Android: AndroidManifest.xml で INTERNET 以外の権限が必要最小限に絞られている
[ ] AdMob: テスト広告 ID(ca-app-pub-3940256099942544/...)が本番ビルドに残っていない
[ ] Crashlytics: シンボル(dSYM)がビルド時に Firebase に自動アップロード設定済み
[ ] Splash Screen: launchAutoHide: false で手動制御に統一
[ ] Live Reload: server.url を本番ビルドで設定していない(残っているとセキュリティリスク)
[ ] App Store Sign in with Apple 経路で 実機テスト (シミュレータでは検出しにくい不具合が出る領域)
このリストは私自身が App Store / Google Play 申請を 50 本以上通してきた中で「これだけは漏らすな」と痛感した項目だけを残したものです。Antigravity に補助してもらいながらも、最終確認は人間の目で通すと、リジェクトによる 1〜2 週間のロスを大幅に減らせます。
まとめ
Capacitor と Antigravity を組み合わせることで、Web 開発の知識だけを持つエンジニアでも iOS/Android ネイティブアプリを高品質にリリースできる時代になりましました。
本記事で解説した内容を振り返ります。
Capacitor の基本概念と Next.js への統合方法
カメラ・プッシュ通知・生体認証などのネイティブ API 実装
Preferences と SQLite を使ったデータ永続化
WebView のパフォーマンス最適化とデバッグ手法
App Store・Google Play 申請に向けたビルド設定
GitHub Actions による CI/CD の完全自動化
Antigravity の力を借りることで、各ステップでのコード生成・エラー解決・ベストプラクティス適用が大幅に効率化されます。複雑に見えるネイティブ設定も、「〇〇を実装してください」という自然言語の指示で大部分を自動化できます。