Ninomae Renichi OFFICIAL
Ninomae Renichi OFFICIAL - https://ninomae-renichi.com/
個人開発iOSアプリApp Store審査

RPG風ToDoアプリ「QuesToDo」をApp Storeで公開しました — 2回のリジェクト(EULAリンクとATT不表示)を直して通すまで

2026年8月8日、「QuesToDo」というiPhone向けのRPG風ToDoアプリがApp Storeに並びました。やることを「クエスト」として扱い、完了するとEXPがもらえて冒険者がレベルアップする、というアプリです。開発に使ったのはWindows PCとiPhone実機だけで、Macは一台も使っていません。ただ、公開までは平坦ではなく、App Store審査で2回リジェクトされました。1回目は利用規約リンクの欠落、2回目は「ATTの許可要求が見つからない」という指摘です。とくに2回目は、コードでダイアログを呼んでいるのに表示されていなかったという、気づきにくい罠でした。この記事では、何を作ったのか、どう設計したのか、そして2回のリジェクトを何が原因でどう直して通したのかを、審査のやり取りを含めて記録として残しておきます。

公開したもの — RPG風ToDoアプリ「QuesToDo」

QuesToDo(クエストゥドゥ)は Quest + ToDo の造語で、毎日のやることを「クエスト」として扱うRPG風のToDoアプリです。ストアのサブタイトルは「RPG風ToDo・タスク管理で習慣化」としました。タスクを完了するとEXP(経験値)がもらえ、冒険者がレベルアップして、スキルやドット絵アバターのパーツが解放されます。無料でダウンロードでき、App内課金もあります。

バージョンは1.0、カテゴリは仕事効率化とライフスタイル、レーティングは4+、対応はiOS 15.1以上でiPhone専用、サイズは約44MBです。データはすべて端末内に保存され、アカウント登録は要りません。バックアップのエクスポートとインポートには対応しています。表示は日本語と英語の2言語です。ストアページは https://apps.apple.com/jp/app/questodo/id6794613228 です。

着手は2026年7月16日、審査に通ったのが8月7日で、同じ日に手動リリースを実行し、ストアに反映されたのが翌8月8日でした。企画から公開まで23日間です。実装コードは13,701行(86ファイル、テスト除く)、自動テスト366件、gitコミット71件、画面数は11画面です。今回もAIエージェントとの協働で作っています。

開発環境についても先に書いておきます。コード編集と開発サーバーはWindows PC、動作確認はiPhone実機、iOSのネイティブビルドだけをExpoのクラウドビルドサービスであるEAS Buildに任せる、という構成です。日々のUIとロジックの開発はWindows上で回し、ネイティブの部分を追加したときだけクラウドでビルドし直します。

なぜ作ったか — 「消化しても何も起きない」を解く

ToDoアプリが続かない理由のうち、私が一番大きいと考えたのは「消化しても何も起きないから」でした。チェックを付けても画面から1行消えるだけで、それが自分の何かになった感覚がありません。だったら、消したときに何かが起きるようにすればいい、というのが出発点です。

解き方に選んだのがゲーム的なフィードバックです。要件定義の段階で「タスク完了で経験値・レベルアップ・報酬などのゲーム的フィードバックを得られる」ことをコアに据え、タスク管理そのものではなく習慣化の継続動機を設計目標に置きました。リスト自体は普通のToDoのままで、変えたのは「消すと前に進む」という手応えだけです。

世界観にドット絵のレトロRPGを選んだのは好みだけの理由ではありません。アセットの内製率が最も高く、粗が「味」として許容されるスタイルだからです。実際、アバターやアイテムのアイコン、UI素材はコードで生成し、大型のスプライトと背景だけ画像生成に頼りました。ドット絵108枚はすべて自作スクリプトで生成しています。グラフィックが律速にならない形を先に決めておくことは、完走にかなり効きました。

何を実装したか — クエスト管理と冒険者の成長

クエスト管理は、普通のToDoアプリとして不足がないことを優先しました。難易度は易・普通・難の3段階で、基礎EXPはそれぞれ10・30・50です。繰り返しは毎日・曜日指定・隔週・N日ごとの4種類で、期限は時刻まで指定できます。優先度、ジャンル、フォルダ整理、検索、並び替え、ドラッグ&ドロップ、完了の取り消しも用意しました。繰り返しクエストの同日二重完了は、データベース側のユニーク制約で弾いています。並び替えは、表示しているサブセットが占めていたスロットだけを入れ替える方式にして、絞り込み中に並び替えても他のクエストの相対順が崩れないようにしました。

成長の側は、必要EXP = 30 × レベルの1.2乗というレベル曲線の上に組み立てました。スキルは魔法18種・とくぎ15種・レベル称号13種・パッシブ6種の計52種を、Lv2からLv99まで解放が途切れないよう、解放間隔が最大6レベルに収まるように配置しています。ほかに実績称号16種、アバターパーツ81点(「装備しない」の3項目を含めるとカタログ上は84項目)、ステータス5種があります。ステータスはv1ではゲーム上の効果を持たせず、育成そのものを目的にしました。習得済みの魔法ととくぎは1日1回だけ発動でき、その日のクエスト完了EXPにブーストがかかります。

プレミアム向けの統計画面「きろく」は、21項目とグラフ3本です。ここは実装方針がはっきり出た場所で、統計はすべて一次データからの導出にしてデータベースには何も保存せず、スキーマ変更をゼロにしました。グラフもチャートライブラリを入れず、React Nativeの標準的なビューだけで描いています。ライブラリを足さないので、再ビルドも要りません。見せ方のほうは、縦に長い17行のリストから、2列のタイル表示、さらに7セクションのアコーディオン(折りたたみ中はヘッダーに代表値を出す)へと3段階で作り直しました。

続ける仕組みは、サーバー不要のローカル通知3種(デイリー・期限・ストリーク防衛)にまとめました。ストリークは連続達成日数のことです。iOSのローカル通知は1アプリ64件が上限のため、期限リマインダーは直近30件までに制限しています。広告の置き方も意識的に決めました。バナー広告を出すのはクエスト一覧・アバター・設定の3画面だけで、ホーム画面と「きろく」には置いていません。ホームは世界観を守る面、きろくはプレミアム向けの面だからです。リワード広告(視聴すると報酬がもらえる動画広告)は、報酬を「+20EXPの即時付与」から「視聴後30分間はEXPが2倍になるブーストタイム」(1日3回まで、最大90分)に変えました。後者なら、広告のあとにタスクをこなす動機が生まれます。

技術構成と設計判断 — EXPを「イベントの追記ログ」として持つ

スタックはTypeScript 5.9(strict・any禁止)/ React Native 0.81 / Expo SDK 54です。データ層は端末内データベースのexpo-sqlite、課金は購入まわりを代行するRevenueCat、広告はGoogleのAdMob、分析はプロダクト分析ツールのPostHog、通知はローカル通知のみで、バックエンドを持たない完全サーバーレス構成です。ビルドと提出はEAS BuildとEAS Submitで、これがMacなしのiOSリリースを成立させています。

設計の背骨は、EXPをイベントソーシング的に管理したことです。イベントソーシングは「現在の状態」ではなく「起きた出来事の列」を真実として保存する考え方で、EXPの獲得を exp_events テーブルへの追記だけで表現し、レベルや累計は「そこから再計算できるキャッシュ」と割り切りました。開発中にレベル曲線を 50 × レベルの1.5乗 から現在の式へ緩和した(旧曲線ではLv44以降のスキル解放が実質到達不能でした)ときも、再計算関数を通すだけで既存データが移行できています。ストリークも実績も統計も同じイベント列から導出しているので、集計値がずれても再計算で復旧できます。

一方で、追記型を貫きすぎない判断も明示的に決めました。「完了の取り消し」と「ゴミ箱の完全削除」は原則の例外として物理削除します。取り消しに負のEXPイベントを追記する案は、テーブル定義側の「EXPは正数のみ」という制約と矛盾するため採りませんでした。さらに「完了の取り消しはプレイの否定だからEXPも戻す、クエストの削除はプレイの否定ではないからEXPとストリークの実績は残す」という線引きを立て、そのためにストリークの導出元をクエスト完了テーブルからEXPイベント側へ移しています。ゴミ箱を空にしても、過去に積んだ記録が遡って壊れないようにするためです。

DBマイグレーション(スキーマ変更の適用手順)は9世代を「追加のみ」で通し、列の追加とテーブルの新規作成しか許さないルールにしました。テストは366件を全パスで維持しています。支えになったのは、レベル計算やストリークを純関数(同じ入力なら常に同じ結果を返す関数)にして、現在時刻や今日の日付を引数で注入する(内部で Date.now() を呼ばない)徹底です。日付跨ぎやタイムゾーンのエッジケースを固定化できました。データベース層のテストはモックではなく、Node.js組み込みのSQLiteに差し替えて実際のSQLで検証しています。

もうひとつ通しているのが「ローカルのデータは疑ってかかる」という方針です。課金状態のキャッシュも、レビュー促進の記録も、広告ブーストも、未来の時刻が入っていたら信用しません。バックアップからの復元では、ブーストの倍率と有効期間を妥当な範囲に限定して受け入れます。極端に長い有効期間の行がそのまま復元されると、EXPが恒久的に倍化したうえリワード広告の導線も塞がって、復旧経路がなくなるからです。課金状態はバックアップのJSONに含めていません。改ざんで広告を消せてしまうためで、購入の引き継ぎはストア側の「購入の復元」に任せています。外部SDK(広告・課金・分析・共有・バックアップ・通知)はサービス層に隔離し、画面から直接呼ばないルールにしました。APIキーが未設定なら安全な無効モードで動き、ネイティブの機能は動的に読み込むので、古いビルドでも起動できなくなることはありません。

審査1回目 — 「利用規約リンクがない」。原因はアプリ内ではなくストアの説明文でした

最初の提出は2026年8月2日、ビルドは1.0.0(4)です。初回のサブスクリプションはアプリの新バージョンと同時提出が必須なので、アプリ本体とApp内課金3商品(月額・年額・買い切り)をまとめて出しました。各商品には審査用のスクリーンショットが要るので、ペイウォール(課金プランを提示する画面)のスクリーンショットを添えています。App Store Connect(ストア掲載情報の入力や審査提出を行う管理画面。以下ASC)の操作では、「審査用に追加」をバージョンだけでなく各App内課金の商品ページからも押す必要がありました。結果が返ってきたのは8月4日、判定は却下でした。

指摘はガイドライン3.1.2(Business: Payments - Subscriptions)で、「自動更新サブスクリプションを提供しているが、アプリのメタデータに機能する利用規約(EULA)へのリンクが含まれていない」という定型メッセージでした。EULAはアプリの利用条件を定める規約で、Appleの標準EULAを使うか、カスタムEULAを登録するかを選べます。私は標準EULAを使う方針でした。

つまずいたのはここです。標準EULAを選んだ場合、ASCには「利用規約URL」の入力欄がありません。あるのはカスタムEULAのアップロード欄だけで、標準EULAならアプリの説明文(App Description)本文にURLを書くしか手段がない、という仕様でした。アプリ内のペイウォールには規約リンクを置いていたのですが、審査が見ていたのはストア側でした。

対応は、説明文の末尾に標準EULAとプライバシーポリシーのURLを追記し、自動更新の条件も明記することでした。条件は4行で、購入確定時にApple IDに請求されること、期間終了の24時間前までに自動更新をオフにしなければ同じ料金・同じ期間で更新されること、更新料金は期間終了前の24時間以内に請求されること、購入後はApp Storeアカウントの設定からいつでも解約・管理できることです。コード変更も再ビルドも不要な、メタデータだけの修正でした。学びは、App内課金を載せるならアプリ内とストアの両方に規約の導線が要ること、そしてサブスク商品の却下はアプリ本体まで巻き込むことです。

このとき一緒に決めたのが、説明文に価格の金額を書かないことでした。説明文に価格を書く場合は実際の価格と完全に一致させる必要があり、ずれていると今度はガイドライン2.3(不正確なメタデータ)で落ちる要因になります。価格改定に追随し損ねるリスクを負うくらいなら、プラン名と期間だけ書いて金額の表示はApp内課金の欄に任せたほうが安全だと判断しました。

審査2回目 — 「ATTの許可要求が見つからない」。呼んでいるのに出ていませんでした

8月5日に説明文を直して再提出し、翌8月6日にまた却下されました。今度はガイドライン2.1(Information Needed)で、「アプリはAppTrackingTransparencyフレームワークを使用しているが、iOS 26.6 / iPadOS 26.6 でレビューした際にATTの許可リクエストを見つけられなかった」というものです。ATTは、広告用の端末識別子を使う前に、アプリをまたいだ追跡の可否をユーザーに尋ねるiOSの仕組みで、広告SDKを積むなら避けて通れません。審査環境は iPhone 17 Pro Max と iPad Air 11-inch (M3) で、ビルドは1回目と同じ1.0.0(4)でした。前回がメタデータのみの修正だったため、中身は同一です。

調べると、原因は3つ重なっていました。1つ目は要求のタイミングです。広告SDKの初期化 initializeAds() を App.tsx の起動時 useEffect から即実行していたため、アプリがまだ active になっていない(スプラッシュから初回レンダーの間の)タイミングで requestTrackingPermissionsAsync() を呼んでいました。iOSのATTダイアログは、アプリの状態が UIApplicationStateActive のとき、かつ他のUIのトランジション中でないときにしか表示されません。条件を満たさないとiOSは提示を見送りますが、エラーにはなりません。

2つ目は、その失敗を検知していなかったことです。expo-tracking-transparencyは提示が見送られても例外を投げず、status: 'undetermined'(未回答)のまま解決します。旧コードは status === 'granted' かどうかしか見ていなかったので、「出せなかった」と「拒否された」を区別できていませんでした。しかも結果をモジュールスコープの変数にメモ化(一度得た結果を保持して次から再利用すること)していたため、一度すべるとそのセッション中は二度と要求されません。呼び出しは成功し、例外も出ず、ただダイアログだけが出ません。見つけにくい理由はここにあります。

3つ目は導線です。ATTに到達する手段が「新規インストール直後の自動表示」だけでした。iOSは一度「許可」か「許可しない」を選ぶと、削除して再インストールするまでダイアログを再表示しません。つまり、審査担当が1回目の審査ですでに回答済みの端末を使っていた場合や、端末側の「Appからのトラッキング要求を許可」がオフだった場合には、アプリ内のどこからも到達できなかったのです。

どう直して通したか — 実装の修正に、手動導線・再現手順・画面収録・英語の説明を足す

修正はビルド1.0.0(5)に入れました。ATTの要求ロジックを書き直し、アプリの状態を表す AppState が active になるまで待ってから要求します。待ちには10秒の上限を付けて、ATT待ちで他の処理を止めないようにしました。要求の直前にもう一度 active を確認し、待っている間に背面へ落ちていたらその回は要求しません。そして undetermined のまま返ってきたら「提示されなかった」と判定し、600ミリ秒→1.5秒→3秒→5秒→8秒と待つ時間を延ばしながら、最大5回まで要求を試みます。あわせて広告SDKの初期化をATTの解決から切り離し、再試行の最中でも広告表示が動くようにしました。

コードと同じくらい効いたのが、設定タブの【一般】に「広告のトラッキング設定」という行を追加したことです。未回答の端末ではATTダイアログを表示し、回答済みならiOSの設定アプリへ誘導します。一度回答するとアプリ側からダイアログを再表示できないための分岐で、審査担当が確実に到達できる導線になりました。

3つ目は審査メモです。①新規インストールして起動するとホーム画面表示の直後にダイアログが出ること、②手動確認は設定タブ →【一般】→「広告のトラッキング設定」から行えること、③一度回答するとアプリを削除して再インストールするまで再表示されないこと、④端末側で「Appからのトラッキング要求を許可」がオフだとiOSが表示自体をブロックすること、の4点を明記しました。さらに実機でダイアログが出る様子の画面収録をApp Review情報に添付し、原因と修正内容を英語で説明した返信も送っています。

8月6日に再提出し、承認メール2通が届いたのは8月7日の21時59分でした。受理されたのは5項目(App Version 1.0、Premium Lifetime / Monthly / Yearly、サブスクリプショングループ Premium)です。手動リリースを選んでいたので、同じ日に自分でリリースを実行しました。3回目に効いたのは、ATTの実装修正に加えて、①設定画面の手動導線、②審査メモへの再現手順、③実機の画面収録、④原因と修正内容の英語での説明という4点をセットで出したことだと思っています。権限まわりの指摘には、直したという主張だけでなく、その場で再現できる手順と証拠を必ず添えるべきでした。

もうひとつ、2回目から学んだのは「メタデータのみの修正で再提出した場合、審査担当は同じビルドを同じ端末で見る可能性がある」ことです。権限まわりの状態は前回の審査から引き継がれている前提で考えたほうが安全でした。

コード以外に必要だった作業 — 証明書・ストア素材・申告・配信地域

リリースまでに用意したアカウントは6つ(Expo、Apple Developer Program、RevenueCat、Google AdMob、PostHog Cloud、Sentry)です。Apple Developer Programは登録審査に1〜3営業日かかるので最初に申請しておく必要があり、年会費も個人開発では回避できません。Macがないので証明書とプロビジョニングプロファイル(署名に必要な証明書と設定ファイル)が不安でしたが、対話形式の質問に答えるだけでEAS Buildが自動生成するため、手作業はありませんでした。ただしWindowsではローカルビルドがiOSに対応していないため、iOSのビルドは必ずクラウド経由になります。

Apple Developer Programの審査を待つ時間を無駄にしないよう、開発は2段階に分けました。段階1はクエストの作成・編集、SQLite、RPGの演出、アバター、多言語対応(i18n)、画面のUIといった、Expo Goという確認用アプリだけで進められる範囲です。段階2は広告・課金・ATTのような、ネイティブの機能を含むため開発ビルドが必須になる範囲です。分けておくと、審査待ちの期間もアプリの中身を進められます。

ストア素材にも固有の落とし穴があります。スクリーンショットは6.5インチの1242×2688で6枚用意しました。iPhone 11(828×1792)で撮影して自作スクリプトで1.5倍に拡大しました(828と1792を1.5倍するとちょうど要求サイズに一致します)。ただし開発ビルドで撮るとバナーが「Test Ad」になるため、最終的には配布テスト用のTestFlight版で6枚を撮り直しています。ASCへの複数同時アップロードは完了順に並んで順序が崩れるので、1枚ずつ上げるのが確実です。

提出ボタンの手前で実際に止まったブロッカーは3点ありました。iPadのスクリーンショット要求(supportsTablet: true のままだったためで、iPhone専用に変更して解消しました)、コンテンツ配信権の未設定、「サインインが必要です」チェックが初期状態でオンだったことです。プライバシー申告は収集データ7種で、トラッキングありはデバイスIDのみです。アカウント機能がなく端末生成の匿名IDしか持たないので、全データを「ユーザーに紐付けられない」と申告できました。審査メモには、アカウント登録が不要であること、課金はRevenueCat経由であること、広告はバナーとリワードであること、通知はすべてローカルでプッシュ用のサーバーを持たないことも書いています。課金があるアプリは審査でデモ確認されるため、購入テストはサンドボックス用のテスターアカウントで事前に済ませておきました。

配信地域は146か国です。全175地域からEEA(欧州経済領域)の29地域を外しました。EEAは30か国を指しますが、そのうちリヒテンシュタインはApp Storeの配信地域として存在しないため、実際にチェックを外すのは29地域になります。外した理由は、GoogleのUMP(同意管理プラットフォーム)の同意フォームが未実装だったことと、DSA(デジタルサービス法)のトレーダー要件で氏名や住所などの情報がEUのストア製品ページに公開表示されることの2つでした。個人開発では自宅住所の公開になってしまうため、MVPの段階では配信しないと決めました。どちらも後から解除できます。

公開後のAdMobのアプリ確認は、初回に失敗するのが通常でした。app-ads.txt(収益化の検証用に開発者サイトへ置くファイル)を正しく置いていても、Googleのクローラーが新しいストア情報を読むのは概ね24時間おきだからです。確認が済むまでも広告は完全停止ではなく配信量が絞られる「制限」の状態なので、焦って設定を触らず、1日ほど待ってから確認をやり直すのが正解でした。

個人開発者への示唆 — 「正しく動く」と「他人が確認できる」は別物

2回のリジェクトを振り返って一番強く思うのは、どちらもコードのバグではなかったということです。1回目は、規約リンクがアプリ内にはあったのにストア側にありませんでした。2回目は、ATTを確かに呼んでいたのに表示されず、到達する導線もありませんでした。審査が見るのは「正しく実装されているか」だけでなく「その場で確認できるか」でした。

権限まわりには一般化できる教訓もあります。権限ダイアログは一度回答すると二度と出ないので、審査担当が再現できる手動導線をアプリ内に必ず用意しておく必要があります。これがガイドライン2.1系の「見つけられない」というリジェクトに対する本質的な対策になります。そして、ATTのように「呼べば出る」と思い込みやすいAPIは、戻り値から提示の成否を判定して再試行するようにします。エラーではなく undetermined が返るだけ、という挙動は同種のAPIでも起こり得ます。

23日という日数も補足しておくと、突貫工事の結果ではありません。仕様書駆動(要件・画面設計・データ設計・環境構築・デザイントークンの5文書を「正」とし、コードより先に仕様書を更新する)とマイルストーン駆動(1依頼=1マイルストーン、完了ごとに実機確認とコミット、先回り実装を禁止して常に動く状態を保つ)、そして複数のAIエージェントによる相互レビューの上に乗っています。実装・調査・ストア入力はAIに任せる一方で、機能の取捨選択、UXの判断、実機での確認、課金と広告の意思決定は自分でやりました。狙いは速さそのものではなく、仕様・テスト・レビューを省かずに速いことです。

最後に、公開はゴールではありませんでした。146か国に配信しているのに、米国のストアページでもアプリ名と説明文が日本語のまま表示されていることに、公開翌日の8月8日に気づきました。ASCに日本語ローカライズしか登録していなかったためで、製品ページの「Languages: English」はアプリ内の多言語対応を指していてストアページの言語ではない、という誤解が原因です。公開済みバージョンのメタデータは編集できないため、英語対応には新しいバージョンのビルドが必要になります。英語のスクリーンショットも撮り直しです。ここまで含めて、ひとつのリリースなのだと思いました。

よくある質問

Q. App Storeの審査でリジェクトされたら、また最初から審査待ちになりますか?

再提出すると審査キューに入り直しますが、私の場合は待ち時間が極端に伸びることはありませんでした。8月2日提出→8月4日却下、8月5日再提出→8月6日却下、8月6日再提出→8月7日承認で、各回1〜2日で結果が返っています。メタデータだけの修正なら再ビルドは不要で、説明文を直してそのまま再提出できます。ただしサブスクリプション商品が却下されると、アプリ本体もApp内課金もまとめて「却下済み」の状態になるため、全部そろえて再提出することになります。

Q. 自動更新サブスクを出すとき、利用規約(EULA)はどこに書けばいいですか?

Apple標準EULAを使う場合、App Store Connect(ストア掲載情報を入力する管理画面)には「利用規約URL」の入力欄が存在せず、あるのはカスタムEULAのアップロード欄だけです。そのため、アプリの説明文(App Description)の本文にURLを自分で書くのが唯一の手段になります。アプリ内のペイウォール(課金プランを提示する画面)に規約リンクを置いていても、ストアのメタデータ側になければガイドライン3.1.2で却下されます。私はこれで1回落ちたので、説明文の末尾にEULAとプライバシーポリシーのURL、そして自動更新の条件を書いておくことをおすすめします。

Q. ATTのダイアログが、コードで呼んでいるのに表示されません。何を疑えばいいですか?

まず要求のタイミングを疑ってください。ATT(App Tracking Transparency)のダイアログは、アプリの状態が active で、かつ他のUIのトランジション中でないときにしか表示されません。起動直後のスプラッシュから初回レンダーの間に呼ぶと、出ないまま結果だけが返ります。しかもこれはエラーにならず status は undetermined(未回答)のままなので、戻り値で「提示できたのか」を必ず判定する必要があります。結果をメモ化していると、一度すべっただけでそのセッション中は二度と要求されません。私は active になるまで待ってから要求し、undetermined なら待つ時間を延ばしながら最大5回まで試す形に変えて解決しました。

Q. 「ATTの許可要求が見つからない」と指摘されたら、コードを直すだけで通りますか?

私の場合、修正だけでは足りないと考えました。権限ダイアログは一度回答すると再インストールするまで出ないため、審査担当が回答済みの端末を使っていると、直っていても再現できないからです。そこで設定画面に手動でダイアログを呼べる導線を追加し、審査メモに再現手順(新規インストールが必要なこと、端末側の「Appからのトラッキング要求を許可」がオンである必要があること)を明記しました。そのうえで実機の画面収録をApp Review情報に添付し、原因と修正内容を英語で説明する返信を送って通っています。

Q. Macがなくても、個人でiOSアプリをリリースできますか?

できました。私はWindows PCとiPhone実機だけで公開まで到達しています。ExpoとEAS Build(Expoのクラウドビルドサービス)を使うとiOSのネイティブビルドがクラウド上で行われ、証明書やプロビジョニングプロファイルも対話形式の質問に答えるだけで自動生成されるので、署名まわりの手作業はありませんでした。ただしWindowsではローカルビルドがiOSに対応していないため、iOSのビルドは必ずクラウド経由になる点は押さえておく必要があります。

Q. 配信地域からEEA(欧州経済領域)を外したのはなぜですか?

理由は2つあります。1つは、AdMobでEEAに広告を配信するにはGoogle認定の同意管理プラットフォーム(UMP)による同意フォームが必要で、これが未実装だったこと。もう1つは、EUのDSA(デジタルサービス法)のトレーダー要件で、氏名や住所などの情報がEUのApp Store製品ページに公開表示されることです。個人開発では自宅住所の公開になってしまうため、最初のリリースでは配信しないと決めました。結果として配信は、全175地域からEEAの29地域を外した146か国です(EEAは30か国ですが、リヒテンシュタインはApp Storeの配信地域として存在しません)。どちらの条件も後から解除できるので、あとからEEAを追加することは可能です。