ジャンルを問わず、ほぼすべてのスマートフォンアプリで必要になる技術・設計・実務を、 実際にApp Store審査提出まで到達したアプリの実例つきで整理したものです。 各項目は「なぜ汎用か」「QuesToDoでの実例」「実践ポイント」の3点セットで読めます。
画面から外部SDKを直接呼ばない — それだけでアプリの寿命が延びる
広告・課金・分析などの外部SDKは、モバイル開発でもっとも「壊れやすい依存」です。SDKのメジャー更新・サービス終了・審査要件の変化は必ず起きます。UI層に直接呼び出しが散らばっていると、その度に全画面を触ることになります。
広告/課金/分析/共有/バックアップ/通知をservices/ディレクトリに隔離し、画面はサービスの関数だけを呼ぶ構成にしました。さらにAPIキーが未設定のときは「安全な無効モード」で動く設計にしたため、キー設定前の開発中も、SDK未搭載の旧ビルドでも、アプリは一度も起動不能になりませんでした。
TypeScript strict + any禁止は「速度を落とす制約」ではなく「後半戦の保険」
モバイルアプリは「リリース後に直しにくい」ソフトウェアです。ユーザーの手元で動くバイナリは即座には差し替えられず、クラッシュは星1レビューに直結します。コンパイル時に潰せるバグをランタイムに残さないことの価値が、Webよりも高い環境です。
strictモード・any禁止を初日から徹底。マスタデータ(スキル52種・アバターパーツ84項目)もTypeScript定数にしたため、カタログの定義ミスは型エラーとして即座に検出されます。i18nのキーもリテラル型で縛り、月名キー(months.1〜12)のような動的参照も型安全に扱いました。
ユーザーの端末にあるDBは「戻せない本番環境」
オフライン対応・高速起動のため、ほとんどのアプリはローカルDB(SQLiteなど)を持ちます。そしてローカルDBの怖さは、全ユーザーの端末それぞれが独立した本番DBであること。どのバージョンから更新してくるか分からないユーザー全員を、安全に最新スキーマへ運ぶ仕組みが必須です。
スキーマ変更はALTER TABLE ADD / CREATEのみ許可する追記型マイグレーションをv1→v9まで9世代実施。PRAGMA user_versionで現在版を管理し、「v3のフィクスチャDBを最新まで昇格させて中身を検証する」テストを世代ごとに常備しました。開発期間を通じて実データ入りの実機DBに毎世代適用し、データ事故ゼロで完走しています。
「現在値」を保存するな、「起きたこと」を保存しろ
ポイント・スコア・課金履歴・学習進捗など、多くのアプリは「積み上がる数値」を持ちます。現在値だけを上書き保存すると、集計仕様の変更・バグ修正・統計機能の追加のたびに過去へ遡れず詰みます。履歴(イベント)を真実にし、現在値は再計算できるキャッシュとして扱うのが堅牢です。
EXP獲得はexp_eventsへの追記のみ。レベル・累計EXP・ストリークはすべてイベント列から純関数で再計算します。開発中にレベル曲線を1度変更しましたが、再計算を通すだけで既存データが新曲線に正しく移行されました。後から追加した統計機能(21項目)も、スキーマ変更なしの純粋な導出だけで実装できています。
モバイルのバグの名所は「日付跨ぎ」と「タイムゾーン」
ストリーク・期限・リマインダー・「今日の〇〇」— 時刻に依存する機能はほぼ全アプリにあります。ロジック内部でDate.now()を呼ぶと、深夜0時前後の挙動やタイムゾーン差をテストで再現できず、ユーザーからの「日付がおかしい」報告に対して再現手段がなくなります。
すべてのロジック関数は現在時刻now(unixミリ秒)と今日todayStr('YYYY-MM-DD')を引数で受け取ります。この規約のもとで、ストリークの日付跨ぎ・繰り返しクエストの出現日・通知スケジュールといった時刻依存ロジックを含む計366件のテストを構築し、任意の日時を固定して検証できました。DB層のテストはモックでなくNode組み込みSQLiteで実SQLを流しています。
後付けのi18nは「全画面リライト」と同じコスト
ストア配信は最初からグローバルです。文字列をハードコードしたアプリの多言語化は、全コンポーネントの書き換えになります。たとえ当面1言語でも、文言を辞書ファイルに分離しておくだけで、文言修正がコード変更なしで済み、レビュー・監修も辞書ファイル1枚で回せます。
表示文字列のハードコード禁止をルール化し、i18next + ja/en 2言語・約700キーで管理。新機能の文言追加時は両方のJSONへ同時追加(英訳が仮でもTODOコメント付きで入れる)を徹底しました。端末ロケール追従+アプリ内切替の両対応で、起動時の言語適用はモジュールスコープで行い「初回フレームだけ英語で描画される」フラッシュも防いでいます。
records.summaryなど)hex直書き禁止 — 一貫性は才能ではなく仕組みで作る
デザイナー不在の開発でUIが素人っぽくなる最大の原因は、画面ごとに微妙に違う色・余白・フォントサイズです。値をトークン(定数)に集約し、選択肢そのものを減らすことで、誰が書いても同じトーンになります。ダークモード対応やブランド変更もトークンの差し替えで済みます。
色・タイポグラフィ・角丸・余白をtheme/tokens.tsに集約し、hex値のコード直書きを禁止。さらに「アクセントは1色のみ」「フォントウェイトは3段階のみ」「影はアバターの下だけ」という制約をデザイン文書で明文化しました。ドット絵はコンテンツ領域限定というルールで、Apple的な端正なUIとRPGの世界観を衝突させずに共存させています。
モバイルアプリは「開きっぱなしで放置される」前提で作る
ユーザーはアプリを終了しません。バックグラウンドに置いたまま翌日開きます。タブ画面はアンマウントされず、propsも自動では更新されません。「昨日のままの画面」「別タブで変えたのに反映されない」は、この前提を見落としたときの定番バグです。
タブ間のデータ反映はfocusイベントでの再読込に統一。さらにAppStateの復帰検知+分単位タイマーで「今日」を追従させ、深夜0時を跨いでもホームの「今日のクエスト」や統計が古い日付のまま残らないようにしました。フォアグラウンド復帰時には課金状態の再評価も行い、サブスク失効をセッション中に反映します。
「その都度スケジュール」ではなく「あるべき状態を毎回作り直す」
リマインダー・期限通知はほぼ全アプリの要件です。個別にスケジュール/キャンセルを積み重ねる方式は、編集・削除・設定変更のたびに整合性が壊れ、「消したはずの通知が来る」バグの温床になります。また通知許可はいつでも取り消されるため、権限がない状態を常に正常系として扱う必要があります。
「現在のデータから、あるべき通知一覧を純関数で導出→既存を全キャンセル→全スケジュール」という宣言的な方式にしました。これで個別の差分管理が不要になり、通知プラン自体をユニットテストできます。ストリーク防衛通知は「今日完了したら翌日20時に予約する」先行スケジュール方式で、アプリを開かない日でも途切れる前に届きます。サーバーレス方針のためAPNs(プッシュ)は使わず、全てローカル通知で構成しました。
コード実装は全体の半分。残り半分は「ストアとお金の実務」
サブスクリプション・買い切りはアプリ収益化の本命ですが、StoreKitの生実装はレシート検証・復元・プラン変更・家族共有など罠が多く、さらに契約書類(W-8BEN等)・ストア側の商品設定・サンドボックステストという実務が必ず付いてきます。この全体像を一度経験しているかどうかで立ち上がり速度が大きく変わります。
RevenueCatを採用し、月額/年額/買い切りの3商品をpremiumという1つのエンタイトルメントに束ねました。画面は「プレミアムか否か」だけを見るため、将来のプラン追加でUIは変わりません。オフライン時は7日猶予付きのローカルキャッシュで判定し、このキャッシュはバックアップに含めない(改ざん耐性)設計です。実機のサンドボックス環境で購入成功・エンタイトルメント反映・失効をテストし、「サンドボックスの月額サブスクは数分で失効する」「未サインイン時はUSD表示になる」「復元はSTORE_PROBLEMが頻発する既知の不安定挙動がある」といった紛らわしい正常挙動も把握済みです。
実装より「事故らない運用設計」が本体
無料アプリの収益源として広告は依然主力です。ただし本番広告を開発中に表示して自分でタップするとアカウント停止(=収益源の喪失)があり得るため、テストIDとの切替を人間の注意力でなく仕組みで保証する必要があります。iOSではATT(トラッキング許可)とSKAdNetworkの対応も事実上必須です。
AdMobでバナー+リワードの2形式を実装。__DEV__ビルドでは本番ユニットIDを設定していてもテストIDに強制切替する構造にし、自己クリック事故を構造的に防止しました。リワード広告は「+○EXP即付与」ではなく「視聴で30分間EXP2倍」のブーストタイム形式にし、視聴が能動的な戦略になるよう設計。ATTの結果は広告リクエストごとに反映し、ロード失敗時は30秒バックオフで再試行します。ホーム画面にはバナー広告を置かない(世界観の聖域)、リワードはユーザーが能動的に選ぶ導線としてホームにのみ置く、というプロダクト判断も明文化しました。
計測は「何でも送る」から「設計して送る」へ
継続率や機能の利用状況が見えなければ改善は勘になります。一方でGDPRや各ストアのプライバシー申告により、「とりあえず全部計測」は法務リスクそのものになりました。イベントを設計し、個人情報を送らない構造を最初から作るのが現代の標準です。
PostHogで11イベントを定義し、イベント名とプロパティを型定義+仕様書で管理(勝手に増やさない運用)。識別子は端末生成の匿名UUIDのみで、名前・メール等のPIIは一切送りません。タッチ操作の自動キャプチャとセッション録画は使わず、送信されるのは定義済みイベントとSDK標準のアプリ起動/復帰イベントのみに限定。App Storeのプライバシー申告(収集データ7種・トラッキング申告)と実装が一致する状態を保っています。
サーバーレスアプリの信頼は「データを失わないこと」で決まる
端末内にデータを持つアプリにとって、機種変更・誤削除はデータ全損のリスクです。エクスポート/インポートは信頼の生命線ですが、「自分が過去に出力したファイル」は将来のスキーマと食い違い、また改ざんされている可能性もある外部入力として扱う必要があります。
バックアップJSONにスキーマバージョンを持たせ、旧版は取込時にマイグレーション。復元は型・日付形式・値域まで検証してから単一トランザクションで全置換し、復元後に再計算を走らせてキャッシュを作り直します。「期限が過去にならない永久ブースト」のような改ざんデータは検証で弾きます。ファイル書込は一時ファイル→世代退避→本体の順に入れ替え、書込中クラッシュでも直前世代へフォールバックできる構造です。課金状態は改ざん耐性のため意図的に含めていません。
「コードが完成」から「ストアに並ぶ」までは、もう一つのプロジェクト
証明書・プロビジョニング・ビルド番号・スクリーンショット規格・プライバシー申告・年齢制限・配信地域・審査対応 — どのアプリでも通る道ですが、初見では数週間溶けます。ビルド→提出を自動化・非対話化しておくと、リジェクト対応の再提出が数分で回せるようになります。
EAS Build/Submitで証明書とASC APIキーをクラウド管理し、2回目以降のビルド・提出を完全非対話化(ビルド番号も自動インクリメント)。ライブラリが自動注入するpush通知entitlementが原因のビルド失敗は、自作config pluginで該当エントリを除去して恒久解決しました。審査提出では「iPad対応フラグが立っているとiPadスクリーンショットが必須になる」ことが判明し、未検証のiPad対応を外してiPhone専用で提出する判断をしています。プライバシー申告・EEA配信除外(同意フォーム未実装のため)・App内課金の同時提出まで含めて完遂しました。
VoiceOverで一周すると、見えなかったバグが見える
アクセシビリティは特別対応ではなく品質項目です。スクリーンリーダーで操作不能な画面は一部のユーザーにとって「クラッシュと同じ」であり、ストア審査やレビューでの指摘対象にもなります。そして多くの問題は、知っていれば数行で防げるものです。
モーダルの背景Pressableがデフォルト設定のままだとVoiceOverが内部要素を畳み込み、シート内の操作に一切到達できなくなる問題を検出し、accessible={false}の明示をモーダル実装の必須ルール化。アコーディオンには開閉状態の読み上げ(expanded state)を付与し、完了演出はスクリーンリーダー使用時に自動クローズを無効化してユーザーのペースで読めるようにしました。