iOSでFlutterの文字がSwiftUIより細く見える原因 — Skiaの補正テーブルと字詰めの差、アプリ側の対処法

はじめに

ネイティブの iOS アプリを Flutter に置き換えたとき、「なんだか文字が薄い」「前より字が間延びして見える」と言われた経験はないでしょうか。気のせいとして片付けられがちなこの違和感を、ピクセル単位で調べた記事が r/FlutterDev で話題になりました。

結論から言うと、原因は2つあります。1つは Skia がグリフの縁に掛けている補正テーブル、もう1つは SF Pro の字詰め(tracking)を Flutter が読んでいないことです。本記事では、この調査の中身を整理し、エンジンの修正を待たずにアプリ側でできる対処までをまとめます。

何が起きているのか — 縁のインクが削られている

調査ではまず、SwiftUI の Text、UILabel、生の CoreGraphics で同じ文字を描き、ライトモード・ダークモードともにピクセルが完全に一致することを確認しています。iOS のネイティブ描画はグレースケールのアンチエイリアスで、縁のピクセルのインク量=輪郭が覆う面積 という素直な描き方です。

Flutter も、グリフの輪郭をラスタライズする段階までは CoreGraphics を使うので、出てくるピクセルは iOS と同じです。違いはその後にあります。Skia がそのピクセルにガンマ/コントラストの補正テーブルを通し、その結果を Impeller のグリフアトラス(文字画像のキャッシュ)に保存して GPU で描いています。

このテーブルは「白背景の黒文字」向けに作られており、縁のピクセルからインクを減らします。文字の内側の塗りつぶし部分は変わらないため、字形はそのままで縁だけが薄くなる、という見え方になります。

計測結果は次のとおりです(17pt・3x 画面)。

指標ネイティブ iOSFlutter
文字全体のインク量100%93.8%(約6%減)
縁ピクセルの平均インク量0.4950.413(約17%薄い)

文字が小さいほど縁の比率が上がるため、影響も大きくなります。描画サイズが 51px なら 94% 残りますが、13px では 78% まで落ちます。キャプションや注釈のような小さい文字ほど薄く見えるわけです。

なぜこうなったのか — 3つの設定が重なった結果

記事は、エンジンのソースから3つの要因を挙げています。

  1. 補正テーブルが有効: skia/BUILD.gn で SK_GAMMA_APPLY_TO_A8 が定義されている。Android では固定値(ガンマ 1.4・コントラスト 0)が入るが、iOS では Skia の既定値(sRGB に近い曲線・コントラスト 0.5)がそのまま使われる。
  2. 常に黒文字用のテーブル: font_glyph_pair.h の ComputeTone が iOS では常に kDark を返す。そのため白文字にも黒文字用のテーブルが使われ、ダークモードの白文字も細くなる。明暗で別のグリフを持つのは macOS だけ。
  3. 逆補正が無効: glyph_atlas.frag には補正を打ち消す text_contrast の処理があるが、iOS では使われていない。

これは見落としではなく、ある程度意図された設計です。2023年の engine PR #39383 でアトラスのグリフを白から黒に切り替えた際、作者は「白文字が少し細すぎる」ことに気づきつつ、太すぎるよりはましだと判断しました。2026年5月の PR #186074 で macOS には明暗別のグリフが入りましたが、iOS は「高密度の画面では差が目立たない」として対象外になっています。

もう1つの原因: SF Pro の字詰め

インク量とは別に、文字の横幅の問題もあります。SF Pro にはサイズごとの字詰め量(tracking)の表があり、iOS はこれを自動で適用します。記事によると 17pt では1文字あたり約 -0.43pt 詰められますが、Flutter はこの表を読まずにフォント既定の間隔で並べます。36文字の行で約 15pt 広がる計算で、字間が空くとそれだけで文字は薄く見えます。

Flutter の Cupertino テキストテーマは 17pt 本文に letterSpacing: -0.41 を固定で入れていますが、自分で TextStyle を組んだ時点でこの値は外れます。

エンジニアへの影響 — アプリ側でできる3つの対処

根本修正(アトラスの描画面で補正を無効化する、シェーダーで逆補正を有効にするなど)はエンジンの C++ を書き換えてビルドし直す必要があり、記事の作者自身も試算のみで実機検証はしていません。関連 Issue(#120857、#150824)はいずれも P2 で、すぐに直る見込みは薄いと考えておくのが無難です。なお #120857 のタイトルは「ライトモードで細すぎ、ダークモードで太すぎ」ですが、今回の調査では白文字も黒文字と同じだけインクが減っていると計測されています。

そのうえで、アプリ側では次の対処が紹介されています。

1. ぼかしなしの Shadow で縁を足す

Text(
  'Hamburgefonstiv',
  style: TextStyle(
    fontSize: 17,
    color: ink, // 必ず不透明な色にする
    shadows: [
      Shadow(color: ink.withValues(alpha: 0.75), offset: Offset(0, 0.01)),
    ],
  ),
)

同じ文字をもう一度、不透明度 75% で重ねることで縁のピクセルにだけインクが足され、ネイティブとの差が約1%まで縮むとしています。Offset(0, 0.01) は、オフセットもぼかしもゼロの影を Flutter が描画しない仕様を避けるためのものです。注意点は2つで、文字色が半透明だと2回分のアルファが重なって濃くなりすぎること、描画コストが倍になることです。画面全体ではなく、本文やリストの主要テキストなど要所に絞るのが現実的でしょう。

2. letterSpacing を SF Pro に合わせる

独自の TextStyle を使う場合は、サイズに応じた letterSpacing を設定します(17pt なら -0.41〜-0.43 前後)。デザインシステムのトークンにサイズごとの値を持たせておくと、画面ごとのばらつきを防げます。

3. フォントを太くして補わない

FontWeight を上げると幹の部分まで太くなり字形が変わる一方、縁は相変わらずネイティブより薄いままです。見た目の印象は近づいても、別物の文字になってしまいます。

また、ネイティブ版とのスクリーンショット比較(ゴールデンテスト)を使っている場合、レイアウトが一致していても文字部分は必ずずれます。比較対象から文字を外すか、インク量の合計で比べるといった工夫が必要です。

まとめ

  • iOS の Flutter で文字が薄く見えるのは、Skia の補正テーブルがグリフの縁のインクを削っているため(17pt で約6%、小さい文字ほど大きい)
  • 白文字にも黒文字用のテーブルが使われるので、ダークモードでも同じく細くなる
  • 加えて SF Pro の字詰めを Flutter が適用しないため、行が横に広がり薄い印象を強めている
  • アプリ側では、ぼかしなし Shadow の重ね描きと letterSpacing の調整が有効。太字化は避ける

ネイティブからの Flutter 移行では、「文字が薄い」というフィードバックに対して原因と数字で説明できるかどうかが信頼に直結します。エンジン側の修正が入るまでは、今回の対処を共通の TextStyle に組み込んでおくのがよさそうです。

ソース

今日のその他のニュース

Firebase Extensions が 2027年3月31日に終了

Firebase Extensions が非推奨となり、2027/3/31 に終了します。インストール済みの拡張機能は動き続けますが、終了後は設定変更や削除ができず、セキュリティパッチも届きません。移行先は中身を第2世代 Cloud Functions として自分のプロジェクトに置く「function kits」です。画像リサイズや Stripe 連携を使っている場合は早めに確認を。(公式 FAQ)

Mistral が時系列予測モデル「Timestral」を公開

5M〜209M パラメータの4サイズで、GPU なしの CPU でも 5M 版は約 10ms で予測できます。21段階の分位点で予測の幅まで出せ、修正 MIT ライセンスで Hugging Face から入手可能です。(Mistral)

Let’s Encrypt がドメイン検証の拠点を追加

業界ルールの変更に合わせ、検証拠点を4つから6つに増やします。ステージングは 10/27、本番は 11/10 から。公式には「影響はほぼない」とされていますが、念のため、検証用アクセスを IP で絞っているサーバーは新しい拠点からの通信が弾かれないか確かめておくと安心です。(Let’s Encrypt Community)