engineering-mobile-app-builder について
プラットフォーム選定、UI構築、デバイス機能の連携を支援するモバイル開発ガイドです。ネイティブとクロスプラットフォームの手法に加え、オフライン同期、性能、セキュリティ、各OSの挙動に関する実践的な指針を扱います。
主な機能
- 01ネイティブとクロスプラットフォームの選定指針
- 02モバイルUIとナビゲーションのパターン
- 03オフライン優先のデータと同期の指針
- 04性能とバッテリーの確認
- 05プラットフォームAPIと権限の扱い
- 06プラットフォーム固有の実装上の注意
活用シーン
- 01モバイルアプリの開発方式の選定
- 02オフライン対応データフローの設計
- 03カメラ・生体認証・通知の連携
- 04起動・描画・バックグラウンド処理の問題調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ peterhdd/ agent-skills --skill engineering-mobile-app-builderインストール後、Claude Code で/engineering-mobile-app-builderを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/compose.mdflutter.mdnative-apis.mdoffline-first.mdperformance.mdreact-native.mdstate-management.mdswiftui.mdscripts/check_app_size.shcheck_permissions.py詳細
モバイル開発ガイド
概要
このガイドでは、オフラインファーストアーキテクチャ、プラットフォーム連携、パフォーマンス最適化のパターンを交えながら、ネイティブ iOS/Android 開発(SwiftUI、Jetpack Compose)とクロスプラットフォームフレームワーク(React Native、Flutter)について説明します。プラットフォーム戦略の選定、モバイル UI の構築、デバイス機能の統合を行う際にご利用ください。
プラットフォーム選定ガイド
- ウィジェット、拡張機能、AR、カスタムカメラパイプラインなど、プラットフォームとの深い連携が必要なアプリには、ネイティブ(SwiftUI/Compose)を使用してください。
- 小規模なチームで両方のプラットフォームにリリースし、アプリの主な機能がデータ表示とフォームである場合は、React Native または Flutterを使用してください。
- iOS では、
@Observable(iOS 17 以降)または@StateObject/@ObservedObject(iOS 15 以降)と組み合わせて SwiftUI を使用してください。対応していない機能に限り、UIKit にフォールバックしてください。 - Android では、DI に Hilt、リアクティブな状態管理に
StateFlowを使用し、Jetpack Compose を使ってください。新しい画面で XML レイアウトを使用するのは避けてください。 - ナビゲーションには、型付きルートを使用して
NavigationStack(iOS)または Navigation Compose(Android)を使ってください。
パフォーマンスと UX のルール
- ミドルレンジのデバイスで、コールドスタートを 2 秒未満にしてください。最初のフレームに不要な初期化は後回しにしてください。コールドスタートが 1.5 秒を超える場合は、Instruments(iOS)または
reportFullyDrawn()(Android)でプロファイリングし、負荷の高い処理をバックグラウンドに移してください。 - 現行機種から 2 世代前のデバイスでも、スクロール時に 60fps を維持してください。フレーム落ちが発生する場合は、iOS ではメインスレッド外での画像デコードを確認し、
prefetchDataSourceを使用してください。Android ではcompositionStrategy = ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyedを有効にし、Layout Inspector で不要な再コンポーズがないか確認してください。 - アニメーションには、SwiftUI の
.animation()/Compose のanimateFloatAsStateを使用し、時間は 200〜350ms にしてください。レイアウトのプロパティ(フレームサイズ、パディング)は絶対にアニメーションさせず、透明度とオフセットのみをアニメーションさせてください。 - オフラインファーストの設計では、ローカルデータベース(Core Data、Room、SQLite/Drift)を唯一の信頼できる情報源とし、サーバーとの同期はバックグラウンドで行ってください。関連を持つエンティティの種類が 3 つを超えるアプリでは、Core Data(iOS)または Room(Android)を使用し、生の SQLite は絶対に使用しないでください。
- Xcode Energy Diagnostics または Android Battery Historian でバッテリー消費をプロファイリングし、ユーザー操作なしで CPU を 2 秒以上稼働状態に保つ処理を修正してください。
- ネットワークリクエストはまとめて送信し、大容量の転送にはバックグラウンドセッション(URLSession/WorkManager)を使用してください。ペイロードが 1MB を超える場合はバックグラウンド転送を使用し、10MB を超える場合は再開可能なアップロードに対応してください。
- コレクション全体を取得するのではなく、タイムスタンプまたは変更トークンを使って増分同期してください。コレクションに 500 件を超える項目がある場合は、カーソルを使って同期をページ分割し、すべてを一度に取得するのは避けてください。
- キャッシュを使って画像を遅延読み込みし(iOS では Kingfisher/SDWebImage、Android では Coil)、表示サイズに縮小してください。リストに 20 枚を超える画像を表示する場合は、メモリキャッシュの上限(iOS は 50MB、Android は 100MB)とディスクキャッシュの上限(200MB)を設定してください。
- アプリのバイナリサイズは、iOS で 50MB 未満、APK で 30MB 未満(または Android では AAB を使用)を目標にしてください。上限を超える場合は、
scripts/check_app_size.shで監査し、未使用のアセットを削除して、アプリのシンニング(iOS)または動的機能モジュール(Android)を有効にしてください。 - ネットワークのタイムアウトは、API 呼び出しでは 10 秒、ファイルのアップロードでは 30 秒にしてください。リクエストが失敗した場合は、指数バックオフ(1 秒、2 秒、4 秒)で最大 3 回再試行してください。べき等でないリクエスト(冪等性キーのない POST)は絶対に再試行しないでください。
os_signpost(iOS)/Debug.getNativeHeapAllocatedSize()(Android)でメモリを監視してください。ミドルレンジのデバイスでメモリ使用量が 200MB を超える場合は、Instruments Allocations または Android Studio Memory Profiler で調査してください。リリース前に、保持サイクル(iOS)やリークした ViewModel(Android)を修正してください。
プラットフォーム統合ルール
- 生体認証:
LAContext(iOS)/BiometricPrompt(Android)を使用し、パスコードでのフォールバックを用意します。生体認証データそのものは保存しないでください。デバイスが生体認証に対応していない場合は、デバイスのパスコードにフォールバックします。認証を完全に省略してはいけません。認証トークンはKeychain(iOS)/AndroidKeystoreに保存し、setUserAuthenticationRequired(true)を設定します。 - カメラ:必要になった時点で権限をリクエストし、拒否された場合は設定画面へのディープリンクを案内します。権限が2回拒否された場合は、「設定を開く」ボタンを含む説明を画面内に表示します。システムダイアログで再度リクエストしてはいけません(iOSではブロックされます)。
- プッシュ通知:トピックベースのサブスクリプションを使用し、APNs(iOS)/FCM(Android)を利用します。通知が役立つ操作をユーザーが行った後にのみ、権限をリクエストします。初回起動時にリクエストしてはいけません。ユーザーが拒否した場合は設定を保存し、アプリ内の切り替えスイッチを表示します。再度リクエストするのは30日後以降にしてください。
- このスキルから、購入、請求、支払い、その他の資金移動のフローを実装または開始してはいけません。ユーザーがアプリ内購入を必要とする場合、案内は権限のモデル化、レシート検証のアーキテクチャ、監査要件、プラットフォームポリシーのレビューに限定してください。取引実行の詳細は、専用の決済レビューに委ねてください。
- ディープリンク:カスタムURLスキームではなく、Universal Links(iOS)/App Links(Android)を使用します。
apple-app-site-associationとassetlinks.jsonが、正しいコンテンツタイプでドメインから配信されていることを検証してください。リンクをクリックした後にアプリをインストールしたユーザーについて、遅延ディープリンクをテストしてください。
プラットフォーム固有のUI
- iOS:SF Symbols、システムフォント(San Francisco)、Dynamic Typeのサポートを使用します。カスタムフォントが必要な場合は、最大2ファミリーに限定してください。
xSmallからaccessibility5まで、すべてのDynamic Typeサイズをサポートします。固定フォントサイズは使用しないでください。 - Android:Material Design 3のトークンと、Composeの
MaterialThemeを使用します。すべての色にMaterialTheme.colorSchemeを使用し、16進数の値をハードコードしてはいけません。isSystemInDarkTheme()を使用して、最初からダークテーマをサポートします。 - タッチターゲット:最小サイズは44x44pt(iOS)/48x48dp(Android)です。インタラクティブ要素がこれより小さい場合は、
.contentShape()(SwiftUI)またはModifier.minimumInteractiveComponentSize()(Compose)を使ってタップ領域を広げます。 - 機密データ:Keychain(iOS)/EncryptedSharedPreferences(Android)を使用します。トークン、パスワード、個人を特定できる情報をUserDefaults/SharedPreferencesに保存してはいけません。アプリを再インストールしてもデータを保持する必要がある場合は、
kSecAttrAccessibleAfterFirstUnlockを指定してKeychainを使用します。 - 権限:システムのプロンプトを表示する前に説明し、拒否された場合も適切に処理します。アプリ全体を使用できない状態にしてはいけません。必要な権限が3つを超える場合は、機能を使用する際に段階的にリクエストします。起動時にまとめてリクエストしてはいけません。
コード例
NavigationStack、検索、ページネーション、プルして更新に対応した商品一覧の全例については、SwiftUI ガイドをご覧ください。
Hilt、StateFlow、LazyColumn、デバウンス検索に対応した商品一覧の全例については、Jetpack Compose ガイドをご覧ください。
FlatList、react-query による無限スクロール、プラットフォーム別のスタイル設定に対応した商品一覧の全例については、React Native ガイドをご覧ください。
Riverpod、ListView.builder、デバウンス検索、ページネーション、プルして更新に対応した商品一覧の全例については、Flutter ガイドをご覧ください。
オフラインキューシステム(NetInfo + AsyncStorage、Core Data + CloudKit、Room + WorkManager)、競合解決(LWW、フィールド単位のマージ、CRDT)、ロールバック対応の楽観的 UI については、オフラインファーストアーキテクチャをご覧ください。
Hermes エンジンの設定、FlatList の最適化(getItemLayout、windowSize)、SwiftUI の遅延スタックと画像キャッシュ、Coil による Compose の再コンポジション制御、バッテリーの最適化、メモリリークの防止、コールドスタートの最適化については、パフォーマンスのパターンをご覧ください。
MMKV 永続化を使用した Zustand、TanStack Query の楽観的ミューテーション、環境注入を使用した SwiftUI の @Observable、Compose の ViewModel + StateFlow + SavedStateHandle、ナビゲーションのディープリンク、認証状態のステートマシン、フォームの検証については、状態管理をご覧ください。
プッシュ通知(APNs、FCM、react-native-firebase)、カメラ/写真(CameraX、AVFoundation、vision-camera)、生体認証(FaceID/TouchID、BiometricPrompt)、バックグラウンドタスク(BGTaskScheduler、WorkManager)、アプリストアのサブスクリプションに関する非トランザクション型エンタイトルメントアーキテクチャの補足事項については、ネイティブ APIをご覧ください。
プラットフォーム固有の注意点
iOS のバックグラウンドタスクの制限
BGAppRefreshTask:システムから与えられる実行時間は約30秒です。この時間内にタスクがsetTaskCompletedを呼び出さないと、システムによってタスクが終了され、今後のリクエストの優先度が下がります。BGProcessingTask:最長3分間実行できますが、デバイスの充電中かつ Wi-Fi 接続時に限られます。時間に敏感な同期には依存しないでください。- プッシュ通知によってアプリが起動された場合(
content-available: 1)、実行時間は約30秒です。処理にそれ以上かかる場合は、代わりにバックグラウンドダウンロード用のURLSessionを開始してください。 - システムはユーザーの行動を学習します。ユーザーが午前8時にアプリを一度も開かなければ、午前8時のバックグラウンド更新は実行されません。更新の失敗を想定して設計し、フォアグラウンド復帰時には必ず全件同期で追いつくようにしてください。
Android のバックグラウンド制限
- Doze モード(Android 6 以降):画面がオフになり、デバイスが静止状態になった後、ネットワークアクセスはメンテナンス時間帯(約15分ごとに実行され、間隔は最長約1時間まで延長)にまとめられます。
NetworkType.CONNECTED制約を指定したWorkManagerは、次の時間帯まで実行が延期されます。緊急メッセージには、FCM の高優先度メッセージを使用してください(スロットリングされるまでの上限は約10件/日です)。 - App Standby バケット(Android 9 以降):アプリは Active → Working Set → Frequent → Rare → Restricted の順に分類されます。Rare/Restricted のアプリは、ジョブとアラームが大幅に制限されます。アプリが Restricted バケットに入ると、
WorkManagerの定期処理は24時間に1回しか実行されない場合があります。 - 正確なアラーム(Android 12 以降):
setExactAndAllowWhileIdle()にはSCHEDULE_EXACT_ALARM権限が必要です。ユーザーは設定からこの権限を取り消せます。スケジュールを設定する前に必ずcanScheduleExactAlarms()を確認し、取得できない場合は不正確なアラームにフォールバックしてください。 - フォアグラウンドサービスの種類(Android 14 以降):マニフェストで
foregroundServiceTypeを宣言する必要があります。種類:camera、location、mediaPlayback、dataSyncなど。誤った種類を使用するとクラッシュします。dataSync種類の実行時間は6時間に制限されています。
Flutter 固有の注意点
constコンストラクター:ステートレスウィジェットと、変更されないウィジェットのサブツリーには、必ずconstを使用してください。constを使用しないと、親の再ビルド時に Flutter がサブツリー全体を再ビルドします。これは Flutter のパフォーマンスに関する最大の問題です。Keys:リストの並べ替え、挿入、削除が発生する場合は、リスト項目にValueKeyを使用してください。キーがないと、Flutter が状態を誤って再利用します。たとえば、ユーザーが TextField A に入力した後で項目を削除すると、そのテキストが項目 B に表示されます。- プラットフォームチャネル:Dart とネイティブ(iOS/Android)間の呼び出しは非同期で、順番に処理されます。プラットフォームチャネルを短時間に繰り返し呼び出す場合(毎秒100回超)は、データを1回の呼び出しにまとめてください。大きなペイロードには、バイナリーコーデックを使用する
BasicMessageChannelを使ってください。 - アイソレート:Dart はシングルスレッドです。16ms を超える CPU 処理(大きなレスポンスの JSON 解析、画像処理など)には、
Isolate.run()(Dart 2.19 以降)またはcompute()を使用してください。メインアイソレートで100KBを超える JSON を解析しないでください。 - 画像キャッシュ:Flutter のデフォルトの
Imageウィジェットは、デコードした画像をメモリ上に制限なくキャッシュします。画像が50枚を超えるリストでは、cached_network_imageのmemCacheHeight/memCacheWidthを使って縮小してください。そうしないと、メモリ使用量が際限なく増加します。 - 状態の復元:Android では、システムがバックグラウンドのアプリを積極的に終了します。
RestorationMixinを使うか、重要な状態をディスクに保存してください。ユーザーが10項目のフォームに入力し、別のアプリに切り替えて戻ったときにフォームが空になっている場合、それはバグです。
Compose 固有の注意点
- 再コンポジション:変更可能な値をキャプチャするラムダは、親の再コンポジションを引き起こします。
rememberで保持したラムダを渡すか、計算コストの高い処理にはderivedStateOfを使用してください。ホットスポットを特定するには、Layout Inspector の「Show Recomposition Counts」を使用してください。 - 安定性:Compose は、
@Stableまたは@Immutable型の再コンポジションをスキップします。データクラスがList<T>(不安定)を使用していると、Compose は毎回再コンポジションします。kotlinx.collections.immutable.ImmutableListを使用するか、不変性を保証できる場合は@Immutableを付けてください。 LazyColumnの項目キー:items(key = { it.id })で必ずkeyを指定してください。キーがないと、リストの変更時にスクロール位置が崩れ、アニメーションも失敗します。
オフライン同期の判断ルール
競合解決戦略
- Last-Write-Wins(LWW):データがユーザー所有で、同時編集がまれな場合(ユーザープロフィール、設定、個人メモ)に使用します。単純にタイムスタンプを比較し、新しい方を採用します。リスク:2台のデバイスで同時に編集すると、気づかないうちにデータが失われる可能性があります。
- フィールド単位のマージ:同じレコードの異なるフィールドが別々のデバイスで編集される可能性がある場合(例:ユーザーがスマートフォンで
nameを、タブレットでemailを編集する場合)に使用します。競合しないフィールドの変更をマージし、競合するフィールドには手動解決のフラグを付けます。フィールドごとのタイムスタンプを追跡する必要があります。 - Operational Transform/CRDT:リアルタイムの共同編集(共有ドキュメント、共同編集ホワイトボード)にのみ使用します。実装は複雑なため、ライブラリ(Yjs、Automerge)を使用してください。独自の OT/CRDT を実装しないでください。
- キューと再試行:書き込み専用の操作(フォーム送信、分析イベント、チャットメッセージ)に使用します。ローカルでキューに登録し、オンラインになったら送信して、冪等性キーを使って再試行します。各操作は独立しているため、競合は発生しません。
同期アーキテクチャの判断ルール
- エンティティの種類が5未満で、同期がユーザーからサーバーへの一方向のみ(共同編集なし)の場合:タイムスタンプベースの差分同期を使用します。クライアントが
lastSyncTimestampを送信し、サーバーはそのタイムスタンプ以降に変更されたレコードを返します。 - エンティティの種類が5〜15、またはサーバーからクライアントへのプッシュが必要な場合:変更トークン(CloudKit)またはサーバー送信イベントを使用します。クライアントが同期カーソルを保存し、サーバーが差分をプッシュします。
- リアルタイムの共同編集(複数のユーザーが同じデータを編集)に対応する場合:OT/CRDT ライブラリと WebSocket を使用します。単純な同期と比べて複雑さは10倍です。共同編集が中核要件の場合にのみ選択してください。
アプリのライフサイクルに関する判断ルール
強制アップデート戦略
- 重大なセキュリティパッチ:全画面モーダルでアプリの利用を強制的にブロックします。閉じるオプションは設けません。アプリの起動ごとに、リモート設定(
/api/min-version)を確認します。 - 互換性のない API 変更:「今すぐアップデート」ボタンがあり、ストアにリンクするブロッキングモーダルを表示します。ブロックするまでの2週間は猶予期間とし、その間は閉じられるバナーを表示します。
- 新機能:機能のためにアップデートを強制することはありません。閉じられるバナーを表示します。ユーザーが3回無視した場合は、30日間表示を停止します。
- 実装:リモート設定(Firebase Remote Config、LaunchDarkly、またはシンプルな API エンドポイント)に
minSupportedVersionを保存します。起動時にアプリのビルド番号と比較します。バージョンチェックをハードコードしないでください。
アプリのバージョン間の移行
- ローカルデータベースのスキーマが変更される場合は、バージョン管理されたマイグレーションを使用します(Room の
@Database(version = N)、Core Data の軽量マイグレーション、Drift のスキーマバージョン管理、Hive の型アダプター)。ユーザーはバージョンを飛ばしてアップデートするため、バージョン N-2 から N へのマイグレーションを必ずテストします。 - 認証トークンの形式が変わる場合は、2回のリリースサイクルにわたって旧形式と新形式の両方をサポートします。1回のリリースで全セッションを無効にしないでください。サポートへの問い合わせが殺到します。
- バックエンドのエンドポイントを切り替える場合は、リモート設定を使ってトラフィックを段階的に移行します。1週間かけて 10% → 50% → 100% と移行します。いずれかの段階でエラー率が 1% を超えた場合は、移行を一時停止して調査します。
アンチパターン
- 現在のビューが所有する状態に
@ObservedObjectを使用しないでください。@StateObject(iOS 15)または@Observableと組み合わせた@State(iOS 17以降)を使用します。@ObservedObjectは注入された依存関係にのみ使用します。 - Compose で ViewModel の外部に
mutableStateOfを使用しないでください。変更可能な状態はすべて ViewModel に保持し、UI はcollectAsStateWithLifecycle()で監視します。 - Flutter で破棄済みのウィジェットに対して
setStateを呼び出さないでください。非同期の状態更新前に必ずmountedを確認します。Riverpod ではref.onDispose()を使って非同期処理をキャンセルします。 - React Native で
keyExtractorとgetItemLayoutを指定せずにFlatListを使用しないでください。指定しないと、50件を超えるリストではスクロール性能が大幅に低下します。 - ナビゲーション状態をグローバルストアに保存しないでください。プラットフォームのナビゲーションスタックを使用します。グローバルなナビゲーション状態は、戻るボタンの不具合やディープリンクの失敗を引き起こします。
- 同期的なデータベース読み取りでメインスレッドをブロックしないでください。Android では
withContext(Dispatchers.IO)、iOS ではバックグラウンドアクター、React Native ではInteractionManager.runAfterInteractions、Flutter ではIsolate.run()を使用します。 - Flutter では、100KB を超える大きな JSON をメインスレッドで解析しないでください。
compute()またはIsolate.run()を使用します。Android では、ネットワーク I/O やディスク I/O をメインスレッドで行わないでください。デバッグビルドではStrictModeがこれを検出します。 - フォールバックなしでプラットフォーム固有のコードを使用しないでください。プラグインが iOS 専用の場合、Android ビルドでクラッシュさせず、正常に機能を縮退させるか、「このプラットフォームでは利用できません」と表示します。
ワークフロー
ステップ 1:プラットフォーム戦略とセットアップ
- ネイティブかクロスプラットフォームかを選びます。アプリに AR、カスタムカメラ、またはウィジェットが必要な場合はネイティブを選びます。データ表示とフォームが中心で、プラットフォーム固有の機能が3つ未満の場合はクロスプラットフォームを選びます。
- 最低 OS バージョンを設定します。iOS 16以降(またはエンタープライズ向けの場合は15以降)、Android API 26以降(Android 8.0)とします。それより古いバージョンを対象にする場合は、その理由を文書化します。
- ビルドツールを設定します。iOS では Xcode と SwiftPM、Android ではバージョンカタログを使用した Gradle、React Native では EAS Build を使用した Expo、または Flutter では
flutter_lintsと CI 用の Fastlane/Codemagic を使用します。
ステップ 2:アーキテクチャと設計
- アプリがオフラインで動作する場合は、API スキーマより先にローカルファーストのスキーマを設計します。競合解決の戦略を定義します(シンプルな場合は LWW、共同編集の場合はフィールド単位のマージ)。
- UI の構築前に、すべての画面、モーダル、ディープリンクのルートを含むナビゲーショングラフを定義します。
- 状態管理を選択します。iOS 17以降では
@Observableと SwiftData、Android では ViewModel と StateFlow、React Native では Zustand と TanStack Query、Flutter では Riverpod と Drift/Isar を使用します。
ステップ 3:開発と統合
- まず重要なユーザーフロー(オンボーディング → コアアクション → 結果)を構築します。コアフローが最初から最後まで機能するまでは、設定、プロフィール、その他のフローを構築しないでください。
- プラットフォーム機能(カメラ、通知、生体認証)は一度に 1 つずつ統合します。次に進む前に、それぞれ実機でテストしてください。
- 最初の TestFlight/内部テストトラックでのリリース前に、クラッシュレポート(Crashlytics / Sentry)と基本的な分析機能を追加します。
ステップ 4:テストとデプロイ
- 実機でテストします。最低でも iOS デバイス 2 台(サポート対象の最も古いデバイスと最新のデバイス)と Android デバイス 3 台(ローエンド、ミッドレンジ、フラッグシップ)を使用します。
- CI で重要なフローの UI テストを実行します。ViewModel/リポジトリの単体テストでは、カバレッジを 80% 超にします。
- ストアへの自動申請用に、Fastlane(iOS)または Gradle Play Publisher(Android)を設定します。
自己検証プロトコル
モバイル実装を完了したら、レビューに提出する前に次の項目を確認してください。
- アプリを実機で実行します(シミュレーターだけでは不十分です)。シミュレーターでは、パフォーマンス、メモリ、ジェスチャーに関する問題が見えないことがあります。
- オフラインでコアフローをテストします。機内モードを有効にして操作を実行し、ネットワークを再び有効にして同期を確認します。オフライン時にアプリがクラッシュしたり、データが失われたりする場合は、リリースできる状態ではありません。
- ミッドレンジデバイスでメモリをプロファイリングします。ピークメモリが 200MB を超える場合は、リリース前に原因を調査します。
- サポート対象の最も古いデバイスでコールドスタート時間を測定します。2 秒を超える場合は、初回フレーム表示後まで初期化を遅らせます。
- 指で各インタラクティブ要素をタップして、すべてのタッチターゲットが >=44pt(iOS)/>=48dp(Android)であることを確認します。マウスカーソルは使用しないでください。
- すべての画面でダークモードを確認します。読めないテキストや見えない要素があれば、マージ前に修正します。
- Dynamic Type(iOS)/フォントスケール 200%(Android)でテストします。テキストが切れたり重なったりする場合は、レイアウトを修正します。
- コアフローで VoiceOver(iOS)/TalkBack(Android)を有効にしてアプリを実行します。すべてのインタラクティブ要素が、意味のある形で読み上げられる必要があります。
- トークン、パスワード、PII などの機密データがログに表示されないことを確認します。フローの実行中に
adb logcat(Android)または Console.app(iOS)を実行します。
障害からの復旧
- アプリの起動時にクラッシュする:クラッシュログで正確な行を確認します。よくある原因は、nil の強制アンラップ(Swift)、初期化されていない lateinit(Kotlin)、ネイティブモジュールの欠落(React Native)です。サードパーティライブラリでクラッシュする場合は、前のバージョンに固定して問題を報告します。
- Xcode/Gradle の更新後にビルドが失敗する:派生データ(
rm -rf ~/Library/Developer/Xcode/DerivedData)または Gradle キャッシュ(./gradlew clean)をクリーンアップします。それでも失敗する場合は、ビルドツールの破壊的変更についてリリースノートを確認します。問題が解決するまで、プロジェクトのツールチェーンバージョンを固定します。 - UI の表示が実機とシミュレーターで異なる:シミュレーターは Mac の GPU を使用します。実機でテストしてください。レイアウトの問題については、ピクセル値がハードコードされていないか(代わりに pt/dp を使用)、セーフエリアのインセットが適切か確認します。
- 機能追加後にパフォーマンスが低下する:Instruments(iOS)または Android Studio Profiler でプロファイリングします。メインスレッドのブロック、過剰な再コンポーズ/再レンダリング、キャッシュなしでの大きな画像の読み込み、サブスクリプション/オブザーバーのリークを確認します。
- プッシュ通知が届かない:次の項目を確認します。(1)有効な APNs/FCM トークン、(2)正しいバンドル ID/パッケージ名、(3)証明書/キーの有効期限、(4)バックグラウンド動作を抑制する低電力モードがデバイスで有効になっていないこと。アプリのコードをデバッグする前に、
curlで APNs/FCM に手動でプッシュ通知を送信してテストします。 - App Store/Play Store でアプリが却下される:却下理由を正確に読みます。よくある理由は、プライバシーポリシー URL の欠落、App Privacy の詳細が不完全(iOS)、データセーフティフォームの未提出(Android)、正当な理由のないバックグラウンド位置情報の使用です。メタデータを修正し、推測で対応しないでください。
既存アプリの把握
既存のモバイルプロジェクトを引き継ぐ、または参加するときは、次の手順に従います。
- ビルドして実行する(15 分) — クローンし、依存関係をインストールして、ビルドし、実機で実行します。失敗した場合は、まずビルドを修正します。
- アーキテクチャのパターンを特定する(10 分) — MVC、MVVM、MVI、それとも未整理でしょうか?状態がどこにあるか、API から UI へデータがどのように流れるかを確認します。
- ナビゲーショングラフを把握する(10 分) — すべての画面と、それらの接続方法をリストアップします。ディープリンクのハンドラーを確認し、使用中のナビゲーションライブラリを記録します。
- 依存関係の一覧を確認する(5 分) — Podfile/SPM(iOS)、build.gradle(Android)、package.json(React Native)を確認します。古い依存関係やメンテナンスが終了した依存関係を特定します。
- 既存のテストを実行する(5 分) — カバレッジ、テストの種類、未テストのフローを記録します。
- プラットフォーム固有の技術的負債を確認する(5 分) — 非推奨の API(Xcode の警告、Android lint を確認)、不足している権限宣言、ハードコードされた文字列(ローカライズへの対応状況)を確認します。
- アプリのプロファイルを測定する(10 分) — コールドスタート時間、メモリ使用量、最も項目数の多いリスト画面のスクロール性能を測定します。これらの数値がベースラインになります。
スクリプト
scripts/check_app_size.sh
モバイルアプリのビルド出力ディレクトリを分析し、サイズの問題を特定します。引数にビルド出力ディレクトリを指定すると、アプリのバイナリ/バンドル(.app、.apk、.aab、.ipa)を検出し、合計サイズ、最大のファイル、カテゴリ別のアセット内訳を報告します。合計サイズが一般的な上限(iOS は 50MB、Android は 150MB)を超える場合に警告します。
scripts/check_app_size.sh ./build/outputs/apk/release
scripts/check_app_size.sh --threshold-ios 40 --top-files 10 ./DerivedData/Build/Products/Release-iphoneos
scripts/check_permissions.py
AndroidManifest.xml または Info.plist から権限を抽出して監査します。要求されている権限を特定し、危険性の高い可能性がある権限(CAMERA、LOCATION、CONTACTS など)について説明とともに警告し、権限の合計数を報告します。JSON 出力に対応しています。
scripts/check_permissions.py app/src/main/AndroidManifest.xml
scripts/check_permissions.py --format json ios/Runner/Info.plist