WWDC 2018 Session 221: TextKit Best Practices

About the author: @Halohily, netease Youdao iOS development engineer Nuggets homepage: Halohily

The introduction

Text content is everywhere in the app, and there are many different ways to display text. Those of you who have focused on improving performance will know that efficient use of text controls is critical to improving overall page performance. To that end, Apple and developers are working hard. Such as Apple’s increasingly refined text framework and YYText, the representative of third-party text frameworks.

This session is designed to guide developers on how to properly use TextKit to present text content. Step by step, it is divided into three parts:

  • The core theory
  • A small example to demonstrate a theory
  • Comprehensive use of excellent actual combat cases

First, core theory

1.1 What is TextKit?

A bit different from the usual framework, we don’t need to use the import keyword to import TextKit. The UIKit framework (for iOS), which contains controls such as UILabel and UITextField, and the AppKit framework (for Mac OS), which contains controls such as NSTextView, are both built on TextKit. TextKit works with Core Text, Core Graphics, and Foundation to provide powerful Text display capabilities for our app.

With the power of TextKit, you can easily display the following text in different styles.

1.2 Select the correct control

For different types of text, we need to select the appropriate text control. So how do you decide? Apple gave us some pretty clear guidance, as you can see below. The situation is slightly different when you use UIKit and AppKit, so I’ll describe it separately.

  • UIKitSelect path:

  • AppKitSelect path:

The description in the picture is very clear and easy to read. Note that UILabel is used to display less text or fewer lines. However, under the AppKit framework, there is no Label control. In this case, you can select the NSTextField control and disable the text editing property to obtain the same features as UILabel.

1.2.1 Correct use of string Drawing

Sometimes, for better performance (to avoid generating too many view object instances), you might call the following method to use text drawing:

func draw(at: CGPoint)

func draw(in: CGRect)

func draw(with: CGRect,
options: NSStringDrawingOptions = [],
context: NSStringDrawingContext?)
Copy the code

However, Apple doesn’t recommend using it that often. If you still need it, Apple has some thoughtful advice:

  • Use as little text as possible
  • Limit callsdrawMethod frequency (minimize the number of calls)
  • Limit the number of custom attributes (minimize custom attributes)

Why is this not recommended? First, controls such as UILabel and UITextView provide a good caching mechanism, so choosing these controls at the right time can actually achieve better performance (compared to String Drawing), especially when using automatic layouts.

If you call the draw method too much when drawing a view string, it will significantly degrade performance. Because the system needs to release all previous attribute objects before each drawing. Therefore, for additional attributes, try to draw them only when you determine their visual effects (such as fonts, colors).

Finally, Apple stresses that if you use string Drawing, you lose all the features provided by the text control shown below. Therefore, use text controls whenever possible.

1.3 Select the right customization points

1.3.1 TextKitArchitectural components of

Like many components in Cocoa, TextKit is based on a “Model-View-Controller” design structure. And these three layers respectively contain storage, layout, and display modules:

  • Storage

Take a closer look at the components, starting with the Storage module that communicates with the Model layer, which contains the NSTextStorage that holds string data and attribute information. Note that it is a subclass of MutableAttributedString, so it is used in the same way as AttributedString is known. NSTextContainer is responsible for modeling the location and region information of the text layout.

  • Display

Next comes the Display module, which communicates with the View layer. In this module we usually focus on the correct selection of text controls.

  • Layout

Finally, there is the Layout module, which communicates with the Controller layer. NSLayoutManager is the only component of this module. It is so powerful that Apple has been described as a beast. It is the “brain” of the whole presentation process, controlling its own layout process.

1.3.2 Layout process

Here’s an overview of the text layout process:

  • Attribute correction

The text layout occurs after the TextStorage properties are modified. For example, make sure that the font selected for this text supports all characters in the text, and replace any that do not. For example, Tempura (Tempura) is a tasty Japanese food. 🍤 specifies Times New Roman. However, the font does not support Japanese and emoji characters. Therefore, in the property correction process, Japanese characters were assigned the Japanese-supporting Hiragino Mincho ProN font, and emoji characters were assigned the Apple Color Emoji font.

  • glyphcharacter

Once the property modification is complete, the layout process begins. Here are some explanations of the above concepts.

Characters are general purpose data that can be converted to binary storage, while GLyph can be translated into visual symbols for characters. The same character can be displayed on the screen in different fonts and visual styles. These different visual styles are presented by GLyph. Glyph generation is the process of determining the glYPH required for the display of characters with specified visual effects (such as fonts). Here is an example:

As you can see, the relationship between character and Glyph is not always one-to-one. The string “ffi” in the figure consists of three characters, but the entire string can be represented by a glyph. In the example below, a single character “n” can also be represented by two glyph.

For this part of the concept, there is a reference: iOS typography Concepts

Back to the layout process diagram, glyph layout, is the process by which NSLayoutManager places glyph on the view.

1.4 Selecting the Correct Configuration

The following is a standard configuration for a complete TextKit component: The Text Container holds a weak reference to the Text View, which holds the entire layout tree through the root Text Storage.

If you have multiple Text pages or lines that need to be laid out, you can use pairs of Text Containers and Text Views, each pair of which corresponds to a page or line. In this case, we can hook the same Container and text View to share layout information.

After the text content is added, it fills the area defined by the first Text Container. The text is displayed in pairs on the Text View and in the Text Container. When there is no space left, a new Container is added along with the Text View, and the text is displayed on a second page or text line.

Multiple Layout Managers allow you to display the same text in different ways. This text can have different and independent layouts and groupings on different views, as shown below in this mode. The text inside the box is the same, but the presentation is different.

1.5 Select the correct customization implementation mode

Just as hammers are important in the toolbox, we have tools that are the equivalent of hammers in development.

  • The agentLike a basic hammer, most of the time, it gets the job done just fine.
  • noticeIt’s also an effective tool.
  • In the end,subclassIt’s also a weapon. It can do almost anything.

In the second part, specific examples will be used to illustrate the usage scenarios of these methods.

2. Specific examples

Text components are ubiquitous in apps. In this part, Apple uses specific pages in Apple News of iOS, TextEdit and Our Journal of Mac OS as examples to explain the core theories mentioned above.

2.1 Apple News on iOS

This is the easy part. It’s mainly used to signal the theory of Choosing the right control. The main knowledge points are as follows:

  • For a line of text with different colors, you can use twoUILabelFor presentation, you can also useNSAttributedStringTo implement.
  • UITextViewUIScrollViewSubclass, which supports sliding by default, needs to be disabled if you want it to work well with automatic layout.

2.2 TextEdit on macOS

This part is mainly used to indicate the theory of Choosing the right configuration.

TextEdit is an app that supports displaying and editing rich text. The features of the text editing section are similar to those of a TextView. Naturally, it conforms to the standard configuration structure described above. Note that the text editing section supports pagination display, and you can see that as the page slides, the TextContainer is resized and the text jumps from page 1 to page 2. Naturally, this is a TextView that uses multiple TextContainers, but is managed by the same TextStorge and LayOutManager, which allow text to jump freely from one TextContainer to another. The following figure shows its configuration structure:

2.3 Our Journal App on macOS

This part is mainly used to indicate the theory of Choosing the right customization approach.

2.3.1 Text Counting Function

As you can see from the figure, a TextField has been added at the bottom of the interface to display the amount of text typed. When our app runs, we want the text count at the bottom to change as we type it in. To achieve this effect, we chose a “lighter” tool – notifications. You can get the amount of text from NSTextStorage by receiving notifications from NSTextStorage. After receiving the notification, update the number in the count TextField.

2.3.2 Automatically Converting bold characters

When we want to emphasize a part of the text, we can use the keyboard shortcut or the menu to set that part of the font to bold. But what if you want to support markup languages such as Markdown, which specify special formatting with certain characters, such as adding a double star before and after the text to make it bold? In this scenario, you need to know when and where the text changes, and notification mechanisms are not convenient for providing sufficient information. So this time use “one hammer” – proxy. Observe NSTextStorageDelegate protocol, realize textStorage (_ : didProcessEditing: range: changeInLength:) method. Define a boldface attribute in the method implementation to add to the text that should be bolded. This way, as soon as you enter a pair of double stars, you can immediately make the text bold.

2.3.3 Snippet text

Bold markup is perfectly implemented. So how do you show a snippet of code? When you type the last dot, as shown in the figure, you generate a code block text that is also identified as a Swift code. For such a complex situation, we need two tools:

  • subclassNSTextStorage

Subclasses inherit NSTextStorage and implement four mandatory implementation methods, especially the replaceCharacters(in:with:) method. The internal implementation assigns NSTextBlock to ParagraphStyle and then adds that ParagraphStyle as an attribute to an NSTextStorage, noting that the corresponding range is code block text.

For the NSTextBlock described above, it’s important to know that NSTextBlock doesn’t customise itself, so we need a subclass of it to do this: The CodeBlock class inherits from NSTextBlock and either sets the background liner in its initialization method or draws the “Swift Code” title using StringDrawing by overwriting the drawBackground method.

This makes the text block look like a code block. Going back to the CustomTextStorage inherited from TextStorage, we can assign the TextBlocks property to the CodeBlock we just added.

Finally, we need the textView to use the new CustomTextStorage, so we replace the storage with LayoutManager.

2.3.4 Markdown Effect preview view

This basically completes an editor that supports the Markdown format. In addition, the markdown editor has another useful feature – two views laid out side by side, one for entering text and one for preview effects, as shown in the figure below:

We can do this using two side-by-side TextViews by simply disabling text editing for the TextView for preview. They show the same thing, but the one on the right is a little bit different. The configuration used is shown below:

Storage is the same because it displays the same content. But all the other parts are two sets, and the textStorage of the left view is assigned to the replaceTextStorage of the LayoutManager of the right view. What is the effect? Once you edit text on one side, the effects are displayed on both sides. However, we generally do not want to display markdown control characters in the preview view, such as double star ** and reference symbol >. Since it is the same shared TextStorge, this means that we must hide these characters later in the process (the layout process). In order to complete this operation, have a natural options – agent: observe NSLayoutManagerDelegate agency agreement, realizing the layoutManager (_ : shouldGenerateGlyphs: properties: CharacterIndexes: font: forGlyphRange:) agent method, we can get will be the layout of the glyphs, if it is used to represent the markdown character glyph, assignment it is empty. Finally, the processed glyphs were sent back. This shows editable text containing the Markdown control character on the left and effect text with the markDown control character removed on the right. This is a good example of customizing TextKit, although a Markdown editor does not actually do this.

Three, the best actual combat case

In this section, Apple provides several guiding principles.

3.1 Be familiar with default attributes

In this example, we need to complete a text presentation like the one above. Its current font is 24-point Comic Sans MS. After setting the bold attribute for the don’t text, we found that the rest of the text (that is, hate) lost its original font setting. This is because when the AttributedString is initialized, no attribute Settings are provided, and the default Settings are used. In this case, the text is initialized with the default Settings, then the don’t section is set separately, and the natural hate section is set with the default Settings.

There are two ways we can approach this. One option is to avoid setting the entire text at the same time and instead set it to bold for Don’t and Comic Sans MS for hate, which is tedious. So the other option is to initialize the AttributedString with an argument to the original font, and then set the don’t part.

In addition to fonts, we need to know the default values for other attributes.

3.2 Use accurate attribute descriptions

  • Avoid actions that reset all or part of the text to default properties.
  • When updating your app to support the upcoming Dark mode, make sure your text color is correct in this mode. This is very important for AppKit developers.

Pay particular attention to the ParagraphStyle property highlighted above. A bad example is when, to cut off the text in the Hate section, you set a separate property ParagraphStyle for that section of text. The results, however, did not meet expectations. This is because before layout, attribute fixing is made, as described earlier. It goes against consistency for a paragraph of text to have multiple attribute values that ParagraphStyle, so when fixing attributes, the system chooses the first ParagraphStyle attribute, which goes by default, and applies it to the whole paragraph.

3.3 Performance: Use discontinuous layout

To understand it, go back to our old friend the layout process. Glyph generation is followed by glyph layout. For large blocks of text, LayoutManager has to do all the glyph generation and layout if you use a whole layout, so you have to wait a long time if you have large blocks of text.

For NSTextView, you can set allowsNonContiguousLayout properties to support the discontinuous layout.

For UITextView, it’s turned on by default. It is important to note that UITextView is a subclass of UIScrollView, allowsNonContiguousLayout attributes require UITextView Scroll Enabled attribute is open. If sliding is not supported, the intermittent layout is meaningless.

This raises an important question. When using intermittent layouts, avoid requesting the entire text layout at once. So if you only have one TextContainer, avoid requesting the entire layout all at once.

3.4 security

Here apple gives a graphic example: developers are like armed soldiers, while iOS and Mac OS are like strong fortresses, and soldiers and fortresses together form strong security fortifications. This means that the security of iOS apps requires collaboration between developers and Apple.

To that end, Apple has a guideline for developers:

  • Set limits for text input

All text input is considered a potential risk. When you allow text to be entered, you open up copy and paste, but you can’t predict what text will be pasted there. It can be plain text, but it can also be extremely long text, which can cause unpredictable problems with your app.

How do you complete validation of text input? Under UIKit, using UITextFieldDelegate, under AppKit via NSFormatter.

To be sure, Apple has given a preview of security improvements to come.

conclusion

Finally, a diagram summarizes the session:

For more WWDC 18 articles, head over to the xSwiftGG WWDC 18 Topics directory