個人で作っている iOS アプリの見た目を直していて、文字サイズを最大(Dynamic Type の XXXL)にして全画面を撮る、というのを一度やりました。
コードを読んでいる限り、ひとつも見つかりませんでした。スクリーンショットでは即座に見えました。
最終的に11件出ました。全部同じ原因です。
結論を先に書きます。「横並びを固定した親 × 拡大するテキスト」は、1つのバグではなく1つのパターンです。 そして日本語では、英語より確実に酷く壊れます。
英語は省略される。日本語は縦書きになる
同じレイアウトで、言語によって壊れ方が違いました。
英語は単語の切れ目で折り返し、入らなければ末尾が … になります。読めなくても「何かが省略された」ことは分かります。
日本語は違います。 どこでも改行できてしまうので、列が1文字幅まで潰れると 1行1文字で縦に積まれます。
実際に出たものを並べます。
| 表示 | 本来 |
|---|---|
Paire / d / Macs |
Paired Macs(単語の途中で分断) |
Claud / e / Code |
Claude Code |
| 「実行中のセッション」が1行1文字 | 実行中のセッション |
| 「基本設定」「詳細設定」「確認」が縦に積まれる | 3ステップの横並び |
| 「De / mo」でカプセルが円になる | Demo バッジ |
Paire / d / Macs は英語なのに単語の途中で切れています。列幅が数文字しか残らないと、英語でもここまで行きます。日本語はもっと手前で縦書きになります。
原因はいつも同じ形をしていた
11件のうち、ほとんどがこれでした。
HStack {
Image(systemName: icon).frame(width: 44) // 固定
Text(label)
Spacer()
Text(value) // 右端固定
}
44pt のアイコンと右端の値が幅を先に取り、ラベルの列に数文字しか残らない。
修正は「値を持つ行だけ、値を下の行へ落とす」。ただしシェブロン・トグルなど操作の手がかりは右端に残す。
この行コンポーネントは設定画面ツリー全体で共有していたので、1箇所直すと設定画面が丸ごと直りました。 逆に言えば、共有コンポーネントの1つの決めが11画面を壊していたということです。
ViewThatFits は万能ではない
縦積みへの切り替えに ViewThatFits を使いました。標準サイズでレイアウトが壊れました。
ViewThatFits は子の 理想幅 で判定します。折り返すタイトルは「折り返さない場合の全長」を理想幅として報告します。だから横並び候補が「収まらない」と判定され、標準サイズなのにバッジがタイトルの下へ落ちました。
これは推論では気づけませんでした。標準サイズのスクリーンショットを撮って初めて分かりました。
使い分けはこうなりました。
-
ViewThatFits— 子が全て短く、固有の幅を持つとき(バッジ、2語の指標) -
isAccessibilitySizeで明示的に分岐 — 子のどれかが「折り返す文」であるとき
右端に固定してよいのは「その場で操作するもの」だけ
途中で自分の判断を1回訂正しています。
最初、.menu の Picker を Toggle と同じ「右端に固定すべきもの」に分類しました。監査ログ画面のスクリーンショットがそれを否定しました。 「絞り込み」が 絞り… / 込み になっていました。
分類し直した基準がこれです。
- 右端に固定する — その場で状態が変わるもの(Toggle)
-
全幅の行に落としてよい — 何かを開くだけのもの(
.menuPicker、シェブロン)
モーダルは一度も検証されていなかった
UI テスト用に UITEST_FORCE_ACCESSIBILITY_XXXL という環境変数を持っていました。ルートビューに .dynamicTypeSize を当てる実装です。
.sheet の environment には伝播しません。
つまりペイウォールもセットアップガイドも、一度も最大文字サイズで描画されたことがありませんでした。 通っていたのは「テストが緑」であって「モーダルが検証された」ではなかった。
システム設定側で当てるように変えました。
xcrun simctl ui <udid> content_size accessibility-extra-extra-extra-large
「収束した」を2回言った
6箇所直したところで「このパターンは収束した」と書きました。早合点でした。
未確認の画面が残っていただけで、見に行ったら4件出ました。7〜10件目です。そのあと QR スキャナ / ホスト追加 / ペアリングを日本語 + XXXL で見に行って、新規発見ゼロを確認してから11件目で収束を宣言しました。
教訓は「画面を実際に見るまで収束を宣言しない」です。 直した数ではなく、見た画面の数で判断する。
ついでに見つかった、複数形が1つも無かった件
監査ログを読んでいて 1 items need attention が目に留まりました。走査したら、数量プレースホルダを持つ英語文字列38件のうち、複数形の変化形を持つものがゼロでした。
- プロジェクト一覧「1 items need attention」
- 設定「1 saved Macs」
- ペイウォール「1 days free」
- 共有シート「1 sessions, 1 active…」← 端末の外に出ていく文
13キーに変化形を追加しました。日本語は複数形一致が無いので変更なしです。%d of %d Macs のように数が名詞に一致していないものは意図的に据え置きました。
検証手順が肝でした。まず1キーだけ入れてビルドし、実画面で 1 item needs attention を確認してから残りに展開。最後にビルド成果物の en.lproj/Localizable.stringsdict を読んで NSStringPluralRuleType が正しく生成されていることを確認しました。
直さなかったものも書いておきます
無理に作業を作らない方針で、正直に記録しています。
- 一覧画面は XXXL でも破綻なし
- オンボーディングは XXXL でもスクロールで全文に到達できる(境界での切れは通常の
ScrollViewの挙動) - 設定上部の余白は
.largeナビゲーションタイトルの標準レイアウトで、iOS 標準アプリと同じ
まとめ
- 「横並び固定 × 拡大テキスト」は1つのバグではなくパターン。 1件見つけたら全画面を疑う
- 日本語は英語より酷く壊れる。 英語は省略で済むが、日本語はどこでも改行できるので縦書きになる
- 共有コンポーネントの1つの決めが、11画面を壊す。 直すときも1箇所で効く
-
ViewThatFitsは理想幅で判定する。 折り返す文が子にいると標準サイズを壊す - 右端に固定してよいのは、その場で操作するものだけ
- 環境変数はシートに届かない。 「テストが緑」は「モーダルを見た」ではない
- 画面を実際に見るまで収束を宣言しない
コードを読んで見つかったものは、この11件のうち0件でした。
この話が出てきたのとは別に、デスクトップで動く AI エージェントを作っています。Wisp です。
話しかけると答えて、頼めばコマンドも実行します。実行の前に、何をするかを必ず出して許可を取ります。