Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Mastering TextKit

Mastering TextKit

try! Swift NYC 2016
- Mastering TextKit

Kishikawa Katsumi

February 18, 2017
Tweet

More Decks by Kishikawa Katsumi

Other Decks in Programming

Transcript

  1. Mastering TextKit
    try! Swift NYC 2016
    Katsumi Kishikawa
    [email protected]
    Hi, I’m Katsumi and today I'm going to talk about TextKit.

    View full-size slide

  2. Katsumi Kishikawa
    Realm Inc.
    [email protected]
    I'm coming from Japan, and I work at Realm.

    Around text component, this ever happen to you?

    View full-size slide

  3. [email protected]
    The text isn’t on the centre.

    View full-size slide

  4. [email protected]
    Irregular line spacing, and so on.

    In this talk I will show you how to render the text in the correct position.

    View full-size slide

  5. Agenda
    ˖ )PXUFYUTMBJEPVUJOJ04
    ˖ #BTJDUZQPHSBQIZ
    ˖ #BTJDVTBHFPG5FYU,JU
    ˖ "EWBODFEFYBNQMFT
    [email protected]
    I’m going to be covering four major points today

    First, I explain what determined the width, height and line spaces of text. It is important to be laid out the text accurately to the aimed position.

    Second, very basic knowledge of typography because it is necessary to do that.

    Third, basic usage of TextKit, almost about NSAttributedString.

    And finally, I'll show more advanced examples displaying rich text.

    View full-size slide

  6. Text Layout after iOS 7
    [email protected]
    So let’s get started! TextKit has been introduced in iOS 7.

    View full-size slide

  7. What is TextKit?
    [email protected]
    What is TextKit?

    View full-size slide

  8. TextKit is
    ˖ .PEFSOUFYUSFOEFSJOHFOHJOF
    ˖ #VJMUPOUPQPG$PSF5FYU
    ˖ )JHIMZJOUFHSBUFEXJUI6*,JU
    [email protected]
    https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/CustomTextProcessing/CustomTextProcessing.html
    TextKit is a modern text rendering engine on iOS.

    It is built on top of CoreText, and very well integrated with UIKit. Thanks to that, you can achieve an advanced text layout, even without using the low-level API directly
    such as the CoreText. TextKit is not a framework in the traditional sense. Instead, TextKit is the name for a set of enhancements to existing text-displaying object and
    work with attributed strings. So, there is no anything special that uses the TextKit. Using UILabel or UITextView means using the TextKit.

    View full-size slide

  9. UILabel
    [email protected]
    OK. Let’s take a look displaying text that uses a UILabel.

    View full-size slide

  10. [email protected]
    UILabel
    Here are two UILabels. The same text but fonts are different.

    Each font is the same size. However, the result is quite different.

    As you may see, the latter label is taller than the former.

    Where do these differences come from?

    View full-size slide

  11. Font Metrics
    [email protected]
    To understand that, I’ll have to explain about font metrics.

    View full-size slide

  12. [email protected]
    Font Metrics
    https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/FontHandling/FontHandling.html
    Digital fonts contain data representing the metrics used for displaying. In other words, the fonts know their sizes that are displayed.

    View full-size slide

  13. [email protected]
    Font Metrics
    https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/FontHandling/FontHandling.html
    The baseline is the hypothetical line upon which characters rest. Some characters such as J and G have descenders that drop below the baseline.

    Definition of the baseline differs depending on the language, though, At this time it is described in the Roman text because the TextKit is based on it.

    The ascent is the distance from the tops of the glyphs to the baseline. The descent is the distance from the baseline to the bottom.

    The leading is the recommended vertical distance from the bottom of the descenders to the top of the next line in a multiline setting.

    View full-size slide

  14. [email protected]
    Font metrics
    Baseline
    X-Height
    Cap height
    Ascent
    Descent
    So far, I've explained what depends the text that on an area to be drawn. It greatly depends on the font. If the different font would be specified, the rendering results will
    change. Because, the text is drawn by the font metrics. They are different for each font.

    View full-size slide

  15. How to know the display size
    [email protected]
    So, can we know the size to be drawn without displayed it?

    View full-size slide

  16. let size = CGSize(width: label.bounds.width, height: CGFloat.max)
    let boundingRect =
    NSString(string: text).boundingRectWithSize(size,
    options: [],
    attributes: [NSFontAttributeName: font],
    context: nil)
    [email protected]
    Get Bounding Rect
    Single Line
    We can achieve it by using the method of NSString or NSAttributedString (it is boundingRectWithSize) that is a part of the enhancement of TextKit. This example is for the
    single line text.

    View full-size slide

  17. let size = CGSize(width: label.bounds.width, height: CGFloat.max)
    let boundingRect =
    NSString(string: text).boundingRectWithSize(size,
    options: [.UsesLineFragmentOrigin],
    attributes: [NSFontAttributeName: font],
    context: nil)
    [email protected]
    Get Bounding Rect
    Multiple Lines
    For multiple lines of text, pass UsesLineFragmentOrigin to the options.

    View full-size slide

  18. [email protected]
    Get Bounding Rect
    This screen shot is overlaying to the label the bounding rect.

    It shows that exactly match the bounding rect and the views.

    View full-size slide

  19. UITextView
    [email protected]
    OK,

    Up next, let me touch on UITextView.

    View full-size slide

  20. [email protected]
    UITextView
    Here is a text view that displays a same text and font. But it looks like a little larger than the UILabel.

    View full-size slide

  21. [email protected]
    UITextView
    Overlay bounding rect on the view. It is certainly larger.

    View full-size slide

  22. [email protected]
    UITextView
    The same result even in the multiple lines. Why?

    View full-size slide

  23. [email protected]
    Default margins
    Because UITextView has margins by default.

    View full-size slide

  24. [email protected]
    Default margins
    textView.textContainer.lineFragmentPadding
    textView.textContainerInset
    UIEdgeInsets(top: 8.0, left: 0.0, bottom: 8.0, right: 0.0)
    5.0
    They are textContainerInset and lineFragmentPadding.

    View full-size slide

  25. [email protected]
    Leading
    Also, UITextView respects the font leading unlike UILabel. Usually, it doesn't matter because, from iOS 9, font leading is rarely used. In fact, San Francisco font has zero
    leading. Same for the other fonts, or have only a very small value.

    View full-size slide

  26. [email protected]
    CJK Font
    font.leading
    One of the exceptions is CJK fonts. CJK fonts has large leading.

    View full-size slide

  27. [email protected]
    Custom Font (Not built-in)
    font.leading
    And external custom fonts. If you use these fonts, they will lead to an unexpected result due to the font leading.

    View full-size slide

  28. Reset Default Margins
    [email protected]
    So, in order to know the exact size that text view to draw, you must remove the default margins.

    View full-size slide

  29. let textView = UITextView(frame: view.bounds)
    ...
    textView.textContainerInset = UIEdgeInsetsZero
    textView.sizeToFit()
    [email protected]
    Remove textContainerInset
    To remove a textContainerInset,

    View full-size slide

  30. let textView = UITextView(frame: view.bounds)
    ...
    textView.textContainerInset = UIEdgeInsetsZero
    textView.sizeToFit()
    [email protected]
    Remove textContainerInset
    assign a zero insets to the textContainerInset.

    View full-size slide

  31. [email protected]
    Remove textContainerInset
    Now textContainerInset has been removed.

    View full-size slide

  32. let textView = UITextView(frame: view.bounds)
    ...
    textView.textContainer.lineFragmentPadding = 0
    textView.sizeToFit()
    [email protected]
    Remove lineFragmentPadding
    To remove a lineFragmentPadding,

    View full-size slide

  33. let textView = UITextView(frame: view.bounds)
    ...
    textView.textContainer.lineFragmentPadding = 0
    textView.sizeToFit()
    [email protected]
    Remove lineFragmentPadding
    assign a zero to textContainer.lineFragmentPadding.

    View full-size slide

  34. [email protected]
    Remove lineFragmentPadding
    Then, lineFragmentPadding has also been removed.

    View full-size slide

  35. let textView = UITextView(frame: view.bounds)
    ...
    textView.layoutManager.usesFontLeading = false
    textView.sizeToFit()
    [email protected]
    Ignore font.leading
    Finally, to ignore a font leading. As explained earlier, leading might lead to an unintentional result. So I recommend to always ignore the leading.

    View full-size slide

  36. [email protected]
    Ignore font.leading
    @interface NSLayoutManager : NSObject
    ...
    // By default, a layout manager will use leading as specified by the
    font. However, this is not appropriate for most UI text, for which a
    fixed leading is usually specified by UI layout guidelines. These
    methods allow the use of the font's leading to be turned off.
    @property(NS_NONATOMIC_IOSONLY) BOOL usesFontLeading;
    Apple also said that leading is not appropriate in the UI text… In the header file.

    I think, Apple should document this.

    View full-size slide

  37. let textView = UITextView(frame: view.bounds)
    ...
    textView.layoutManager.usesFontLeading = false
    textView.sizeToFit()
    [email protected]
    Ignore font.leading
    Anyway, To ignore a font leading, disable layoutManager’s `usesFontLeading` property.

    View full-size slide

  38. [email protected]
    Ignore font.leading
    Did it!

    It is now possible to know the exact size even in the text view.

    View full-size slide

  39. 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)
    [email protected]
    Note: Including font.leading
    Just as a side note, when you specify usesFontLeading option, you can get a size including the leading.

    View full-size slide

  40. [email protected]
    Including font.leading
    Like this.

    View full-size slide

  41. [email protected]
    textView.textContainerInset = UIEdgeInsetsZero
    textView.textContainer.lineFragmentPadding = 0
    textView.layoutManager.usesFontLeading = false
    Reset Default Margins
    Alright. This is the way to reset the margins of the text view. To accurately layout in place aimed at, you need to know these behaviours.

    View full-size slide

  42. Displaying Rich Text
    [email protected]
    So far, I've shown how to get exact size in single style text. Then, what about the multiple style text? There is nothing special.

    Because even the text has multiple styles, it would be displayed on the same component such as UILabel, UITextView and attributed string.

    In other words, all we have to do is construct a correct attributed string.

    View full-size slide

  43. 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
    [email protected]
    NSAttributedString
    This is a simple example of NSAttributedString. It changes a font, colour, and more wider spaces between the lines.

    View full-size slide

  44. [email protected]
    Change Line Height
    Even as you specify what kind of styles,

    View full-size slide

  45. [email protected]
    Change Line Height
    you can get the correct result.

    To achieve the fixed spaces between each line, like this,

    View full-size slide

  46. 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
    [email protected]
    Control Line Height
    set same value as font size to minimum/maximum line height,

    View full-size slide

  47. let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.minimumLineHeight = ceil(font.lineHeight)
    paragraphStyle.maximumLineHeight = ceil(font.lineHeight)
    paragraphStyle.lineSpacing = ceil(font.pointSize / 2)
    [email protected]
    min/maxLineHeight
    then adjust line spacing property. Why there are two similar properties?

    View full-size slide

  48. [email protected]
    min/maxLineHeight
    https://github.com/ibireme/YYText
    MinimumLineHeight MaximumLineHeight
    Because for the multiple sizes fonts in the same paragraph. MinimumLineHeight keeps the line spacing, MaximumLineHeight will do the opposite.

    View full-size slide

  49. [email protected]
    Line spacing
    https://github.com/ibireme/YYText
    Line spacing is spacing between lines,

    View full-size slide

  50. [email protected]
    FirstLineHeadIndent
    https://github.com/ibireme/YYText
    FirstLineHeadIndent shifts start point of every first line.

    View full-size slide

  51. [email protected]
    TextAlignment
    https://github.com/ibireme/YYText
    Text alignment,

    View full-size slide

  52. [email protected]
    Kerning
    https://github.com/ibireme/YYText
    Kerning is adjusting the spacing between characters.

    View full-size slide

  53. Advanced Example of
    NSAttributedString
    [email protected]
    There are too many attributes to show, so, after this, we will look at the some advanced examples.

    View full-size slide

  54. [email protected]
    This is the first example. There are multiple fonts, paragraphs in different styles, and bullet point.

    View full-size slide

  55. [email protected]
    It consists single view and attributed string. There is no any subviews obviously.

    View full-size slide

  56. [email protected]
    Tab stops, and headIndent

    View full-size slide

  57. [email protected]
    Mathematical Formulas
    Another example is rendering mathematical formulas. I'm pretty sure displaying math is the most difficult challenge in text rendering.

    But TextKit can.

    View full-size slide

  58. [email protected]
    Mathematical Formulas
    The first example is the Quadratic Formula.

    View full-size slide

  59. [email protected]
    Mathematical Formulas
    Now, we start from this.

    View full-size slide

  60. [email protected]
    Mathematical Formulas
    [NSFontAttributeName: UIFont(name: "LatinModernMath-Regular"]
    First, change to the math font.

    View full-size slide

  61. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: 18]
    Shift the baseline of the radical symbol.

    View full-size slide

  62. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: -26]
    Get down 2a for the fractional.

    View full-size slide

  63. [email protected]
    Mathematical Formulas
    [NSKernAttributeName: -68]
    And move left using the kerning attribute.

    View full-size slide

  64. [email protected]
    Mathematical Formulas
    [String(kCTSuperscriptAttributeName): 1]
    2 to superscript for the power

    View full-size slide

  65. [email protected]
    Mathematical Formulas
    [NSFontAttributeName: font.scale(x: 0.6, y: 0.6)]
    Superscript size is also defined by the font.

    Almost the same size in this case, then scale the font manually.

    View full-size slide

  66. [email protected]
    Scaling font
    extension UIFont {
    func scale(x x: CGFloat, y: CGFloat) -> UIFont {
    return transform(CGAffineTransformMakeScale(x, y))
    }
    func transform(matrix: CGAffineTransform) -> UIFont {
    return UIFont(descriptor: fontDescriptor().fontDescriptorWithMatrix(matrix), size: pointSize)
    }
    }
    Scale method is like this.

    Using FontDescriptor, which is part of the TextKit, you can apply CGAffineTransform to the fonts.

    View full-size slide

  67. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: 2]
    In the same way, continue to adjust the layout.

    View full-size slide

  68. [email protected]
    Mathematical Formulas
    "\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}"
    This is a character of a horizontal line.

    View full-size slide

  69. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: 14]
    Up the line

    View full-size slide

  70. [email protected]
    Mathematical Formulas
    [NSKernAttributeName: -12]
    and move left

    View full-size slide

  71. [email protected]
    Mathematical Formulas
    “\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}...”
    Same

    View full-size slide

  72. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: -12]
    and same

    View full-size slide

  73. [email protected]
    Mathematical Formulas
    [NSKernAttributeName: -135]
    Finally, centre the x and equals sign,

    View full-size slide

  74. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: -12]
    Did it. It’s so beautiful, isn’t it..

    View full-size slide

  75. [email protected]
    Mathematical Formulas
    Let’s see the more complex example, but it is the same to do.

    View full-size slide

  76. [email protected]
    Mathematical Formulas
    [NSFontAttributeName: UIFont(name: "LatinModernMath-Regular"]

    View full-size slide

  77. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: -20]
    It just only repeat same steps as well.

    View full-size slide

  78. [email protected]
    Mathematical Formulas
    [NSKernAttributeName: -14]

    View full-size slide

  79. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: -8]

    View full-size slide

  80. [email protected]
    Mathematical Formulas
    [NSKernAttributeName: -14]

    View full-size slide

  81. [email protected]
    Mathematical Formulas
    [NSKernAttributeName: 4]

    View full-size slide

  82. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: -8]

    View full-size slide

  83. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: -24]

    View full-size slide

  84. [email protected]
    Mathematical Formulas
    [NSFontAttributeName: font.scale(x: 0.6, y: 0.6)]

    View full-size slide

  85. [email protected]
    Mathematical Formulas
    [NSKernAttributeName: -20]

    View full-size slide

  86. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: 10]

    View full-size slide

  87. [email protected]
    Mathematical Formulas
    [NSFontAttributeName: font.scale(x: 0.6, y: 0.6)]

    View full-size slide

  88. [email protected]
    Mathematical Formulas
    [NSKernAttributeName: -14]

    View full-size slide

  89. [email protected]
    Mathematical Formulas
    [String(kCTSuperscriptAttributeName): -1]

    View full-size slide

  90. [email protected]
    Mathematical Formulas
    [NSFontAttributeName: font.scale(x: 0.6, y: 0.6)]

    View full-size slide

  91. [email protected]
    Mathematical Formulas
    [String(kCTSuperscriptAttributeName): 1]

    View full-size slide

  92. [email protected]
    Mathematical Formulas
    [NSFontAttributeName: font.scale(x: 0.6, y: 0.6)]

    View full-size slide

  93. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: -8]
    Here, the radical symbol needs twice height.

    View full-size slide

  94. [email protected]
    Mathematical Formulas
    [NSFontAttributeName: font.scale(x: 1, y: 2.4)]
    So scale the font twice to y axis.

    View full-size slide

  95. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: 20]

    View full-size slide

  96. [email protected]
    Mathematical Formulas
    [NSKernAttributeName: 2]

    View full-size slide

  97. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: 16]

    View full-size slide

  98. [email protected]
    Mathematical Formulas
    [NSKernAttributeName: -118]

    View full-size slide

  99. [email protected]
    Mathematical Formulas
    [NSBaselineOffsetAttributeName: -8]
    Alright, I did it!

    View full-size slide

  100. [email protected]
    https://github.com/
    kishikawakatsumi/TextKitExamples
    The examples are on GitHub. I’m happy that you see and play with it.

    View full-size slide

  101. Summary
    • No longer use CoreText directly
    • Carefully to choose font. Text layout is based
    on font metrics
    • The most important is constructing
    NSAttributedString
    [email protected]
    In summary, the main points of my presentation were,

    No longer use CoreText directly. TextKit covers ninety-nine percent use cases.

    Carefully to choose font. Because text layout is based on font metrics. The most important is constructing attributed string correctly.

    That’s all. So mastering attributes strings means mastering TextKit.

    That’s it. Thank you for listening.

    View full-size slide