🍎

Build Log #3:美容アプリのApp Store申請でリジェクトされた全記録

に公開

美容系のパーソナルカラー診断アプリを App Store に申請したら、3回連続でリジェクトを食らった。3回目で通った。

リジェクト理由は全部「あー、それ確かにApple怒るよな」って内容で、申請前に潰せたものばかり。同じ罠踏む人を1人でも減らすために全部書く。

このシリーズは Build Log として、俺が運用してるSaaSの開発ログを全部書く。前回(#2)は LINE 受付AIを1日で作って即営業した話だった。今回は B2C アプリの「Apple審査」というプラットフォームの壁の話。

note 版:https://note.com/mintototo1/n/n19d935eadf4a


数字パネル

・申請→初回リジェクトまで:48時間
・リジェクト総数:3回
・最終承認まで:12日
・対応に溶かした時間:合計18時間
・本人スキル:iOS開発初心者、Capacitor + WebView 構成
・原因の99%:Appleガイドラインを読み込まずに突っ込んだ俺の怠慢


Reject #1(Guideline 5.1.1 Privacy):アカウント削除導線がない

App Store Review Guideline 5.1.1:ユーザーがアプリ内で 自分のアカウントを削除できる導線が必須。サインインさせるアプリはほぼ全部該当する。

俺のアプリは「メアドサインアップ → 診断 → 結果保存」の構造。アカウント削除画面を作ってなかった。

// 設定画面に「アカウント削除」セクションを追加
struct AccountSettingsView: View {
  @State private var showDeleteAlert = false

  var body: some View {
    Section(header: Text("アカウント")) {
      Button(role: .destructive) {
        showDeleteAlert = true
      } label: {
        Text("アカウントを削除")
      }
    }
    .alert("アカウントを削除しますか?", isPresented: $showDeleteAlert) {
      Button("削除", role: .destructive) { Task { await deleteAccount() } }
      Button("キャンセル", role: .cancel) { }
    } message: {
      Text("この操作は取り消せません。診断履歴とアカウント情報がすべて消えます。")
    }
  }

  func deleteAccount() async {
    // Supabase auth.admin.deleteUser を呼ぶ Edge Function に向ける
    await api.delete("/api/account/delete")
    await Auth.shared.signOut()
  }
}

サーバー側(Supabase Edge Function):

// supabase/functions/account-delete/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

Deno.serve(async (req) => {
  const authHeader = req.headers.get('Authorization')!;
  const userClient = createClient(SUPABASE_URL, SUPABASE_ANON, {
    global: { headers: { Authorization: authHeader } }
  });
  const { data: { user } } = await userClient.auth.getUser();
  if (!user) return new Response('Unauthorized', { status: 401 });

  // service_role で削除(auth.admin は service_role 必須)
  const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE);
  await admin.from('profiles').delete().eq('id', user.id);
  await admin.auth.admin.deleteUser(user.id);

  return new Response(JSON.stringify({ ok: true }));
});

学び:サインインさせるアプリには アカウント削除導線が義務。後付けすると審査が止まる。最初から組み込む。


Reject #2(Guideline 2.1):Apple Sign In がない

Apple Sign In は「他社のソーシャルログイン(Google / X / LINE / etc)を提供する場合は、Apple Sign In も同等以上の位置に提供しろ」というルール。

俺は Google Sign In だけ実装してた。Apple Sign In なし。落ちた。

実装:

import AuthenticationServices

struct SignInView: View {
  var body: some View {
    VStack(spacing: 12) {
      // Apple Sign In を Google より上に配置(同等以上に)
      SignInWithAppleButton(
        onRequest: { request in
          request.requestedScopes = [.fullName, .email]
        },
        onCompletion: { result in
          switch result {
          case .success(let auth):
            handleAppleAuth(auth)
          case .failure(let error):
            print(error)
          }
        }
      )
      .frame(height: 50)

      // Google Sign In
      Button(action: { /* google flow */ }) {
        Text("Googleで続ける")
      }
    }
  }

  func handleAppleAuth(_ auth: ASAuthorization) {
    guard let cred = auth.credential as? ASAuthorizationAppleIDCredential,
          let token = cred.identityToken,
          let tokenStr = String(data: token, encoding: .utf8) else { return }
    Task {
      await Auth.shared.signInWithApple(idToken: tokenStr)
    }
  }
}

Supabase 側で Apple Provider を有効化する設定も必要:

# Supabase Dashboard → Authentication → Providers → Apple → Enable
# 必須項目:
# - Services ID(Apple Developer で作成、Bundle ID とは別)
# - Team ID
# - Key ID
# - Private Key(.p8 ファイルの中身)

学び:他社ソーシャルログイン入れるなら、必ず Apple Sign In も。位置も同等以上(同じ目立ち方)。


Reject #3(Guideline 5.1.1 Privacy Manifest):Privacy Manifest がない

2024年5月から「Privacy Manifest(PrivacyInfo.xcprivacy)」が必須化された。アプリが収集するデータ種別と、Required Reason API の使用理由を明記する Plist。

特に第三者 SDK(Firebase / RevenueCat / Sentry / etc)にも独自の PrivacyInfo.xcprivacy が必要で、Xcode が依存解決時に統合する。

俺は何も入れてなかった。リジェクト。

PrivacyInfo.xcprivacy(プロジェクトルート直下):

<?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>NSPrivacyCollectedDataTypes</key>
  <array>
    <dict>
      <key>NSPrivacyCollectedDataType</key>
      <string>NSPrivacyCollectedDataTypeEmailAddress</string>
      <key>NSPrivacyCollectedDataTypeLinked</key>
      <true/>
      <key>NSPrivacyCollectedDataTypeTracking</key>
      <false/>
      <key>NSPrivacyCollectedDataTypePurposes</key>
      <array>
        <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
      </array>
    </dict>
  </array>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>CA92.1</string>
      </array>
    </dict>
  </array>
  <key>NSPrivacyTracking</key>
  <false/>
</dict>
</plist>

「Required Reason API」のリストは Apple ドキュメントに載ってる。UserDefaults / FileTimestamp / SystemBootTime / DiskSpace の4カテゴリで、それぞれ「使ってもいい理由コード(CA92.1 とか)」を書く。

学び:2024年以降の iOS 申請は PrivacyInfo.xcprivacy 必須。アプリ本体 + 第三者 SDK 両方分。


Reject されなかったが指摘された点(Metadata)

リジェクトじゃなく Metadata Rejection(軽い差し戻し)だったが、3回目の申請で食らった指摘:

  1. App Privacy(プライバシーラベル)の不一致
    App Store Connect で申告したデータ種別と、PrivacyInfo.xcprivacy / 実装が一致してないと差し戻し。
    俺は「Email Address: 収集しない」と申告してたのに、Supabase 経由で email を保存してた。即修正。

  2. デモアカウントの提供義務
    サインインが必要なアプリは、審査用にデモアカウントを App Review Information の「Sign-In Information」に書く必要がある。書いてなかった。

    Username: appreview@example.com
    Password: AppReview2026!
    
  3. スクリーンショットの言語不一致
    日本語アプリなのに英語のスクショを貼ってた。日本語スクショに差し替え。


何が原因で3回も食らったか

正直に書く。Apple のガイドラインを App Store Review Guideline ページしか読んでなかった。実際の申請に効くのは:

  • App Store Review Guidelines(基本)
  • Human Interface Guidelines(UI/UX)
  • App Store Connect Help(メタデータ)
  • Privacy Manifest specifically(2024〜)
  • Accessibility Programming Guide(軽くチェック)

俺はガイドラインの目次だけ流し読みしてた。実装に入る前に チェックリスト化してれば3回目まで行かなかった。


確定ルール(保存推奨。iOS 申請の初日チェックリスト)

  1. アカウント削除導線を 設定画面に必ず実装(5.1.1)
  2. Apple Sign In を 他ソーシャルと同等以上の位置に配置(2.1)
  3. PrivacyInfo.xcprivacy を アプリ本体 + 第三者 SDK 全部に用意(5.1.1)
  4. App Privacy(プライバシーラベル)と PrivacyInfo の データ種別を完全一致させる
  5. App Review Information に デモアカウント を必ず記載
  6. スクリーンショットは アプリの言語と一致(日本語アプリなら日本語スクショ)
  7. Required Reason API(UserDefaults / FileTimestamp / SystemBootTime / DiskSpace)の 理由コードを PrivacyInfo に書く
  8. ATT(App Tracking Transparency)を使う場合は NSUserTrackingUsageDescription を Info.plist に書く
  9. データ収集なし= NSPrivacyTracking: false、収集ありなら必ず NSPrivacyCollectedDataTypes を埋める
  10. 申請前に App Review Guidelines + Privacy Manifest + Human Interface Guidelines を チェックリスト化 して機械的に確認

このリストは俺が次の iOS アプリの Day 1 で見返すために書いた。コピペして lessons.md に貼っとけば、リジェクトを2回減らせる。


このシリーズは続く。次の記事は「会社サイトを30分で作って Apple 審査通した手順」を書く。会社情報の Web ページが Apple 審査で求められる場面の話。
保存しといて、明日からの自分に見せて。

#BuildInPublic #ClaudeCode #個人開発 #iOS #AppStore

俺が運営してるプロダクト

🎬 VideoTracker — 不動産業者向け動画自動生成 SaaS
動画1本¥596。問合せ倍率の想定値はシミュレーションで2.8倍(実測は検証中)。
https://komugi-ai.jp/realestate

🤖 Mint Agent — Slack で @AI に話しかけて業務代行(近日リリース)
議事録投稿・メール返信・データ集計が Slack 内で完結
→ ベータ Waitlist:https://agent.komugi-ai.jp

業務効率化・SaaS 開発相談 → X DM @mintnekoneko0
過去記事まとめ:https://note.com/mintototo1

Discussion