リッチテキストを扱うのはそんなに簡単なことではありません。フォントや文字、グリフ、絵文字、画像、リガチャなど、考慮しなければいけないことがたくさんあります。
この try! Swift の講演では、AppleのOSにおけるテキストレイアウトの基本や、複雑なテキストのレイアウトの扱い方をご紹介します。
イントロダクション (0:00)
こんにちは岸川です。日本から来て、Realmで働いています。今回は、 TextKit についてお話します。
スライド3にお見せしているようなこと、みなさんも経験ありませんか?テキストがありますが、正しくセンタリングされていません。また、スライド6のように、行間がおかしくなることがあります。
この講演では、どうやったらテキストが正しい位置にレンダリングされるのかお見せします。以下4つの項目がメインになります。
- テキストの幅や高さ、行間の設定の仕方の説明
- タイポグラフィの基礎知識
- TextKitやNSAttributedStringの基本的な使い方
- テキストの表示に関する応用例
TextKitとは? (1:16)
TextKitはiOS7で導入されました。モダンなテキストレンダリングエンジンです。CoreTextの上に位置し、UIKitととても相性の良いものです。このおかげで、CoreTextのような低レベルのAPIを使うことなく、難しいテキストのレイアウトを行うことができます。
TextKitはフレームワークではありません。TextKitは、既存のテキスト表示オブジェクトを強化する設定の名前です。 TextKitを使う特別なものはありません。 UILabelやText Attributeを使うことは、TextKitを使うのと同じことです。
フォントメトリクス (2:18)
UILabelを使って、テキストを表示してみましょう。スライド12では、2つの違うUILabelを用意しました。同じテキストですが、フォントが異なっています。それぞれのフォントは同じサイズで設定していますが、表示が異なっていますよね。この違いはどこから来たのでしょう。
フォントはテキストを表示するためのメトリクスを渡すデータを持っています。スライド15では、フォントメトリクスの異なる例を用意しました。ベースラインは文字の上の仮の線です。”J”や”G”のような文字は中心がベースラインの下にあります。
ベースラインは言語によって異なります。ここでは、TextKitがRoman体に基づいているので、Roman体で説明しています。
Ascentはグリフの上端からベースラインまでの距離です。Descentはベースラインから下端の距離です。LeadingはDescenderの下端からマージンの設定における次のラインの上端までの垂直距離です。
スプレッドシートを使うことなく、描画されるサイズを知ることはできるのでしょうか?NSStringのメソッドやNSAttributeString、つまり boundingRectWithSize:
を使うことで知ることができます。
let size = CGSize(width: label.bounds.width, height: CGFloat.max)
let boundingRect =
NSString(string: text).boundingRectWithSize(size,
options: [.UsesLineFragmentOrigin],
attributes: [NSFontAttributeName: font],
context: nil)
これがTextKitの良いところです。この例はテキスト一行の例です。
複数行については、 options
を使うことができます。
let size = CGSize(width: label.bounds.width, height: CGFloat.max)
let boundingRect =
NSString(string: text).boundingRectWithSize(size,
options: [.UsesLineFragmentOrigin],
attributes: [NSFontAttributeName: font],
context: nil)
スライド20では、Labelと同じテキストで違うフォントに対するboundingRectを用意しました。Bounding Sizeはピッタリ一致しています。
TextView (5:44)
次に、スライド22では、テキストを表示しているUITextViewを用意しました。ですが、UILabelよりすこし大きいようです。次のスライドでは、boundingRectをViewに重ねてみると、明らかに大きいですね。複数行のテキストでも同じようです。なぜでしょう?
それは、Text Attributeがデフォルトでマージンを持っているからです。 textContainerInset
や lineFragmentPadding
があります。また、UITextViewはUILabelと違って、Font Leadingに従います。iOS9からは、Font Leadingがめったに使われないので気にする必要はありません。実際、San Francisco fontのようにiOS9から導入された新しいフォントでは、Leadingは0になっています。他のフォントでも同じで、とても小さな値しか持っていません。
CJK fontは例外です。CJK fontのLeadingは大きな値になっていて、外部カスタムフォントです。このフォントを使うと、Font Leadingによって、予期しない表示になることがあります。
テキストの正しいサイズを知るには、異なるマージンを取り除く必要があります。 textContainerInset
を取り除くにはこのようにします。
let textView = UITextView(frame: view.bounds)
...
textView.textContainerInset = UIEdgeInsetsZero
textView.sizeToFit()
それから、 lineFragmentPadding
も取り除きます。
let textView = UITextView(frame: view.bounds)
...
textView.textContainer.lineFragmentPadding = 0
textView.sizeToFit()
先ほど説明したとおり、Leadingは予期しない表示をもたらすかもしれません。常にLeadingを無視することをおすすめします。ドキュメントには載ってませんが、ヘッダファイルを見るとAppleもLeadingはのUIの表示使うことには適さないと言っています。Font Leadingを無視するには、このようにします。
let textView = UITextView(frame: view.bounds)
...
textView.layoutManager.usesFontLeading = false
textView.sizeToFit()
これでTextViewの正しいサイズがわかるようになりました。スライドに書いてあるとおり、もしFont Leadingを使うNSStringを指定するときは、このようにします。
let size = CGSize(width: textView.bounds.width, height: CGFloat.max)
let boundingRect =
NSString(string: text).boundingRectWithSize(size,
options: [.UsesLineFragmentOrigin, .UsesFontLeading],
attributes: [NSFontAttributeName: font],
context: nil)
リッチテキストの表示 (9:29)
これまで、一行のテキストの正しいサイズを知る方法をお見せしました。複数行についてはどうでしょう?特別なことではありません。UILabelやTextView、Attribute Stringのような同じコンポーネントが表示されます。すなわち、正しいNSAttributedString
を定義するだけでいいのです。簡単な例です。
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = ceil(font.lineHeight)
paragraphStyle.maximumLineHeight = ceil(font.lineHeight)
paragraphStyle.lineSpacing = ceil(font.pointSize / 2)
let attributes = [
NSFontAttributeName: font,
NSForegroundColorAttributeName: UIColor(...),
NSParagraphStyleAttributeName: paragraphStyle,
]
let attributedText = NSAttributedString(string: text, attributes: attributes)
textView.attributedText = attributedText
フォントを設定し、色を変え、ライン間の幅を広げています。各行間を固定するには、スライド46のように、フォントの行の高さの最大値、最小値をセットします。
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = ceil(font.lineHeight)
paragraphStyle.maximumLineHeight = ceil(font.lineHeight)
paragraphStyle.lineSpacing = ceil(font.pointSize / 2)
Line Spacingは行と行の間隔です。FirstLineHeadIndentは段落の1行目を右にシフトします。カーニングは、文字間の間隔を調整することです。表示する属性が多すぎるので、これ以降、いくつかの応用例を見ていきます。
NSAttributedStringの応用例 (11:29)
スライド56に最初の例を用意しました。たくさんのフォントや異なるスタイルのパラグラフ、ブレークポイントがあります。これは一つのViewとNSAttributedStringで構成しています。SubViewはありません。フォントサイズも定義していません。画像の行間もです。
次の例を見てみます。テキストレンダリングにおいて、数式の表示が一番難しい挑戦です。一つ目の例は二次式です。
x = -b ± √b2-4ac2a
スライド65からひとつひとつ属性を適応していくと、欲しい数式が正しく表示されるようになります。
スライド80から始まる2つ目の数式では、TextKitを使ってどのようにカスタマイズしていくのか、順番に見ることができます。
この例は、私の GitHubのリポジトリ でご覧いただけます。これを見て、遊んでもらえたらうれしいです!
まとめ (15:52)
この発表をまとめると、以下のようになります。
- もうCoreTextを直接使うことはありません。TextKitはユースケースの99%をカバーしていると思います。
- テキストウェアはフォントメトリクスに依存するので、気をつけてフォントを選びましょう。
- NSAttributedStringを正しく構成するのが一番大事なことです。
- マスターするとは、TextKitを極めることです。
ありがとうございました。
参考資料
- Apple Docs - Using Text Kit to Draw and Manage Text
- Apple Docs - Font Handling
- YYText - GitHub Repository
- TextKitExamples - GitHub Repository
About the content
2016年9月のtry! Swift NYCの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。