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回目の申請で食らった指摘:
-
App Privacy(プライバシーラベル)の不一致
App Store Connect で申告したデータ種別と、PrivacyInfo.xcprivacy / 実装が一致してないと差し戻し。
俺は「Email Address: 収集しない」と申告してたのに、Supabase 経由で email を保存してた。即修正。 -
デモアカウントの提供義務
サインインが必要なアプリは、審査用にデモアカウントを App Review Information の「Sign-In Information」に書く必要がある。書いてなかった。Username: appreview@example.com Password: AppReview2026! -
スクリーンショットの言語不一致
日本語アプリなのに英語のスクショを貼ってた。日本語スクショに差し替え。
何が原因で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 申請の初日チェックリスト)
- アカウント削除導線を 設定画面に必ず実装(5.1.1)
- Apple Sign In を 他ソーシャルと同等以上の位置に配置(2.1)
- PrivacyInfo.xcprivacy を アプリ本体 + 第三者 SDK 全部に用意(5.1.1)
- App Privacy(プライバシーラベル)と PrivacyInfo の データ種別を完全一致させる
- App Review Information に デモアカウント を必ず記載
- スクリーンショットは アプリの言語と一致(日本語アプリなら日本語スクショ)
- Required Reason API(UserDefaults / FileTimestamp / SystemBootTime / DiskSpace)の 理由コードを PrivacyInfo に書く
- ATT(App Tracking Transparency)を使う場合は
NSUserTrackingUsageDescriptionを Info.plist に書く - データ収集なし= NSPrivacyTracking: false、収集ありなら必ず NSPrivacyCollectedDataTypes を埋める
- 申請前に 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 ビルド)
-
xcodebuildRelease 構成で 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