Discover the latest updates to TextKit and text views in UI frameworks. Explore layout refinements and API enhancements, learn how you can maintain compatibility across multiple OS versions, and find out how to modernize your app with TextKit 2.
To get the most out of this session, watch “Meet TextKit 2” from WWDC21.
- Hi, and welcome to What's New in TextKit and text views! I'm Donna Tom, and I'm a TextKit engineer.
In iOS 15 and macOS Monterey, we introduced TextKit 2, a powerful new text engine with improved performance, correctness, and safety.
TextKit 2's viewport-based layout architecture delivers high performance text layout, especially for documents with large contents.
TextKit 2 provides a better text experience for international audiences by removing the unnecessary complexity of working with glyphs, and it has full support for modern font technologies like OpenType and Variable Fonts.
And TextKit 2's focus on working with higher level objects to control text layout makes it easier for you to customize the layout of your text so you can build cooler stuff with less code.
Moving forward, the TextKit 2 engine forms the foundation of text layout and rendering on all of Apple's platforms.
Future performance enhancements, updates, and improvements will all be focused on the TextKit 2 engine.
By updating to TextKit 2, your app can get the benefits of these improvements as we roll them out.
For an in-depth introduction to TextKit2, watch the Meet TextKit2 video. That video covers the fundamentals and how to build your own text layout components using TextKit 2.
In contrast, this video covers the latest advancements in TextKit 2 and how to get the most out of TextKit 2-backed text views. That's right, I said text views, plural, because now, as of iOS 16 and macOS Ventura, all text controls in UIKit and AppKit are using TextKit 2, including UITextView. So we're using TextKit 2 for layout and rendering all throughout the system. It's important that all apps transition to TextKit 2 as soon as possible, and we've added a number of tools to make the transition easier for you. For many apps, this can be a zero code transition. And we expect this to be true for apps that don't make any special modifications to their text views. I'll tell you a bit more about that later.
But first, I'll start by going over what's new in TextKit 2, including some of those tools I just mentioned.
After that, I'll dive deep into the details of the TextKit 1 compatibility mode for text views.
Then I'll finish with a discussion of modernization strategies you can use when preparing to transition your code to TextKit 2.
So first up is what's new in TextKit 2.
TextKit 2 first came to UIKit in iOS 15 where UITextField was upgraded to use it. In iOS 16, the UIKit transition to TextKit 2 is complete, with all text controls using TextKit 2 by default, including UITextView. Most text views will be automatically opted in to TextKit 2, requiring zero adoption on your part. There are just a few situations where text views might not get opted in, and I'll cover that in the compatibility segment of this video.
And it's a similar story for AppKit. TextKit 2 first came to AppKit in macOS Big Sur. In macOS Monterey, NSTextField was upgraded to use it by default, and it was available for NSTextView by opting in.
In macOS Ventura, all text controls use TextKit 2 by default. Just like UITextView, most NSTextViews get automatic opt in to TextKit 2 and require zero adoption on your part.
TextEdit, which is a thin wrapper around NSTextView, uses TextKit 2 everywhere in macOS Ventura. TextEdit has been using TextKit 2 in plain text mode since macOS Big Sur. In macOS Ventura, rich text mode uses TextKit 2 as well.
Since TextKit 2 is the new standard, we've added some convenience constructors for both UITextView and NSTextView. Use these new constructors to choose at initialization time which text engine to use.
To create a text view that uses TextKit 2, use the new constructor and pass true for the "UsingTextLayoutManager" parameter. If the text view needs to use TextKit 1 for compatibility, pass "false" instead.
And there's a new Text Layout option for text views created in Interface Builder. This new option gives you control of which layout system to use on a per-instance basis. The default setting is the system default, which is TextKit 2.
You can also choose to explicitly use TextKit 2 or TextKit 1.
TextKit 2 now supports non-simple text containers. Non-simple text containers may have holes or gaps in them. This allows text to wrap around images or other inline content.
To create a non-simple text container, use the exclusionPaths property on NSTextContainer to define the areas where text should not be laid out. For an example of how to do this, check out the TextKitAndTextView sample code from the resources associated with this video. You can find the related example on the "exclusion path" tab.
We've enhanced the line breaking engine in TextKit 2 to choose more even line breaks for justified paragraphs. This is a subtle change that's easier to notice on longer paragraphs of text.
Here we have two versions of the same text, laid out in the same area.
Notice the stretched out lines and large interword spacing with traditional line breaking.
There's much less of that going on with the new even line breaking. This makes the text easier to read, and you get it for free with TextKit 2. There's no adoption required.
And finally, we've added text list support in TextKit 2 for all platforms. With text lists, you can programmatically create numbered or bulleted lists for display in a text view. TextKit 2 uses NSTextList to represent text lists, just like TextKit 1. NSTextList used to be available in AppKit only, but in iOS 16, it's available in UIKit too.
Use NSTextList together with NSmutableParagraphStyle to specify that a paragraph in your text storage should be formatted as a list for display. The text view is responsible for picking up these attributes from the text storage and reformatting the paragraph content to look like a list.
While NSTextList itself isn't new, there are a few new TextKit 2 additions. Since lists can have nested items, it's natural to represent them as a tree structure. In TextKit 2, we've enhanced NSTextElement to support structuring them as trees with properties for accessing child and parent elements.
And we've added a new element subclass called NSTextListElement. When the content manager comes across a NSTextList in the text content, it will generate NSTextListElements to represent the items in the list.
To get a more in-depth view of how to create text lists and add items, refer to the TextKitAndTextView sample code. You can find the related example on the "list" tab.
And while you're exploring the sample code, don't miss the text attachment example which shows how to use the text attachment view provider APIs in TextKit 2.
These APIs let you use a UI or NSView as the text attachment, and events can be handled directly by the attachment view. This makes event handling with text attachments a whole lot easier, and it's only possible with TextKit 2. All right, that's it for what's new in TextKit 2. Next, I'll get into the details of the TextKit 1 compatibility mode. Since TextKit 2 is such a radical departure from the design of TextKit 1, we understand that full adoption of TextKit 2 may take some time for apps that are heavily invested in the TextKit 1 architecture. We want these apps to continue to work well until the transition can be made, and that's why we've added a special TextKit 1 compatibility mode for UITextView and NSTextView. When you explicitly call an NSLayoutManager API, the text view replaces its NSTextLayoutManager with an NSLayoutManager and reconfigures itself to use TextKit 1. This can also happen if the text view encounters attributes not yet supported by TextKit 2, such as tables, or when printing.
If you encounter an unexpected runtime fallback to TextKit 1 in UITextView, check the log for a message warning about the switch. Set a breakpoint on the symbol underscore UITextViewEnablingCompatibilityMode to capture a stack trace and other useful debugging information.
For NSTextView, you can get more information about unexpected runtime fallbacks by subscribing to the willSwitch or didSwitchToNSLayoutManager notifications.
If you must drop back to TextKit 1, it's best to opt out at initialization time with programmatically initialized text views. Do this by using your own text container and a TextKit 1 layout manager.
Another option is to use the new convenience constructor to initialize a TextKit 1 text view and pass false as the parameter. This will make your text view use TextKit 1.
And a third option is to use Interface Builder and set the new Text Layout option to TextKit 1 on your text view.
Here's something to watch out for. If you're swapping out your text container's layout manager during or after initialization, then your text view will fall back to TextKit 1 as designed. It's inefficient to create all the TextKit 2 objects during initialization only to throw them away moments later. There's also potential user side effects, depending on the timing. If it happens during typing, the text view could lose its focus and interrupt input, requiring the text view to be selected again to resume. Avoid this by opting the text view out at initialization time. Now that you know all about compatibility mode, it's time to talk about how to avoid it altogether by modernizing your app and adopting TextKit 2. And there's one really important thing I want you to remember.
There can be only one layout manager per text view. A text view can't have both an NSTextLayoutManager and an NSLayoutManager at the same time.
Once a text view switches to TextKit 1, there's no automatic way of going back. The process of switching layout systems is expensive, and you will lose any UI state that was present at the time of the switch. So for optimum performance and usability, the system will never switch a text view back to TextKit 2 from TextKit 1. It's a one-way operation.
This means it's really important to avoid compatibility mode. And there's a few different reasons a text view will enter compatibility mode. The number one reason for a text view to enter compatibility mode is accessing the text view's layoutManager property. The other reasons are much less common.
So an important strategy is to avoid accessing the text view's layout manager property. Also avoid accessing the layout manager through the text view's text container. Audit your code for uses of these properties, and remove them or replace them with TextKit 2 equivalents.
If you're deploying your app to older OS versions that don't have TextKit 2, you might not be able to entirely remove your layoutManager code.
In that case, you should first check for the text view's NSTextLayoutManager.
Put your TextKit 2 code in the if clause and put the TextKit 1 code in the else clause, including the layoutManager access. This way, the TextKit 1 code only runs when TextKit 2 is not available, and your layoutManager query won't cause an unintended fallback to TextKit 1.
If you've followed all this advice and you still encounter an unexpected fallback to TextKit 1 coming from the system, that's our problem, so please report the issue with Feedback Assistant. Include a capture of the stack trace at the time of fallback, which you can get from breaking on underscore UITextViewEnablingCompatibilityMode in UIKit, or willSwitchToNSLayoutManagerNotification in AppKit.
Okay, now I'll get into the specifics of updating code related to TextKit 1 types, starting with NSLayoutManager. Once you've audited your code for NSLayoutManager queries, you'll need to figure out the TextKit 2 equivalents with NSTextLayoutManager.
Some layout manager APIs have similar names between TextKit 1 and 2, and the substitutions are straightforward. Here's a few examples. In TextKit 1, you call usedRect(for: textContainer) on NSLayoutManager to get the bounding rectangle for the text inside a text container. In TextKit 2, you get this from the usageBoundsForTextContainer property on NSTextLayoutManager.
In TextKit 1, we used the name "temporary attributes" for attributes that affected only the rendering, and not the layout. In TextKit 2, we more accurately call those "rendering attributes." But there are some TextKit 1 APIs that have no direct equivalents in TextKit 2. To understand why, you need to understand there is no correct character to glyph mapping for many words in Indic scripts like Kannada.
In these scripts, glyphs can be split up, reordered, recombined, or even deleted.
The glyph-based APIs on NSLayoutManager assume you can directly associate a contiguous range of characters with a contiguous range of glyphs, and that's just not true for all scripts. Using these APIs can result in broken layout and rendering for text written in scripts like Kannada. That's why there are zero glyph APIs in TextKit 2. You can't just substitute a single TextKit 2 API for a TextKit 1 glyph API. Replacing these APIs requires a different approach.
So here's how to update glyph-based code. The first step is to identify which glyph APIs you're using. Next, look at how you're using those APIs and define what you are trying to do at a high level. Glyph-based code is very low level, and there are many details that aren't relevant to your high-level task.
Once you've defined the high-level task, then examine the structures available to you in TextKit 2 such as layout fragments, line fragments, and text selections. These can help you accomplish your task. For example, consider this TextKit 1 code. There's two glyph APIs used here: numberOfGlyphs, and lineFragmentRect(forGlyphAt: index) . This TextKit 1 code is iterating over all of the glyphs in the document and counting the line fragment rects. The high-level task is counting the number of lines of wrapped text in the text view. Since this code is working with line fragment rects, the TextKit 2 structures to use are NSTextLineFragment and NSTextLayoutFragment.
And here's the code rewritten to use TextKit 2. Instead of iterating over glyphs, it's enumerating the text layout fragments in the document and supplying a closure that counts all of the text line fragments within each layout fragment.
Keep that example in mind when updating your own code for TextKit 2. Now I'm going to shift gears and discuss updating code that's based on NSRange.
TextKit 1 uses NSRange to index into text content, and NSRange is a linear index into a string. For the text "Hello TextKit 2!" exclamation point, the NSRange that represents the "TextKit 2 exclamation point" is location 6 and length 10, since it begins at the 6th character and it's 10 characters long. This linear model is easy to understand, and it works great for indexing into strings.
But the linear model doesn't work for indexing into any content that has more structure than a string. Here's an example. HTML documents are represented as a tree structure, where each tag is a node in the tree. If our Hello TextKit 2! text is part of an HTML document, there's no way for our NSRange to tell us that the text is inside the span tag, nested 3 levels deep. The linear model isn't expressive enough to store that information, so we can't use it to index into a nested structure like this one. This is why TextKit 2 added new types for representing ranges in the text content. NSTextLocation is an object that represents a single location inside the text content. NSTextRange consists of a start and end location. The end location is excluded from the range. These new types can represent the nested structure of this HTML document by defining a location as the DOM node plus a character offset.
Since NSTextLocation is a protocol, any custom object can be a location as long as it implements the NSTextLocation protocol methods. This is crucial infrastructure for working with different types of backing stores that support structured data in their models.
But text views are built on NSAttributedString backing stores that don't have this structure, and we can't change that without breaking lots of apps, including yours. So you'll continue to use NSRange when using text view APIs like selectedRange or scrollRangeToVisible. And you'll need to convert between NSRange and NSTextRange when communicating with the TextKit 2 layout manager or content manager.
To convert a text view's NSRange to an NSTextRange, define the location as the integer index into the attributed string.
Use the NSRange location as the start location for NSTextRange.
Use the NSRange location plus the length as the end location of the NSTextRange. Conceptually, that's how to map from NSRange to NSTextRange.
In practice, the code looks a little different because NSTextLocations must be objects.
You need to go through the content manager to compute the locations.
For the start location, ask the content manager for the location of the beginning of the document, then offset it by the NSRange's location. Then offset the start location by the NSRange's length to get the end location.
To go in the other direction, use the text content manager to get two different offsets.
The NSRange's location is the offset between the beginning of the document and the NSTextRange's location. And the NSRange's length is the offset between the start and end locations of the NSTextRange.
UITextViews and UITextFields conform to the UITextInput protocol, which uses UITextPosition and range. Most of the time, you won't need to convert a UITextRange directly to an NSTextRange when using UITextView or UITextField. But if you do, use the integer offsets as the intermediary between the two range types.
On the other hand, if you're using a custom view with UITextInput, you have direct control over the UITextPosition and UITextRange subclasses used with your view. You can make your UITextPosition subclass conform to NSTextLocation, implement the required method, and use your subclass to create NSTextRanges directly.
Finally, here's a reminder to avoid reusing UITextPosition objects across different views, even if the content in both views is similar. A UITextPosition is only valid for the view used to create it.
All right, now you've got lots of strategies at your disposal for modernizing your code. Apply these strategies, and your app will be ready to reap the benefits of TextKit 2.
And that's what's new in TextKit and text views. I covered a lot of great improvements in TextKit 2 and shared some strategies for updating your apps while maintaining compatibility for older OS versions. Use TextKit 2 in your apps today to take full advantage of the new improvements. Check your text views to make sure they aren't unintentionally falling back to TextKit 1. And finally, employ the modernization strategies to get your app on TextKit 2. We can't wait to read what you'll create with TextKit 2 and text views. Thanks for watching!
let textContentManager = textLayoutManager.textContentManager
let location = textContentManager.offset(from: textContentManager.documentRange.location,
let length = textContentManager.offset(from: nsTextRange!.location,
let nsRange =NSRange(location: location, length: length)