How is TextKit structured, how do TextKit 1 and 2 differ, and how do UILabel, UITextView and SwiftUI Text relate?
Predict first
Does a new text view use TextKit 2, and what happens when you read its layoutManager?
import UIKit
let tv = UITextView()
print(tv.textLayoutManager != nil)
_ = tv.layoutManager
print(tv.textLayoutManager == nil)
- true, then true
- false, then true
- true, then false
Predict first
What does a TextKit 1 layout manager report for text in a 60-point-wide container?
import UIKit
let st = NSTextStorage(string: "Hello world, this wraps")
let lm = NSLayoutManager()
let tc = NSTextContainer(size: CGSize(width: 60, height: CGFloat.greatestFiniteMagnitude))
st.addLayoutManager(lm)
lm.addTextContainer(tc)
lm.ensureLayout(for: tc)
let used = lm.usedRect(for: tc)
var lines = 0
lm.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: lm.numberOfGlyphs)) { _, _, _, _, _ in lines += 1 }
print(used.width <= 60, lines > 1)
- true true
- false true
- true false
Mental model. The storage is the manuscript, the layout engine is the typesetter, and the container is the page shape. TextKit 2 sets only the pages you are about to see; TextKit 1 can set the whole book.
TextKit separates three roles. The text storage (NSTextStorage, an NSMutableAttributedString) holds the characters and attributes (font, colour, paragraph style, attachments). The layout engine turns them into positioned lines and glyphs. The text container (NSTextContainer) defines the region the text flows into (a width, a height, exclusion paths for text wrapping around shapes, a line-fragment padding and a maximum line count). In TextKit 1 the layout engine is NSLayoutManager, which works with glyphs and can answer usedRect(for:), line-fragment enumeration and glyph-to-character mapping. TextKit 2 (iOS 15+, the default for UITextView, UITextField and labels on recent systems) uses NSTextLayoutManager and NSTextContentManager, works with text elements and layout fragments instead of glyphs, lays out lazily (so very long documents are fast), and handles complex scripts and non-contiguous layout better. Its API is different: no glyph indexes, and viewport-based layout.
The two are not mixed: a UITextView uses TextKit 2 by default, and the moment you access its layoutManager property it silently falls back to TextKit 1 (and textLayoutManager becomes nil), which is a common surprise that changes performance and behaviour. To opt in deliberately, create the view with usingTextLayoutManager: false. UILabel and UIButton use the same engine through simpler APIs: they measure with intrinsicContentSize and wrap by their width (numberOfLines = 0 and preferredMaxLayoutWidth or constraints). To measure text yourself, use boundingRect(with:options:context:) with .usesLineFragmentOrigin, or a layout manager with a container sized to your width. SwiftUI Text is a separate, value-based API that uses the same text system underneath, supports attributed strings, Markdown, interpolation and Dynamic Type, and for editing you wrap a UITextView or use TextEditor. Always use Dynamic Type fonts (UIFont.preferredFont(forTextStyle:), adjustsFontForContentSizeCategory) and test long text, right-to-left languages and large sizes.
A new text view starts on TextKit 2. Touching the TextKit 1 layout manager switches it over, and the TextKit 2 manager is gone.
The text wraps inside the container, so the used width stays within 60 points and it takes several line fragments.
Common traps
- Reading
textView.layoutManager“just to look”. It switches the view to TextKit 1 for good; usetextLayoutManager. - Measuring text with a different width or options than will be used. The wrapped height differs; measure with the real container width.
They might follow up with
- How do you keep text legible for everyone? Dynamic Type styles,
adjustsFontForContentSizeCategoryand layouts that grow. - What is an exclusion path? A shape the text flows around inside the container.