The handling of rich text is most definitely not easy. We have to consider a lot of things like fonts, characters, glyphs, emojis, images, ligatures, etc.
In this try!Swift talk, I will show you the basics of laying out text and how to handle complex text layouts in Apple’s OS.
Introduction (0:00)
Hi, I’m Katsumi. A little bit about me, I’m from Japan, and I work at Realm. In this post, I’m going to be talking about TextKit.
You can see on slide 3, has this ever happened to you? We have some text, and it isn’t centered correctly; or on slide 6, where we have irregular line spacing.
In this talk, I will show you how to render the text in the correct position. I’m going to be covering four major points.
- First, I will explain setting the width, height and line spaces of the text.
- Second, some very basic knowledge about typography.
- Third, basic uses of TextKit and NSAttributedString.
- And finally, I will show more advanced examples about displaying text.
What is TextKit? (1:16)
TextKit was introduced back in iOS 7. It is a modern text-ware rendering engine. It is built on top of CoreText and very well integrated with UIKit. Thanks to that, you can achieve advanced text layout without using low-level APIs such as CoreText.
TextKit is not a framework in the traditional sense. Instead, TextKit is the name of setup enhancements to existing text displaying objects; there isn’t anything special that uses TextKit. Using UILabel or a text attribute means using TextKit.
Font Metrics (2:18)
Let’s take a look at displaying text that uses UILabel. On slide 12, we have two different UILabels. They have the same text, but the fonts are different. Each font has the same size, but the results are quite different. Where do these differences come from?
Fonts contain data representing the metrics used for displaying them. On slide 15 we have an example for different font metrics. The baseline is a hypothetical line upon which characters rest. Some characters such as “J” and “G” have the center that drops below the baseline.
The definition of the baseline differs, depending on the language. Though at this time, it is described in the Roman text because TextKit is based on it.
The ascent is the distance from the top of the glyphs to the baselines, and descent is from the baselines to the bottom. The leading is the required vertical distance from the bottom of the descender to the top of the next line in the marginal settings.
Can we know the size to be drawn without using a spreadsheet? We can achieve it by using an NSString method or NSAttributeStrength: 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)
That is a part of the enhancement of TextKit. This example is for a single line of text.
For multiple lines of text we can use 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)
On slide 20, we have the Label and the boundingRect for the same text with different fonts, and their bounding sizes match exactly.
TextView (5:44)
Next, on slide 22 we have a UITextView that displays some text, but it looks like a little larger than the UILabel. On the next slide, if we overlay a boundingRect on the view, it’s certainly larger. And it’s the same result even on multiple lines of text. Why?
Because your Text Attribute has margins by default. We have a textContainerInset
and a lineFragmentPadding
. Also, UITextView respects the font leading, unlike UILabel. Usually, it doesn’t matter because since iOS 9, font leading is rarely used. In fact, the San Francisco font – it is a new font from iOS 9 – has zero leading. Same for the other fonts, all have only very small values.
One of the exceptions is CJK fonts. CJK fonts have large leadings. And external custom fonts. If you use these fonts, they will lead to unexpected results due to the font leading.
To know the exact size that the Text is built to draw, you must remove the different margins. To remove the textContainerInset
:
let textView = UITextView(frame: view.bounds)
...
textView.textContainerInset = UIEdgeInsetsZero
textView.sizeToFit()
We also have to remove the lineFragmentPadding
:
let textView = UITextView(frame: view.bounds)
...
textView.textContainer.lineFragmentPadding = 0
textView.sizeToFit()
As explained earlier, leading might lead to an unintentional result. I recommend always ignoring it. Apple also said that the leading is not appropriate in the UItext in the header. To ignore the font leading:
let textView = UITextView(frame: view.bounds)
...
textView.layoutManager.usesFontLeading = false
textView.sizeToFit()
It is now possible to know the exact size even in the TextView. Just a side note, if you want to specify a NSString to use font leading, you can do:
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)
Displaying Rich Text (9:29)
So far, I have shown how to get the exact size in single text line. What about multiple lines? There is nothing special. It would be displayed on the same components such as UILabel, TextViews and Attribute Strength. In other words, all we have to do is define a correct NSAttributedString
. This is a simple example:
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
It changes the font, color and sets a wider space between the lines. To achieve the fixed spacing between each line, like you can see on slide 46, set the same values as the font size to minimum and maximum line height:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = ceil(font.lineHeight)
paragraphStyle.maximumLineHeight = ceil(font.lineHeight)
paragraphStyle.lineSpacing = ceil(font.pointSize / 2)
Line spacing is the spacing between lines. FirstLineHeadIndent to shift the start point to point of adding every first line. Cloning is adjusting the spacing between characters. There are too many attributes to show, so after this, we will look at some advanced examples.
Advanced Examples of NSAttributedString (11:29)
On slide 56, we have our first example. There are many fonts, paragraphs in different styles and break points. It consists of a single view and NSAttributedString. There are no subviews, no defined font sizes, nor defined line spaces in the images.
For our next examples, displaying mathematical formulas is the most difficult challenge in text rendering. The first example is the quadratic formula:
x = -b ± √b2-4ac2a
You can see on the slides, starting on slide 65, step-by-step on the attributes we can use to make that mathematical formula look exactly how we want it.
We have a second mathematical formula example starting on slide 80, which you can see step-by-step how we can customize it using TextKit.
You can find the examples on my GitHub repository. I’m happy that you can see and play with it!
Conclusion (15:52)
In summary, the main points of my presentation are:
- No longer use CoreText directly. I think TextKit covers 99% of use cases.
- Choose fonts carefully because text-ware is based on font metrics.
- The most important thing is correctly constructing
NSAttributedString
. Mastering that means mastering TextKit.
Thank you.
Receive news and updates from Realm straight to your inbox