0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

0
Last updated at Posted at 2026-05-04

note 版:https://note.com/mintototo1/n/n19d935eadf4a
Zenn 版:https://zenn.dev/mintototo1/articles/buildlog-03-app-store-reject

美容系のパーソナルカラー診断アプリを 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

明日からコピペで使えるチェックリスト:App Store 起動クラッシュ防止

提出前 必須チェック(Release ビルド)

  • xcodebuild Release 構成で archive → IPA 生成
  • シミュレータに Release IPA を install(Debug ではダメ、本番再現性が違う)
  • xcrun simctl launch <udid> <bundle-id> で起動 → 6 秒生存確認
  • 6 秒で死んだら xcrun simctl spawn <udid> log stream --predicate 'process == "App"' でクラッシュログ取得

起動クラッシュの典型原因(リジェクト 2.1.0 ルート全部)

  • AdMob の GADApplicationIdentifier が Info.plist に無い(GoogleMobileAds が +load で abort)
  • StoreKit 初期化が applicationDidFinishLaunching で例外を投げる(try/catch で守る)
  • Capacitor / RN ブリッジの初期化失敗(プラットフォーム固有 plist キー)
  • env / API key が baked されてない(dev で動いて本番で undefined 参照)
  • Privacy Manifest (PrivacyInfo.xcprivacy) 不在で iOS 17+ がブロック

Privacy / 同意系(Apple は 2.1.0 と 5.1.x 両方で殺す)

  • NSCameraUsageDescription 等の *UsageDescription を全 SDK 分追加
  • ATT (App Tracking Transparency) を実装してから IDFA 取得
  • 子供向け表記する場合は データ収集ゼロ宣言が必須

リジェクト食らったあとの動き

  • Resolution Center のメッセージを 全文コピーして Apple に「再現できなかった」とは絶対言わない
  • 必ず再現環境を作って fix を証明してから再提出
  • Build number を bump(同じ番号は ASC が拒否)
  • 再提出は 24-48h で再審査が走る(同じ違反だと連続 reject で警告フラグ)

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

🎬 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

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?