TextKit 2 (NSTextLayoutManager) API was announced publicly during WWDC21, which is over 4 years ago. Before that, it was in private development for a few years and gained widespread adoption in the macOS and iOS frameworks. Promised an easier, faster, overall better API and text layout engine that replaces the aged TextKit 1 (NSLayoutManager) engine.
Over the years, I gained some level of expertise in TextKit 2 and macOS/iOS text processing, which resulted in STTextView - a re-implementation of TextView for macOS (AppKit) and iOS (UIKit) using TextKit 2 framework as a text layout engine, as well as public speaking praising the new, better engine we've just got to solve all the problems.
Based on my 4 years of experience working with it, I feel like I fell into a trap. It's not a silver bullet. It is arguably an improvement over TextKit 1. I want to discuss certain issues that make the TextKit 2 annoying to use (at best) and not the right tool for the job (at the worst)
The architecture & implementation
The TextKit2 architecture is good. The abstraction and the components make a lot of sense and deliver on the premise of progressive complexity. BUT the implementation is less so on par with the architecture. On the one side, NSTextContentManager provides an abstract interface for the layout engine. In practice, using anything other than NSTextContentStorage is impossible. NSTextContentStorage is one (and the only) provided implementation of the storage that works. That itself is backed by NSTextStorage, which is an abstract interface for the content storage itself - meaning all the problems I may have with NSTextStorage apply to TextKit 2 as well. In short, the UITextView/NSTextView won't work with anything other than NSTextContentStorage.
Text content manager operates on a series of NSTextElement blocks, but again, the only working implementation must inherit from NSTextParagraph, or you're in trouble (runtime assertions).
The implementation is inconsistent, and it seems intentional. TextKit2 is implemented to be used by UITextView, and that is quickly obvious. What a waste of a great idea that could have been otherwise.
Bugs in software are expected, and for TextKit 2, it's no exception. I reported many bugs myself. Some issues are fixed, while others remain unresolved. Many users received no response. Additionally, bugs occur in specific versions, and regressions are common. It is annoying to maintain compatibility, of course. From my perspective, probably the most annoying bugs are around the "extra line fragment" (the rectangle for the extra line fragment at the end of a document) and its broken layout.
Viewport is a struggle
Real struggle, though, is around the newly introduced idea of the viewport and how it works. Viewport is a tool that optimizes text layout engine work and minimizes memory footprint by focusing on the visible area, rather than the entire document, all the time. Viewport is a small portion of the visible area that "moves" as the user interacts with different parts of the document (eg, scrolling moves the viewport frame)

The viewport promise is that I don't have to ensure the layout of the whole document to get the layout of a random fragment of the document, and only layout lazily fragments that are actually important to display. To make this feature work, it requires various caching, managing intervals, invalidating ranges, and other related tasks; the TextKit 2 framework handles all of that.
Here's the stage: imagine you have a window with a text in it. Text scrolls up and down; as you scroll, the visible area displays the layout text. So, a typical text editor/viewer scenario.

One of the problems with viewport management is the very same thing that is the feature of the viewport. When ensuring layout only in the viewport (visible area), all other parts of the document are estimated. Specifically, the total height of the document is estimated. The estimation changes frequently as I lay out more/different parts of the document. That happens when I move the viewport while scrolling up/down. TextKit updates the value of NSTextLayoutManager.usageBoundsForTextContainer whenever the estimates change. The recipe to estimate the total height of the document is
- ensureLayout(for: documentRange.endLocation) that says, ensure layout of the end of the document, without forcing layout of the whole document. That operation, by definition, results in an estimated size.
- Resize the view to match the usageBoundsForTextContainer value. In a scrollview, this results in an update of the scroller to reflect the current document position.
The first problem I notice with this approach is that as I scroll the document and continue to lay out the changing viewport position, the value of usageBoundsForTextContainer is unstable. It frequently changes value significantly. In a scrollview, such frequent and significant changes to the height result in "juggery" of the scroller position and size

The jiggery is super annoying and hard to accept. This is also expected, given that the height is estimated. Works as-designed:
This is correct and as-designed – The viewport-based layout in TextKit2 doesn't require that the document is fully laid out; it just needs that the part of text to be displayed on screen is laid out, and that is the way it achieves a better scrolling performance.

A slightly "better" as a more stable value (from my observation), I receive when asking for the location of the last "layout element", using enumerateTextLayoutFragments and asking for the layout frame of the last, and only last fragment.
enumerateTextLayoutFragments(from: documentRange.endLocation, options: [.reverse, .ensuresLayout]) {
layoutFragment in lastLineMaxY = layoutFragment.layoutFragmentFrame.maxY
return false
}
That estimation is also just an estimate, and usually the value is significantly higher than the final, fully laid out document. How do I jump to the end of the document? The answer is:
- receive an estimated (too big or too small) content height
- update the view content size with the estimated height
- enforce layout at the end of the document
- move (relocate) the viewport to the end of that height (either final or estimated)
And yes, the viewport will display the end of the document, but the total height of the content is still estimated, meaning the scroller is most likely at the wrong position (it is wrong). What's the "fix" to that? The best guess is to artificially and contiusly "adjust" viewport position, meaning: the view scroll to estimated bottom of the document. Still, we ignore that fact and recognize that fact (from the context) and "fake" the viewport to display end of the document at that position, even if that position is way out of bounds of the document size. That operation (more likely, I need more adjustments like this) is fragile, and frankly, not easy to handle in a way that is not noticeable.
For a long time, I thought that I "hold it wrong" and there must be a way (maybe a private API) that addresses these problems, then I realized I'm not wrong. TextEdit app from macOS suffers from the very same issues I do in my implementations:
TextEdit and TextKit 2 glitches. if you know where to push button.
TextEdit and TextKit 2 glitches. if you know where to push button.
So, so
Today, I believe that's not me. The TextKit 2 API and its implementation are lacking and unexpectedly difficult to use correctly. While the design is solid, it proved challenging to apply in real-world applications. I wish I had a better or more optimistic summary of my findings, but it is what it is. I've started to think that TextKit 2 might not be the best tool for text layout, especially when it comes to text editing UI. I remain open to suggestions, and hopefully, I will find a way to use TextKit 2 without compromising user experience.