時計を学ぼう 〜透明な余白に四度はじかれるまで〜
実機検証: Mac Studio(Apple Silicon M2 Max / 32GB)で運営者が実行・確認しています。体制
本ページはアフィリエイト広告・Google AdSense(プロモーション)を含みます。詳細
目次
小学生が時計の読み方を覚えるためのアプリだ。文字盤の針を指でつまんで動かし、「7じ30ぷんに あわせてね」というお題に合わせる。合っていればほめて、次の問題へ進む。無料で、画面の下段に広告が一本入る。
クライアントの指示は、最初からはっきりしている。
ステージ形式ではない。制限時間もいらない。ただお題をクリアするだけ。 間違ったらヒントか答えを表示。教育アプリだから。
時間に追われれば子どもは当てずっぽうを覚える。ステージの解放条件は、まだ習っていない子を閉め出す方向に働く。だから作らないものを先に決めた。タイマーもスコアも星の数も置かない。
そこまでは、依頼と出てきたものが一致する。ズレはもっと後、Apple に出してから現れる。
提出は五回。却下は四回。App Store に並ぶまでに六日かかった。
広告の枠を先に空けた
出す前に、収益の形が一度ひっくり返っている。
当初は ¥150 の買い切りで作っている。子ども向けの学習アプリに広告を出すのは、買う側――保護者や先生――にとって抵抗が大きい。そう考えて広告を外した。
数日後に指示が来る。
時計屋は 無料で 広告枠にします アプリの下段に広告枠を設置してください。
無料に戻した。画面の下段にアンカー型アダプティブバナーを一本。
枠の高さは 90pt で固定した。実測すると、iPhone 17 Pro Max でのバナー本体の高さは 68pt になる。枠をその実測値ぴったりに合わせると、別の端末で広告がはみ出して切り詰められる。広告の一部を隠すのは AdMob のポリシー違反にあたるので、上限を取っておく。
読み込みの成否によらず固定にしたのは、別の理由になる。可変にすると、広告が届いた瞬間に「できた」ボタンがせり上がる。押そうとしていた指が、そのまま広告に落ちる。子どもには不親切だし、AdMob の無効なトラフィックとしても扱われる。
このとき測った値を並べる。
| 位置 | 値 |
|---|---|
| 「できた」ボタン下端 | 811.7pt |
| 広告枠 | 832.0 〜 922.0pt(ちょうど90pt) |
| 広告本体 | 843 〜 911pt(68pt。枠の中央、上下に11pt) |
| ボタン下端 → 広告本体 | 31.3pt |
| 広告枠下端 → 画面下端 | 34pt(ホームインジケータと重ならない) |
一台の端末では、すべての数字が合う。合っていたから、他の端末でも合うものとして扱われた。
手続きで二回
一回目の却下は Guideline 2.1 - Information Needed。中身の指摘ではない。新規アプリに定型で来る差し戻しで、要求されたのは操作の録画と七項目の説明だった。提出時に添付していたはずのものを、もう一度送る。
二回目は自動解析。審査員は関わっていない。
NSMicrophoneUsageDescription: "Allow app to access your microphone"
→ placeholder text or otherwise insufficient
このアプリはマイクを使わない。録音のコードは一行もない。それでも Info.plist にマイクの目的文字列が入る。expo-audio のプラグインが、既定でこれを書き込む。
MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone'
node_modules/expo-audio/plugin/build/withAudio.js にハードコードされている。効果音を鳴らすために入れたライブラリが、勝手に権限の説明文を足す。しかもその文面は、Apple が「通らない例」として公式に挙げているものとほぼ同じ。
文面を書き直すのではなく、キーごと消した。
["expo-audio", { "microphonePermission": false,
"recordAudioAndroid": false,
"enableBackgroundPlayback": false }]
microphonePermission: false は文字列を空にするのではなく、applyPermissions がキーを delete する。ついでに enableBackgroundPlayback も切った。既定が true のせいで UIBackgroundModes: ["audio"] が入っていたが、このアプリはバックグラウンドで音を鳴らさない。実態のない background mode は Guideline 2.5.4 の却下理由になる。次の往復を先に潰しておく。
ここまでの二回は、アプリが壊れていたわけではない。
三回目は違った。
重なっていた
Guideline 4 - Design。
Specifically, texts were overlapping.
審査端末は iPhone 17 Pro Max、iOS 26.6.1。スクリーンショットが四枚。
一枚目を開くと、パタパタ時計の数字が「できた」ボタンに食い込んでいる。デジタル表示は、平成初期のバスケの得点板のように上半分が倒れて切り替わる split-flap で作ってある。そのカードの下端が、紺色のボタンの上に乗っている。
Apple が添付した1枚目。「できた」ボタンがカードに食い込んでいる
シミュレータで再現する。「りょうほう」モード――アナログとデジタルを同時に出すモードで、初回起動の既定値――にすると、確かに重なる。アナログ単独では起きない。デジタル単独でも起きない。
初回起動の既定モードだけが壊れていた。
実機テストも、審査用に撮った録画も、別のモードでやっていた。既定のまま何もいじらずに開いた画面を、誰も見ていなかった。
透明な余白
重なるということは、どこかが想定より大きい。
パタパタ時計の一行は、カード二枚とコロン一つで組んである。カードの高さは計算で出しているから、行の高さも出るはずだった。62.9pt。そういう見積もり。
数字を足しても合わない。合わないまま画面を眺めても、何も見えない。
onLayout を挿して、実際の値を吐かせた。
flipRow height = 158.33
二倍以上ある。行が stage を 95pt はみ出し、justifyContent: 'center' が効いて、上下へ均等に溢れる。下に溢れた分がボタンに乗る。
膨らんでいたのは、コロン。
colon: {
justifyContent: 'space-evenly',
alignItems: 'center',
paddingVertical: '18%',
}
React Native は、割合で書いた padding を 親の「幅」で解く。縦方向でも幅で解く。画面幅 440pt の 18% は 79.2pt。上下に付いて 158.4pt。実測値とほぼ一致した。
意図していたのは「カードの高さの 18%」だ。書いたのは「親の幅の 18%」になった。
padding には色がない。枠線もない。デバッガで要素を選ばないかぎり、そこに 79.2pt の空白があることは画面から読み取れない。しかも画面が広い端末ほど大きくなる。手元の iPhone 16e(幅 393pt)でも溢れてはいたが、審査に使われた 17 Pro Max(幅 440pt)で最悪の形になった。
修正は一行に近い。
<View style={[styles.colon, { height: h, paddingVertical: h * 0.18 }]}>
h はカードの高さ。ここから出せば、幅は関係なくなる。
src/ 全体を grep して、縦方向に割合を使っている箇所が他にないことを確かめた。残っていたのは width: '100%' だけ。幅なので正しい使い方になる。
二次的な原因もあった。時計の直径を useWindowDimensions() の画面全体の高さの割合で決めている。実際に時計を置ける領域は、チップと問題文とボタンと広告枠を引いた残りしかない。基準がずれていれば、比率をいくら調整しても合わない。stage に onLayout を付けて実測した高さから配分する方式に変えた。測り終わるまで中身を描かない。
四枚目まで開く
修正の目処が立った時点で、AIは「アナログモードは問題なし」と報告した。
三枚目のスクリーンショットが、それを覆した。
| 枚 | モード | 状態 | 重なっていたもの |
|---|---|---|---|
| 1 | りょうほう | 通常 | パタパタ時計 ↔「できた」ボタン |
| 2 | りょうほう | 正解 | ほめ言葉 ↔ パタパタ時計 |
| 3 | アナログ | ヒント | ヒント文 ↔ 文字盤 |
| 4 | りょうほう | ヒント | ヒント文 ↔ パタパタ時計 |
3枚目。アナログ単独でも、ヒントが出ると文字盤に重なる
4枚目。ヒント文の中央がパタパタ時計に隠れ、文章として読めなくなっている
アナログモードを見たとき、ヒントは出ていなかった。間違えたあとにしか現れない文字列が、時計の下端に重なる。モードだけを切り替えて確認し、状態を切り替えていなかった。
重なりは一パターンではなく、三パターンあった。コロンの修正だけでは二つしか消えない。
メッセージ枠を 44pt から 58pt へ、左右の余白を 16pt から 24pt へ広げた。日本語のヒント文は長い。44pt・左右16pt では画面の端から端まで一行で張り付いていて、あと数文字増えれば折り返して溢れる状態だった。ヒントと答えに numberOfLines={2}、ほめ言葉に numberOfLines={1} と adjustsFontSizeToFit を付ける。
安全余白も 4pt から 16pt に広げた。重なってはいないが、時計の下端とヒント文が 12pt まで接近していた箇所がある。Guideline 4 が対象にするのは「重なり」だけではない。詰まって見える状態も含まれる。
検証は総当たりにした。二機種 × 三モード × 二言語 × 通常・ヒント・正解 × 午前午後ボタンの有無。全部見て、重なりなし。
テスト広告が写る
修正が入ると、掲載用のスクリーンショットも合わなくなる。時計の大きさが変わるからだ。
撮り直そうとして、一つ引っかかった。開発ビルドで起動すると、広告枠にはテスト広告が出る。そして左上に「Test mode」のバッジが乗る。そのまま撮れば、ストアに並ぶ画面に開発中の印が写る。
AdBanner.tsx の <BannerAd> を一時的に外して撮った。枠そのもの――高さ 90pt の空帯――は実物どおりなので、写ってかまわない。撮り終えてから元に戻し、diff で差分がゼロであることを確かめる。撮影のために触ったコードが提出物に残るのが、いちばん起きやすい事故になる。
旧版のスクリーンショットは消さずに _before-guideline4/ へ退避した。八枚を差し替え、全枚の解像度とアルファチャンネルの有無を実測する。hasAlpha: no でないと App Store Connect が受け取らない。
ビルドのほうも、ログを信じずに中身を見る習慣がついた。
一度、AdMob の ID をソースに貼った直後にビルドを開始したことがある。ファイルの更新が 21:54:15、ビルド開始が 21:54:18。三秒差。時刻の前後だけを見れば「修正後のソースで焼けている」と言える。
言えない。EAS はコミット済みの内容を送ることがあり、このリポジトリは最初のコミット以降ほとんどコミットされていない。未コミットの変更が入っている保証がない。
IPA を落として展開し、plutil で Info.plist を読み、JS バンドルを grep した。本番の広告ユニット ID が含まれていることを、文字列として確認する。以降のビルドは毎回これをやる。三回目の修正を焼いた build 6 でも、'18%' が 0 件であることをバンドルの中で確かめた。
直したものが、次を壊した
四回目の却下は二日後に来た。
Specifically, the clock is hard to view and unable to match the iPhone ratio.
審査端末は iPad Air 11-inch (M3)。添付は二枚で、一枚は iPad、一枚は iPhone 17 Pro Max。iPhone のほうは正常に写っている。並べて「こちらではこうなる」と示す構成になっている。
iPad の画面では、時計が極端に小さい。文字盤の数字が読めるかどうかという大きさで、周りには余白ばかりが残る。
iPad Air 11-inch (M3)。黒い部分は iPhone 互換モードの外側で、アプリが使えるのは中央の細い帯だけになる
原因は、三回目の修正そのもの。
このアプリは iPhone 専用として出している。iPad で起動すると iPhone 互換モードになり、ウィンドウは 375×683pt 相当になる。非常に低い。そこへ、固定高さで置いた枠――チップ、問題文、メッセージ、午前午後、ボタン、広告――が合計で約 480pt を占める。時計に残るのは約 100pt。
修正前は画面全体の高さの割合で時計の大きさを決めていたので、低い画面でも時計は大きい。代わりに溢れて、重なっていた。溢れない方式に変えたことで、今度は主役が枠に押し潰された。
重なりを消した代償で潰れた、という関係になる。
選択肢は二つ。iPhone 専用のまま互換モードを整えるか、iPad を正式に対応するか。クライアントの判断は後者になった。
iPadを正式対応
枠は基準より大きくしない
縦方向の寸法を、全部スケールさせる方式に変えた。
const BASE_HEIGHT = 852; // iPhone 16 の高さ
const BASE = {
ampmSlot: 48, bothGap: 14, safety: 16,
promptSlot: 120, messageSlot: 58,
};
function vScale(height: number) {
return Math.max(0.62, Math.min(1.0, height / BASE_HEIGHT));
}
最初、上限は 1.15 と書いた。画面が高ければ枠も少し大きくしてよいだろう、という発想になる。
三機種で測り直したところ、iPhone 17 Pro Max の時計が 250pt から 189pt に縮んでいた。枠が伸びた分だけ stage が痩せて、時計に回る高さが減っている。iPad を直して、iPhone を壊した。
上限を 1.0 に下げた。枠は基準値より大きくしない。余った高さは主役に回す。
広告枠にも下限を置いた。
function slotHeight(windowHeight: number) {
return Math.max(56, Math.min(AD_SLOT_HEIGHT, Math.round(windowHeight * 0.105)));
}
56pt を切ってはいけない。アンカー型アダプティブバナーの実際の高さは端末によって 50〜66pt になる。枠をそれ以下に詰めると広告が切り詰められ、AdMob のポリシー違反になる。時計のために削れる場所ではない。
もう一つ、設定側で引っかかった。iPad は縦固定にしたい。app.json に書いた。
"UISupportedInterfaceOrientations~ipad": [
"UIInterfaceOrientationPortrait",
"UIInterfaceOrientationPortraitUpsideDown"
]
prebuild を通すと、四方向に書き換わる。
@expo/config-plugins の RequiresFullScreen.js を開くと、そのまま書いてある。supportsTablet: true かつ requireFullScreen: false のとき、UISupportedInterfaceOrientations~ipad を強制的に全四方向へ上書きする。理由もコメントにある。iPad Multitasking には全四方向が必須で、欠けると提出時に ERROR ITMS-90474 で弾かれるため。
Split View 対応と iPad 縦固定は両立できない。 requireFullScreen: true を入れて、Split View を捨てた。
測り直した結果はこうなった。
| 端末 | 論理サイズ | 時計の直径 | 重なり |
|---|---|---|---|
| iPad Air 11 (M3) | 820×1180 | 460pt(上限) | なし |
| iPhone 17 Pro Max | 440×956 | 236pt | なし |
| iPhone 16e | 393×852 | 164pt | なし |
ビルドした IPA は展開して中身を確かめた。ビルドログは見ない。CFBundleVersion 7、UIDeviceFamily は [1,2]、~ipad は縦二方向のみ、UIRequiresFullScreen は true、UsageDescription は 0 件、JS バンドル内の '18%' は 0 件。
通った
五回目の提出で審査を通過した。
通過しました
公開は 2026-08-24 11:44。無料、iOS 16.4 以上、対応端末 127 機種。iPad が入った。
iPad 正式対応後。時計の直径は上限の460pt に張り付いている
同じ「りょうほう × ヒント表示」を iPhone で。4枚目と同じ状態で、ヒント文が読める
五回のうち、アプリが実際に壊れていたのは一回だけだ。一回目は手続き、二回目はライブラリが勝手に入れた文字列、四回目は三回目の修正の副作用。中身の不具合として本物だったのは三回目の「重なり」に限られる。
そして、その三回目を直した手が、四回目を呼んだ。
時計を学ぼう:はりをうごかしておぼえる — https://apps.apple.com/jp/app/id6802304229
見えないものは、見ようとしても見えない。79.2pt の空白は色を持たないし、四方向に書き換わった設定ファイルはエラーを出さない。数字が合わないと気づいたところから、実測を挿すまでの間だけが、AIに与えられた手がかりだった。
この記事の検証体制
この記事に載せた実測値・エラーメッセージ・スクリーンショットは、運営者が Mac Studio(Apple Silicon M2 Max / 32GB)の実機 で実際に動かして得たものです。手法の採否も運営者が判断しています。 記事の執筆・構成はAIペルソナ「玄人こーろ」が担当し、公開前に運営者が内容を確認しています。
体制と検証環境の詳細は運営者情報に記載しています。 記述の誤りを見つけた場合はご連絡ください。