2016年9月11日日曜日

iOSのフォントのお話

最近フォント周りについて深く掘り下げる機会がありましたので、その際のメモを残しておこうと思います。かなり読む人置いてけぼりな中身になってますが、フォントを詳しく触り始めるとなるほどーとためになる(と思う)のでどうかご了承ください(´・_・`)

UIFontのプロパティについて

UIFontにはフォントに関する数値を表すプロパティが存在します。いろいろありますが、もっとも重要なのは以下に列挙するプロパティです。

  • pointSize
  • lineHeight
  • ascender
  • descender
  • leading
  • capHeight

以下の画像を見ると非常にわかりやすいかと思います。


参照元: https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/CustomTextProcessing/CustomTextProcessing.html および http://stackoverflow.com/a/35922853

これを踏まえて各プロパティを解説すると以下のとおりになります。
  • pointSize
    • フォントのサイズを表します。
    • ほとんどすべてのケースでpointSize = ascender + descenderとなり、すなわちpointSizeは上端と下端に空白を含まない純粋なフォントの高さだけを表す数値であると言っても良いと思います。
      • 上下に空白を作らない高さでフォントを描画したい場合はこの高さを使うと良い、と言い切って間違いないと思います、ごく一部の変なフォント以外。
  • lineHeight
    • フォントの上部末端(上端空白が存在する場合はそれも)から下部末端(line gapも存在するなら含む)までの完全な高さを表します。
    • 飾り文字などではないまっとうなフォントであれば、必ずこのlineHeightの中に文字の高さが収まるようになっています。
      • 一部飾り文字のあるフォントについてはfont leadingを使って下部末端のline gapを表現していることがあります。
    • pointSizeとの最も大きな違いは、上端と下端にline gapに相当する空白を持っています。特にシステムフォントで顕著にそれが見られます。詳しくは後述。
    • 実際にフォントが画面上にレンダリングされるときはこのlineHeightがすべての基準になっているケースが多いです。
  • ascender
    • 全てのアルファベットの文字の最大高さを表します。
    • この高さまで描画されるのはごく一部の文字だけで、殆どの文字はcapHeightまでの高さしか使いません。詳細はこの後のcapHeightを参照してください。
  • descender
    • アルファベットのBaselineより下に描画される文字、例えばj p q yなどの下付き部分の高さを表します。
    • 日本語の文字でこの領域に描画される文字は私が知っている限りでは存在しません。
    • UIFontが返す値は常にマイナスになるので気をつけてください。
    • CTFontが返す値は常にプラスです。
      • めんどくさいので常にfabs()を通すといいと思います。
  • leading
    • Baselineからフォントの下部末端(line gapを含む)までの高さを表します。
    • 上記の図ですとline gapの部分だけがleadingとなっていますがヒラギノフォントで試した限りは誤りです。Baselineから下全てが対象になっているようです。
    • lineHeightに含まれている下部末端のline gapとは「全く別の値が付与されていることがあります」。
      • 例えば飾り文字のあるフォントについて、lineHeightよりもフォントの描画が大きくなるぐらい下に飾りが伸びていたとしても、leadingの値はそこまで考慮して付与してあるので、usesFontLeadingのオプションを付与すると下の行にはみでなくなる、みたいなケースが存在します。
  • capHeight
    • アルファベットの大文字の最大高さを表します。
    • 日本語の文字は(特に漢字は)基本的にすべてcapHeightの高さに描画されます。
    • capHeightより上に描画されるのは、例えばÅ(Aウムラウト)のように修飾子が付いているアルファベットや、日本語だと濁点および【】隅付き括弧のような一部の文字が、capHeightよりも上に描画されます。
特に覚えておきたいのがleadingというプロパティの働きです。UIKit, TextKit, Core TextにusesFontLeadingというような名前のオプションが存在するケースが多々ありますが(例えばNSAttributedString.boundingRect(with:options:context:)のoptionsとかですね)、これは何かというと、
usesFontLeadingが指定されている場合、lineHeightの計算の際に常にUIFont.leadingの値が使用される。従って、もしattributed stringにparagraph styleを付与してlineSpacingに0を付与していたとしても、フォントにleadingの値が付与されていたら、それ以上に行の高さが低くならず必ず行間が開いてしまう。
というオプションになります。paragraph styleでlineSpacingやparagraphSpacingを0に指定しているにもかかわらずどうしても行間がゼロにならないとか、指定するより広がってしまうみたいな状況になった場合は、このfont leadingを疑ってみてください。

ちなみにiOS 8以上については以下のとおりです。

  • UILabelは常にfont leadingを使用しません。
  • UITextFieldは未調査ですがおそらく常にfont leadingを使用しません。
  • UITextViewはデフォルトfont leadingを使用しますが、UITextView.layoutManager.usesFontLeadingの値をfalseにすることで回避可能です。
  • その他のラベルパーツ(UIButton.titleLabelなど)については完全には調査できていませんが、UIButton.titleLabelに関してのみいうとiOS 9の地点ではfont leadingが常に「使用されてしまいます」。これはおそらくiOS 6以前のUILabelの挙動がそのまま残っているためではないかと思われます。同じUILabelクラスですが中身と挙動がUILabelと異なるため気をつけてください。このようなケースは他にもあると思われます。

iOSビルトインフォントの問題点

幾つかのiOSビルトインフォントについては、先述のUIFontのプロパティの値に疑問がある数値が設定されているケースが有るようです。具体的にご紹介します。

1. ヒラギノフォントの場合

ヒラギノフォント、すなわちヒラギノ角ゴシックW3/W6、それからヒラギノ明朝W3/W6については以下の様なフォントの設定の特徴があります。
  • leadingの値がきちっと付与されています。
  • 常にpointSize == lineHeightになります。
  • おそらくバグだと思うのですが、descenderの値が本来必要な量の半分しか設定されていません。そのためヒラギノフォントを明示的に付与したラベルにg, j, y, qなどの下付き部分があるアルファベット文字を描画すると下が途中で千切れてしまいます。
    • 対処法としてはfont leadingを使用するか、または自力でdescender分だけラベルの高さを高くするか、明示的に指定してヒラギノフォントなんて使うのをやめてシステムフォントを使用してください。

2. システムフォントの場合

次にシステムフォントはどうなっているのかというと、以下の様な特徴があります。
  • 常にleadingの値がゼロになります。従ってfont leadingを使うモードで描画しても使わないモードで描画しても全く同じ結果になります。
    • iOS 8はHelvetica Neue, iOS 9以上はSan Franciscoが採用されていますが、いずれのケースでも全く同様の設定になっています。
  • pointSizeよりlineHeightが高く、ちょうどその差分の分だけ自動的にline gapとして行間が開くような設定になっています。
    • 要するに簡単にいえば、font leadingを使わないで、かつattributed stringでlineSpacingやparagraphSpacingをゼロに設定したとしても、必ず行間がちょっとだけ空いてしまいます。まずやることはないと思いますが、正確に行間ゼロを作るのは著しく困難です。

UIFontのpointSizeについて

UIFont.pointSizeについて、みなさん普段から何気なしに17とか14とか値を指定されているかと思いますが、このpointSizeに指定する値の単位って気にされていますでしょうか?このフォントのpointSizeとはPostScriptポイントないしDTPポイントと呼ばれるものらしいです。一般的に1PostScriptポイント=1/72インチとされています。

ではこの単位はiOS上で使われている論理座標系のpt単位とは全く違うものなのかというと、どうやら大昔のMacintoshのころからの名残で、macOS/iOS上の論理座標系の1pt=1PostScriptポイントとなるように定められていて、これは現在に至るまで常に維持されているということらしいです。

要するに、UIFontのpointSize=iOS上での1ptと完全に一致します。このことはlineHeightが16のフォントを画面上にレンダリングすると論理単位16pt(2倍Retinaなら32pxになる)ことからも確認ができます。便利ですね。


UIFont.leadingのiOS 8以下での不具合

かいつまんで言うと、UIFont.leadingの値は、iOS 8以下のときに常にUIFont.lineHeightと同じ値を返します。これは"font leading"という単語が使われている文脈や文化圏によって意味が異なり、font leading = line heightとなる文化圏があったので誤用されてしまった、ということらしいです。しかしながら実際のiOSでは先述の通り「font leading = BaselineからフォントのLine gapを含む下端までの距離」と定義されていますから、このような値では困ります。

対処法として、iOS 8以下の場合はCTFontを使ってleadingの値を取得してください。こちらにコードを用意しました。
https://gist.github.com/akisute/3c3d162da73abd784525c9ed7859cda2

なお、iOS 9以上はこの問題は発生しません。iOS 8なんてそろそろサポート切れるんで忘れていいかもしれませんが、念のため。

まとめ

デザイナーさんにフォント周りで行間調整がおかしいぞーとかベースラインがずれてるぞーとか突っ込まれまくっていて必死こいて修正方法を調べている時に、こんな記事を見つけました。

https://www.raizlabs.com/dev/2015/08/advanced-ios-typography/

Stop Saying "No" to Designers.
このサイトには英字フォントをBaselineではなくcapHeightの位置で上揃えにするべく奮闘した一人の漢の話が書かれています。おお、海外にも私と同じ境遇の漢が居たのです。

我々もデザイナーにNOと言わないタイポグラフィを実現させたいですね\(^o^)/