8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者でもApp Store審査が一発で通る】AIが"審査員"になってくれるアプリを作ったら、申請が怖くなくなった

8
Posted at

学校のコンテストで作ったiOSアプリを、いざApp Storeに出そうとして初めて「審査」という壁の存在を知りました。しかも審査待ちで完成が遅れる始末。その悔しさをそのままぶつけて、「審査に出す前に、AIに審査員役をやってもらう」アプリを約2ヶ月かけて個人開発し、リリースまで持っていった——という記録です。

ありがたいことに、これは自分にとって人生で初めて収益化できたプロダクトにもなりました。

こんな人に読んでほしい

  • 個人開発でアプリ/Webサービスをリリースまで持っていきたい人
  • Claude / LLM を「ちゃんと動くプロダクト」に組み込む現実的なやり方を知りたい人
  • App Storeの審査に一度でも泣いたことがある人

どんなアプリ?

  • App Storeの審査リジェクトを申請前にシミュレーションできるWebアプリ 「AppReview AI」 を個人開発してリリースしました
  • アプリ情報や Info.plist / .xcprivacy をアップロードすると、Claude(Anthropic API)がApple審査員になりきってリジェクトリスクを診断してくれます
  • 専門知識ゼロの初心者でも、申請前に「ここが引っかかる」が分かるので、一発合格に近づけます
  • スタックは Next.js 16 (App Router) + Supabase + Anthropic Claude API + Stripe + Vercel
  • 開発期間は約2ヶ月。個人開発で初めて「使われて、対価をもらう」ところまで通せたプロダクトです
  • 本番はこちら 👉 https://www.appreview-ai.jp

スクリーンショット 2026-05-24 1.56.29.png

▲ トップページ。「提出前に数秒で仮審査」をコンセプトに、Appleの審査官が見るチェック項目を自動スキャンする


なぜ作ったのか — 学校のコンテストで「審査」の壁にぶつかった

きっかけは完全に個人的な体験です。

学校のコンテストでiOSアプリを作りました。コードを書いて、動くものが完成して、「あとはApp Storeに出すだけ!」というところまで来て——そこで初めて知りました

App Storeに出すには、Appleの「審査」を通さないといけない。

正直、その時まで「作ってアップロードすれば誰でも使える」くらいに思っていました。せっかくなのでちゃんとリリースしようと審査に出したのですが、ここで2つ目の誤算がありました。

審査が終わるまで、思った以上に時間がかかる。

審査待ちのあいだ何もできず、結果として完成(公開)が予定よりずれてしまったんです。「審査の存在を知らなかった」「審査の流れや必須項目を知らなかった」——この知識のギャップだけで時間を損したのが、すごく悔しかった。

App Storeの審査は年々厳しくなっていて、初心者がいきなり全部把握するのは無理ゲーです。

  • アカウント削除機能(2022年6月〜、ログイン機能があれば必須 / 5.1.1(v))
  • Privacy Manifests(.xcprivacy(2024年5月〜義務化)
  • AI生成コンテンツのラベリング(2024年〜)
  • Sign in with Apple(サードパーティSSOがあるなら必須 / 4.8)
  • UGCのブロック・通報機能(投稿機能があれば必須 / 1.2)

これらを知らずに申請してリジェクトされると、1回のやり取りで1〜3日が溶けます。当時の自分のように、リリースがどんどん後ろにずれていく。

そこで思いました。

どうせなら、審査員に出す前に「審査員にレビューしてもらえる」ものを作ればいいのでは?

幸い、いまはガイドラインを丸ごと理解しているLLMがあります。Apple審査員の思考をシステムプロンプトに落とし込めば、当時の自分のような初心者でも、申請前にリジェクトリスクが分かる——そんなセルフ審査ツールが作れるはず。それで生まれたのが AppReview AI です。


何ができるアプリか

機能 内容
AI審査シミュレーション アプリ情報+plistを入力すると、AIがApple審査員の視点でリジェクトリスクを判定(pass / warning / reject+スコア0-100)
テクニカルスキャン Info.plist / .xcprivacy / .entitlements を解析し、必須キー不足や曖昧な権限説明を機械的に検出
AI審査コンサルタント 審査に関する疑問をチャットで相談(現在審査中のアプリ情報をコンテキストに保持)
プライバシーポリシー自動生成 Appleが受理しやすい形式で自動生成
審査メモ自動生成 App Store Connectの「審査に関するメモ」を日英で生成

実際の審査結果はこんな画面で返ってきます。

S__53952550.jpg

▲ 審査結果画面(自作アプリ「エモナル」をテスト審査したもの)。合格確率・チェックサマリー(即修正/要注意/合格)・テクニカルスキャン結果を一画面で表示し、各項目に「なぜダメか+どう直すか」が付く

テキストにするとこんなイメージです。

総合スコア: 64 / 100   判定: ⚠️ warning(修正してから申請推奨)

❌ リジェクト濃厚
  ・ログイン機能があるのに「アカウント削除」導線が見当たりません
    → ガイドライン5.1.1(v)違反。設定内に削除ボタンとデータ削除フローが必須

⚠️ 警告
  ・NSCameraUsageDescription が「アプリのために使用します」と曖昧
    → 用途を具体的に(例:「プロフィール写真の撮影に使用します」)

✅ 合格
  ・プライバシーポリシーURLが有効(HTTPS / 200 OK)

「ダメ」だけでなく なぜダメか+どう直すか まで返すのを徹底しました。当時の自分が一番ほしかったのがこれだったからです。

料金は 無料で2回まで → ¥980の買い切りで永久無制限。サブスクではなく買い切りにしたのは、「申請前に何度か使いたいだけ」というユースケースにサブスクは重いと判断したからです。


技術スタック

Frontend / Backend : Next.js 16 (App Router, React 19)
UI                 : Tailwind CSS v4 + shadcn/ui + framer-motion
Auth / DB          : Supabase (@supabase/ssr)
AI                 : Anthropic Claude API (@anthropic-ai/sdk)
決済                : Stripe (PaymentIntent + Webhook)
plist解析           : xml2js
Hosting            : Vercel

構成自体は「個人開発で最後まで一人で出し切れること」を最優先に選びました。普段から触っていて、認証・DB・決済・ホスティングまで個人で完結できるのがこの組み合わせだったからです。

ただ、ひとつだけ完全に油断していたのが Next.js 16 でした。「いつものNext.jsでしょ」と思って書き始めたら、App Routerまわりの作法や型が記憶と違っていて、node_modules の中のドキュメントを読みながら書く羽目に。新しいバージョンに飛びつくとこういう"自分の知識が古い"問題に普通にぶつかる、というのを久しぶりに味わいました。

以下、作っていて特に頭を使った(=ハマった)ところを3つ書きます。ここが個人開発の本編です。


全体の仕組み(コード × AI のハイブリッド)

細かい実装に入る前に、審査の流れだけ図にしておきます。このアプリの肝は 「確実に判定できるものはコードで、グレーゾーンはAIで」 という役割分担です。

ポイントは、どこで何が壊れても必ず最後の「判定」までたどり着くように作っていること。AIが落ちても、JSONが壊れても、ユーザーには必ず結果が返ります。理由は次の②で書きます。


詰まった① Apple審査員に「なりきらせる」システムプロンプト設計

このアプリの心臓部は、Claudeに渡すシステムプロンプトです。

最初は正直ナメていて、「このアプリ、App Store審査通りそう?」くらいの雑な指示を投げていました。結果は当然ふわふわで、passreject かが毎回ブレるし、指摘も「気をつけましょう」レベルで使い物にならない。これでは自分が当時欲しかったツールになりません。

そこで方針を変えて、実際のガイドライン番号・義務化時期・頻出リジェクト事例を構造化してシステムプロンプトに丸ごと埋め込みました。「審査員という役」を演じさせるのではなく、「審査員が持っている知識」を渡すイメージです。

const SYSTEM_PROMPT = `あなたはAppleのApp Store審査チームのシニアレビュアーです。
2026年最新のApp Store Review Guidelinesに完全に精通しており、実際のリジェクト事例も豊富に知っています。

【2026年重要ガイドライン(セクション番号付き)】

■ アカウント・認証
5. アカウント削除機能 [ガイドライン5.1.1 (v) - 2022年6月必須化]
   - ログイン機能があれば「アカウント削除」のUIが必須
   - 削除ボタンをわかりにくい場所に隠してはいけない
...

【実際のリジェクト事例(頻出)】
- "アカウント削除ボタンは設定の奥深くにあり見つかりにくい" → リジェクト
- "プライバシーポリシーURLが404エラー" → 即リジェクト
- "Sign in with Google はあるが Sign in with Apple がない" → リジェクト
...

【審査判定基準】
- pass (合格): すべての必須要件を満たし、ガイドラインに準拠
- warning (要注意): 修正推奨だが現時点ではリジェクトにならない可能性が高い
- reject (リジェクト濃厚): リジェクトの可能性が非常に高い問題が1件以上ある`

ポイントは3つ。

  1. 役割を明確に固定する(「シニアレビュアー」)。曖昧な役割だと出力もブレる
  2. 判定ラベルとスコアリング基準を定義に含めるpass/warning/reject の境界をプロンプト内で言語化しておくと、出力の一貫性が段違いに上がる
  3. 「なぜリジェクトされるか」+「具体的な修正方法」を必ず書かせる。ユーザーが次に取るべきアクションまで出すのが体験のキモ

さらに、ユーザーが入力した「機能フラグ」に応じて、プロンプト側で条件分岐して必須チェック項目を動的に差し込んでいます。

${formData.hasLogin ? '・ログイン→アカウント削除必須(5.1.1)・Sign in with Apple必須(4.8)' : ''}
${formData.hasUgc ? '・UGC→ブロック/通報UI必須(1.2)' : ''}
${formData.hasAiFeatures ? '・AI→ラベリング必須' : ''}
${formData.targetAge === '4+' && formData.hasUgc ? '🚨4+レーティング+UGC→禁止(1.3)' : ''}

「該当しそうな論点だけをコンテキストに乗せる」ことで、トークンを節約しつつ判定の見落としを減らしています。


詰まった② JSON出力が普通に壊れる(モデルフォールバックで耐える)

ここが一番ハマりました。

AIに審査結果を画面に出すには、構造化データ(JSON)で返してもらう必要があります。ところが**「JSONで返して」とお願いしても普通に壊れる**。コードブロックの ```json が付いてきたり、JSONの前後に「承知しました、以下が結果です」みたいな前置きが混ざったり。最初はこれで JSON.parse が落ちまくって、テスト中に何度も画面が真っ白になりました。

開発中に「ユーザーがお金を払って審査ボタンを押したのに真っ白」が一番怖いので、ここは2段構え+多段フォールバックで徹底的に守りました。

1段目:プロンプトで出力スキーマを固定

JSONのみ返してください(コードブロックなし):
{"metadataResults":[...],"overallScore":0-100,
 "verdict":"pass"|"warning"|"reject",
 "criticalIssues":[...],"warnings":[...],
 "passedChecks":[...],"recommendations":[...]}

2段目:パース側で防御的に処理

const cleaned = text.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim()
const jsonMatch = cleaned.match(/\{[\s\S]*\}/)   // 前後の余計な文章を捨てて { ... } だけ抜く
if (jsonMatch) {
  const parsed = JSON.parse(jsonMatch[0])
  // ... 各フィールドに || デフォルト値 を必ず噛ませる
}

それでもAPIが落ちる・タイムアウトする可能性があるので、複数モデルのフォールバックを実装しています。

const FALLBACK_REVIEW_MODELS = [
  'claude-haiku-4-5-20251001',  // まず速くて安いモデルで
  'claude-sonnet-4-6',          // ダメなら賢いモデル
  'claude-opus-4-7',            // 最後の砦
]

export async function createReviewCompletion(prompt: string) {
  const errors: string[] = []
  for (const model of REVIEW_MODELS) {
    try {
      const response = await client.messages.create({
        model, max_tokens: 1800, system: SYSTEM_PROMPT,
        messages: [{ role: 'user', content: prompt }],
      }, { signal: AbortSignal.timeout(18000) })   // 18秒でタイムアウト
      const text = /* テキストブロックを連結 */
      if (!text) throw new Error('empty response')
      return { text, model }
    } catch (error) {
      errors.push(`${model}: ${error.message}`)   // 失敗したら次のモデルへ
    }
  }
  throw new Error(errors.join(' | '))
}

安いモデルから順に試し、失敗したら賢いモデルに昇格」というのは、コストと可用性のバランスが良くて気に入っています。さらにパースが完全に失敗した場合でも、テクニカルスキャンの結果だけでローカル採点するフォールバックを用意して、ユーザーには必ず何らかの結果が返るようにしています。

// AIが完全にコケてもローカルで採点
const score = Math.max(0, Math.min(100, 100 - criticalCount * 20 - warningCount * 5))
verdict: criticalCount > 0 ? 'reject' : warningCount > 3 ? 'warning' : 'pass'

💡 これはApp Storeに限らず、LLMに構造化データ(JSON)を返させるアプリ全般で使える"型"です。

  1. スキーマをプロンプトで固定する
  2. 返ってきたテキストから正規表現で { ... } だけ抜く
  3. モデルを多段フォールバックさせる(安い→賢い)
  4. 最後はコードで決定的に値を出す

この4段を噛ませるだけで、LLMの"気まぐれ"をプロダクトの信頼性に変換できます。LLMを組み込むときは毎回これを思い出すようになりました。


詰まった③ 「AIに全部任せる」を諦めて、半分は自分で書いた

当初は「全部AIに審査させればよくない?」と思っていました。でも試すうちに、AIは確実に存在をチェックすべき項目(plistの必須キーなど)をたまに見逃すことが分かってきました。審査ツールとして「たまに見逃す」は致命的です。

そこで割り切って、機械的に判定できるものは自分でコードを書いて確実に検出することにしました。xml2js で plist をパースし、必須キーや権限説明の有無をチェックします。

// 2026年必須のプライバシー説明キー(一部)
const REQUIRED_USAGE_DESCRIPTIONS = {
  NSCameraUsageDescription: 'カメラへのアクセス',
  NSLocationWhenInUseUsageDescription: '使用中の位置情報',
  NSUserTrackingUsageDescription: 'ユーザートラッキング (ATT)',
  NSHealthShareUsageDescription: 'ヘルスデータの読み取り',
  // ...
}

// Privacy Manifestで必要なキー
const PRIVACY_MANIFEST_REQUIRED_APIS = [
  'NSPrivacyAccessedAPITypes',
  'NSPrivacyCollectedDataTypes',
  'NSPrivacyTrackingDomains',
  'NSPrivacyTracking',
]

そして「.xcprivacy がアップロードされていない」こと自体を error として返します。

if (!xcprivacyFile) {
  allChecks.push({
    checkName: 'Privacy Manifest (.xcprivacy)',
    status: 'error',
    message: 'Privacy Manifestファイルが見つかりません。2024年5月以降、すべてのアプリで必須です。',
    fixSuggestion: 'Xcodeで「File > New > File > App Privacy」から PrivacyInfo.xcprivacy を作成してください。',
  })
}

確定的な判定はコードで、グレーゾーンの判断はAIで と役割分担することで、「AIが見落とした必須項目」を機械側が拾えるようにしています。これがハイブリッドにしている一番の理由です。

なお、アップロードファイルは 1MB上限でバリデーションし、Info.plist のような小さな構成ファイルだけを想定しています。


お金まわり:個人開発で「買い切り」にした理由

正直、課金を入れるかは最後まで迷いました。でもAIの審査1回ごとにAPIコストがかかる以上、無料で無制限にすると自分の財布が死にます。かといってサブスクは、解約・再開・失効の状態管理を一人で抱えるのがしんどい。

落としどころが 無料2回 → ¥980の買い切り でした。「申請前に何回か使いたいだけ」というこのアプリの使い方に、月額はそもそも合わないという判断もあります。

無料利用回数の管理はSupabase側で行い、APIルートの先頭で必ずチェックを挟みます。

const access = await checkUserAccess()
if (!access.allowed) {
  if (access.reason === 'UNAUTHENTICATED')
    return NextResponse.json({ error: 'ログインが必要です' }, { status: 401 })
  return NextResponse.json({ error: '無料トライアル終了', code: 'TRIAL_ENDED' }, { status: 402 })
}

402 Payment Required を返してフロント側で課金モーダルを開く、という流れです。決済は Stripe の PaymentIntent + Webhook で、Webhookを受けてSupabaseのユーザーをプレミアムに昇格させています。買い切りなのでサブスクの状態管理がいらず、実装はかなりシンプルになりました。


個人開発で作ってみて、自分が学んだこと

一人で最後まで作って出してみて、特に「次もこうしよう」と思ったのはこのあたりです。

  • AIに全部任せようとしない。全部AIだと見落とすし、全部ルールベースだと保守が地獄。「確実な判定はコード、グレーゾーンはAI」の線引きを決めた瞬間に設計がスッと固まった
  • システムプロンプトはコードと同じく"育てる資産"。ガイドライン番号と義務化時期を書き溜めておくと、モデルを差し替えても精度が落ちにくい。プロンプトをちゃんとバージョン管理する意識が芽生えた
  • 「真っ白な画面」を出さない設計に一番時間をかける価値がある。モデル多段 → ローカル採点、と最後まで落ちていく作りにしたら、心理的にめちゃくちゃ楽になった
  • 新しいバージョンに飛びつくと普通にハマる(Next.js 16)。でもその"ハマり"も含めて個人開発は楽しい

そして何より、「自分が困ったこと」をそのまま作ると、最後までモチベが切れないというのが一番の学びでした。審査で時間を溶かした悔しさが、そのまま完成まで走るエネルギーになりました。


リリース後:人生で初めて「自分の作ったもの」でお金をもらえた

このアプリ、約2ヶ月かけて作りました。学校・コンテストの体験から生まれたものを、ちゃんと世に出るプロダクトとして仕上げるまで、地味に長かったです。

そしてリリース後、実際にいろんな人に使ってもらえて、買い切り(¥980)を購入してくれる人が出ました

これが自分にとって人生で初めての収益化でした。

正直、金額の大小よりも「自分が困って作ったものを、他人もお金を払うほど必要としてくれた」という事実が嬉しかったです。Stripe のダッシュボードに最初の決済が表示されたときのことは、たぶんずっと覚えていると思います。個人開発を続けるモチベーションとして、これ以上のものはありませんでした。

「作っただけ」で終わらず「使われて、対価をもらう」ところまで一人で通せたのは、自分の中でかなり大きな一歩になりました。


正直、まだ課題だらけ(これからやりたいこと)

良いことばかり書きましたが、正直まだ「完成」とは全く思っていません。自分用のメモも兼ねて、課題を残しておきます。

  • 判定精度の検証がこれから。理想は「このアプリで合格判定 → 実際の審査も一発合格」の相関を、実データで地道に確かめていくこと。いまはまだガイドライン知識ベースの"推定"です
  • .ipa を直接読めるようにしたい。今は plist などを個別にアップロードする方式だけど、ビルド成果物を丸ごと解析できれば手間がぐっと減るはず
  • ガイドライン改定への追従。Appleのガイドラインは更新され続けるので、システムプロンプトをどう運用で最新化していくかは継続課題
  • 英語UI対応。審査で困っているのは日本人だけではないはず

このあたり、「自分ならこう作る」という意見があればぜひコメントで教えてください。むしろそれが聞きたくてこの記事を書いている面もあります。


さいごに

App Storeの審査は「知っているかどうか」だけで結果が変わってしまう世界です。コンテストの時の自分は、その"知らなかった"だけで時間を失いました。その悔しさをぶつけて作ったのがこのアプリです。

もし同じように「審査の存在をいま知った」「何を直せばいいか分からない」という人がいたら、申請前に一度通してみてください。当時の自分が一番ほしかったツールのつもりで作りました 👇

🔗 AppReview AI : https://www.appreview-ai.jp

個人開発って、「自分が困ったこと」を見つけられた時点で半分勝ちだと思っています。残りの半分は、それを途中で投げずに世に出すこと。今回その両方を、初めて最後まで通せました。

この記事が、いま何かを作っている誰かの「最後まで出す」の後押しになれば嬉しいです。設計のツッコミ・質問はコメントで大歓迎します!

※ 本アプリの判定はあくまでシミュレーションであり、実際のApp Store審査の結果を保証するものではありません。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?