提出前ゲート 〜赤くならなかった一行〜
実機検証: Mac Studio(Apple Silicon M2 Max / 32GB)で運営者が実行・確認しています。体制
本ページはアフィリエイト広告・Google AdSense(プロモーション)を含みます。詳細
目次
朝の6時16分、クライアントから届いたのは6文字だった。
tanikojoの続き
単位変換工場という名前のアプリのこと。数値を投入すると工場の製造ラインを通って別の単位で出てくる、小学生向けの教育ゲームで、そのときは審査に出したまま結果を待っていた。ビルド番号は7。1から数えて7回、何かを直しては出し直している。
続き、と言われて AI が最初に開いたのは、アプリのコードではない。提出前に走らせる検査のほうだった。
誰も設計しなかった道具
その検査は preflight.py という1本のファイルで、iOS アプリの雛形を置いたディレクトリに1本だけ転がる。npx expo prebuild が吐き出した Info.plist と app.json を突き合わせ、条件に一つでも当たれば exit 1 で止まる。
このファイルには、設計思想と呼べるものがない。先頭にこう書いてある。
NG の根拠はすべて 2026-08 のさんすうおじさん/バイナリーハンズ/時計を学ぼうの実例。
却下されるたびに検査が1本ずつ足された。図面が先にあったのではなく、痛い目に遭った順に積み上がって、気づけば18本の検査を持つファイルになった。
冒頭には空の集合がひとつ置いてある。
# ここに無い UsageDescription が Info.plist にあれば落とす。
# expo-av / expo-audio の config plugin がマイク権限を勝手に注入し、
# 録音しないアプリが Guideline 5.1.1 で自動却下された(2026-08-19 さんすうおじさん)。
ALLOWED_USAGE_KEYS: set[str] = set()
録音しないアプリに、ライブラリが勝手にマイクの権限を書き込む。そのまま出せば、人間の審査員に届く前に自動で弾かれた。以来「実際に使う権限だけを明示的に許可リストへ足す」という運用になり、空集合はその名残。何も許可していない、という意思表示。
他の検査も似た出自を持つ。
広告ユニットIDを見る検査は、アプリIDとの取り違えから生まれた。8月27日、単位変換工場の設定ファイルに、区切りが ~ のアプリIDを貼ってしまう。広告は1つも出ない。それでも審査は通る。 気づくとしたらリリース後、収益がいつまでもゼロなのを眺めるとき。Info.plist からは見えないので、検査はソースファイルを直接読みにいく。
掲載文の字数を数える検査は、英訳で伸びたぶんに気づけなかったところから来ている。8月30日、単位変換工場の英語プロモーションテキストが239字。上限は170字。和文は132字で収まっていたので、英語にした時点で69字はみ出していた。App Store Connect に貼るまで誰も数えず、最後はクライアントが数えて発覚する。
掲載用スクショの寸法を測る検査は、6.9インチで撮った1320×2868を「6.5インチ」の枠に入れて 寸法が正しくありません で止まった日の産物。枠ごとに寸法が違う、という一行を検査の形にしたもの。
掲載文が英語かどうかを見る検査には、8月29日の日付が入る。単位変換工場は配信地域に北米を入れ、アプリの中も日本語と英語の両方を出すようにしてあった。掲載シートだけが日本語のまま。英語圏のストアには読めない説明が並ぶ状態で、これもクライアントに指摘されるまで誰も気づかなかった。
対応端末を確かめる検査は、少し毛色が違う。prebuild を通した直後の Info.plist には UIDeviceFamily が書かれていない。Expo はそこに書かず、Xcode 側の TARGETED_DEVICE_FAMILY で決まる仕組みで、IPA になった段階で初めて UIDeviceFamily として現れる。見る場所を間違えると、正しく設定してあっても「無い」と読める。 それで検査は pbxproj を読みにいく。
18本は、そういう日の集まりだった。
止めるものと、止めないもの
このファイルには、fail() と warn() の2つがある。fail は末尾で exit 1 を引き起こして提出を止める。warn は画面に「注意」として並ぶだけで、何も止めない。
どちらに落とすかの基準は、前日に書かれた。9月1日、SŌBAN というそろばんの練習アプリで、スクショを撮ったあとに画面を作り替えた事件があった。5つの段ぶん画面が変わったのに、寸法の検査は OK を出し続ける。寸法しか見ていないので当然で、そのとき提出されようとしていたのは「もう存在しない画面」の写真だった。
そこで足された検査には、こう書いてある。
`fail` にはしない。**撮り直しが要るかは中身を見ないと決まらない**
(文言の直しだけなら撮り直さなくてよい)。判定が一意に決まらないものは `warn`。
判定が一意に決まらないものは warn。線引きとしては筋が通る。この一文が翌日どう効くかは、まだ誰も知らない。
18本目
その朝に足されたのは、掲載する URL を実際に叩く検査だった。
審査に出すときには、サポートURL、マーケティングURL、プライバシーポリシーURLを書く欄がある。どこに何を書くかは appstore-listing.md という掲載シートにまとめてあり、提出のたびにそこから転記する。書いてあるかどうかは、目で見れば分かる。開くかどうかは、誰も確かめてこなかった。
check_listing_urls() は掲載シートから URL の行を拾い、curl で叩いて %{http_code} を読む。200 なら OK、それ以外は NG で提出を止める。
書き終えて、単位変換工場に対して回す。3分もかかっていない。
OK サポートURL が開く(https://kuroto-blog.pages.dev/about/)
NG マーケティングURL が開かない: HTTP 404 — https://kuroto-blog.pages.dev/apps/tannikojo/
NG プライバシーポリシーURL が開かない: HTTP 404 — https://kuroto-blog.pages.dev/apps/tannikojo-privacy/
不合格 2 件。直してから提出する。
審査に出したビルドが、存在しないページを2枚指している。
書いた、と、公開されている
404 の理由を追うと、話はアプリの外に出る。
2枚のページの原稿は、確かに書いてあった。ブログのリポジトリのなかに、ファイルとして存在した。ただし git の管理下に入っていない。commit されていないので push もされず、当然デプロイもされない。ローカルのディスクの上だけで完成した原稿。
原稿がある、という状態を、公開されている証拠として扱ってしまう。手元で開けるのだから外からも開けるだろう、と。手元で開けることと外から開けることのあいだには、commit と push とデプロイが挟まる。 その3つが抜けていた。
プライバシーポリシーが開かないのは Guideline 5.1.1 の却下事由。マーケティングURLが開かなければ、さんすうおじさんが8月に4日溶かしたのと同じ道に入る。あのときは AdMob のアプリ確認が通らず、しかも「配信準備完了」の状態に入るとメタデータがロックされて後から直せなかった。どちらの穴も、404 のままでは埋まらない。
ページを公開する作業のついでに、AI はアプリの実装とページの文面を突き合わせた。ここで2件、書いてあることが実物と食い違う。広告が出る画面を1画面と書いていたが、実際には3画面。使っている書体を同梱していると書いていたが、同梱していない。どちらも読んだ人が誤解する種類の間違いで、直してから出した。
同じ commit では、SŌBAN のプライバシーポリシーのページも一緒に出した。こちらは同梱している配布書体の利用条件を1文足した。3枚のページのうち2枚は単位変換工場の紹介とプライバシーポリシー、残りの1枚が SŌBAN のぶん。
公開は6時21分。検査を書いてから3分後に、その検査が指した穴が塞がる。本番のURLを叩き直し、3枚とも200。ついでに app-ads.txt も200のままであることを確かめた。8月にさんすうおじさんの AdMob が止まったとき、原因はこのファイルの不在だった。広告の在庫は、ドメインに置いた1行のテキストで身元を証明する。
三分後に届いた別件
6時24分、クライアントがメールの本文を貼ってくる。Apple から来ていた。
Thank you for submitting the new app, 単位変換工場, for review.
We noticed some issues that require your attention.
Review Device: iPad Air 11-inch (M3)
Version reviewed: 1.0.0 (7)
Guideline 4 - Design
Parts of the app's user interface were crowded, laid out, or displayed
in a way that made it difficult to use the app when reviewed on
iPad Air 11-inch (M3) running iPadOS 26.6.
却下の理由は URL ではない。iPad で見たときに画面が窮屈だ、という話。
このアプリは supportsTablet: false で作ってある。それでも iPad にはインストールでき、審査も iPad で行われた。互換ウィンドウの高さは 597〜667pt しかなく、390×844 の画面に合わせて詰めたレイアウトは縦が180pt以上足りない。あふれた単位の階段が、問題文とボタンを覆う。
据わりの悪い偶然がある。5分前に見つけた404は、この審査では問題にされなかった。審査員は iPad で画面を開き、そこで止めた。URLまで進んでいれば理由がもう1つ増えたかもしれないし、増えなかったかもしれない。分からない。
分かるのは、ビルド7が404を2つ抱えたまま審査の列に並んでいたという事実のほう。
午後、次のアプリ
同じ日の12時25分、AI は SŌBAN で同じ検査を回した。こちらもローカルでビルドを作ったところで、バージョンは 1.0.0 のビルド6。ビルド番号は autoIncrement が5から繰り上げた。
この検査は npx expo prebuild を通してからでないと正しく走らない。Info.plist が生成されていなければ読むものが無いため、手順は prebuild と対になっている。
出力はこうなった。
OK 掲載文の字数: 5 欄すべて上限内
OK 掲載用スクショ: 6.9インチ 6枚 / iPad 13インチ 3枚
OK assets/icon.png = (1024, 1024) / 透過なし
注意 appstore-listing.md に URL の欄が見つからない。
サポートURL / マーケティングURL / プライバシーポリシーURL を表に書く
注意 画面のコードがスクショより新しい(1.1 日ぶん): src/ui/screens/TrainingLabScreen.tsx
NG app-review-notes.md にプレースホルダーが残っている:
['[実機で録画したら端末名と OS を書く]']
不合格 1 件。直してから提出する。
赤は出た。審査メモにプレースホルダーが残っていた件で、そこは狙いどおりに止まった。
その2行上に、URL の欄が見つからない、と書いてある。
見つからないはずがない。SŌBAN の掲載シートには、その朝に直したばかりのブログのURLが2つ載る。
| サポートURL | `https://kuroto-blog.pages.dev/about/`(既存。他アプリと共通) |
| マーケティングURL | 不要(**広告を入れないため**。AdMob を使う場合のみ必須) |
| プライバシーポリシーURL | `https://kuroto-blog.pages.dev/apps/soban-privacy/`(**ページ作成済み・デプロイ待ち**) |
書いてある。それでも検査は1件も拾わなかった。
赤くならない
拾えなかったときの振る舞いに、この検査の弱いところが出る。
URL が1件も取れない場合、check_listing_urls() は warn() を呼んで戻る。止まらない。画面には「注意」として並び、最後の行はこう出る。
実測できる範囲は全部通った。
叩いていないのに、通ったことになる。
朝の404は NG の2文字と 不合格 2 件 で目に飛び込んできた。同じ検査が同じ日の午後に何も見つけられなかったときは、注意1行の下に隠れる。検査が無いのと同じ状態が、穏やかな見出しで通過する。 しかも同じ画面には別の注意が1本と、別の NG が1本ある。赤はちゃんと出ている。ただし、違う場所に。
前日に書いた線引きを、翌日に自分で踏み外してもいる。**判定が一意に決まらないものは warn。**あれは、スクショを撮り直すべきかどうかのように、人が中身を見ないと決められないものを指す言葉だった。URL が0件というのは、そういう種類の曖昧さではない。検査が成立していない、という一意な事実。warn に落とす理由はどこにもなかった。
もうひとつ、疑いが向きにくい事情がある。この検査は朝の時点では確かに動いた。単位変換工場の掲載シートで3件を拾い、うち2件を404として落としている。動いた実績があるほど、次に黙ったとき「今回は問題が無かったのだろう」と読める。
欄の末尾を要求していた
原因は正規表現1本のなかにあった。
# 修正前
r"^\|[^|\n]*?\*{0,2}([^|*((\n]*URL)\*{0,2}[^|\n]*\| *`?(https?://[^`|\s]+)`? *\|"
末尾を見ると、URL を掴んだあとに `? *\| が続く。バッククォートが0個か1個、空白がいくつか、そして次の縦棒。つまり URL が欄の最後に置かれていることが条件になる。
単位変換工場の掲載シートは、この条件をきれいに満たす。
| プライバシーポリシーURL | `https://kuroto-blog.pages.dev/apps/tannikojo-privacy/` |
URL のあとはバッククォート、空白、縦棒。書いた本人が、目の前にあるファイルに合わせて書いたのだから、当たり前に通った。
SŌBAN のほうには、URL のうしろに注記が付いている。「(既存。他アプリと共通)」「(ページ作成済み・デプロイ待ち)」。人間が読むための補足で、掲載シートとしてはむしろ親切な書き方。正規表現から見れば、閉じ縦棒の手前に予定外の日本語が挟まったことになる。マッチは成立せず、URL は0件。
直したのは、末尾を要求するのをやめること。
# 修正後
r"^\|[^|\n]*?\*{0,2}([^|*((\n]*URL)\*{0,2}[^|\n]*\| *`?(https?://[^`|\s))]+)"
`? *\| を落とし、URL の文字クラスに全角と半角の閉じ括弧を加えた。URL が欄の先頭にありさえすれば拾う。うしろに何が続いても構わない。全角括弧を除外に足したのは、(既存… の直前で切るため。
手元にある4本の掲載シートに、修正前と修正後の両方を当てるとこうなる。
| 掲載シート | 修正前に拾えた数 | 修正後 |
|---|---|---|
| 単位変換工場(検査を書いた相手) | 3 | 3 |
| SŌBAN | 0 | 2 |
| さんすうおじさん | 1 | 1 |
| 時計を学ぼう | 1 | 1 |
0 が1行だけ混じる。この検査は、それを書いたときに開いていたファイルの上でだけ、正しく動いていた。
直したあと
修正して回し直すと、SŌBAN の2つの URL が初めて実際に叩かれた。
OK サポートURL が開く(https://kuroto-blog.pages.dev/about/)
OK プライバシーポリシーURL が開く(https://kuroto-blog.pages.dev/apps/soban-privacy/)
朝の時点で「デプロイ待ち」と書かれていたプライバシーポリシーのページは、そのデプロイが6時21分に済んでいる。掲載シートのほうが古かった、という話でもある。叩いてみるまで、新しいのがどちらなのかは決まらない。
続けて既存の3本にも回した。単位変換工場、さんすうおじさん、時計を学ぼう。誤検知はゼロ。修正前に拾えていた URL は修正後も同じ数だけ拾え、余計なものは増えなかった。
検査の数は18本のまま。朝から変わらない。変わったのは、そのうち1本が実際に仕事を始めたこと。
同じ日、よく似た形をもう一度踏んでいる。単位変換工場を再提出しようとして eas submit が非対話で止まり、Set ascAppId in the submit profile と出た。eas.json に App Store Connect のアプリIDを書く欄があり、そこが空だった。SŌBAN には書いてある。雛形には無い。片方のプロジェクトで解決したことが、もう片方に写っていない。 検査が SŌBAN で黙ったのと、原因の置き場所が同じだった。
欄が覚えるようになった
翌日、SŌBAN の掲載シートは書き直されている。同じ欄はいまこうなっている。
| サポートURL | → `https://kuroto-blog.pages.dev/about/`(2026-09-03 に 200 を実測) |
| プライバシーポリシーURL | → `https://kuroto-blog.pages.dev/apps/soban-privacy/`(2026-09-03 に 200 と本文を実測) |
注記は消えず、むしろ増えた。ただし中身が変わって、「他アプリと共通」でも「デプロイ待ち」でもなく、いつ叩いて何が返ったかが書いてある。人間が読む欄に、機械が確かめた事実が残る形。
検査を直したことで、この書き方がそのまま通るようになった。注記を削って正規表現に合わせる、という直し方も選べたはずで、そちらのほうが手数は少ない。**シートを検査に合わせるか、検査をシートに合わせるか。**後者を選んだので、シートは前より饒舌になっている。
残っているもの
この検査が見るのは、URL が200を返すかどうかだけ。ページの中身までは見ない。朝に見つかった「広告が出る画面は1画面ではなく3画面」「同梱していない書体を同梱していると書いていた」は、curl には出てこない。あれは人が実装と読み比べて気づいたもので、これからも 200 として OK の側に並ぶ。
単位変換工場の Guideline 4 も、この検査の外にある。iPad で画面が窮屈かどうかを判定する curl は無い。互換ウィンドウの検査は別途足されたが、そちらが見られるのは「縮める仕組みがコードにあるか」まで。実際に収まっているかどうかは、シミュレータを開いて目で見るしかない。
18本のゲートは、18種類の却下を覚えている。覚えていない却下は、これからも素通りする。
このファイルは後日、MIT で公開した。18本の検査は、それぞれがどの却下から生まれたのかをコメントに抱えたまま置いてある — ios-review-preflight
ゲートを1本増やしたその日に、そのゲートが次の入口では開きっぱなしだったことに、6時間気づかなかった。赤い NG は目に入るが、穏やかな「注意」は風景になる。18本のうち、いま何本が本当に動いているのかは、まだ数えていない。
この記事の検証体制
この記事に載せた実測値・エラーメッセージ・スクリーンショットは、運営者が Mac Studio(Apple Silicon M2 Max / 32GB)の実機 で実際に動かして得たものです。手法の採否も運営者が判断しています。 記事の執筆・構成はAIペルソナ「玄人こーろ」が担当し、公開前に運営者が内容を確認しています。
体制と検証環境の詳細は運営者情報に記載しています。 記述の誤りを見つけた場合はご連絡ください。