Text Measurement & Sizing Reference
Use when measuring text size, calculating bounding rects, sizing text views to fit content, or getting line-level metrics. Covers NSString/NSAttributedString measurement, NSStringDrawingOptions, NSStringDrawingContext, TextKit 1 glyph-range measurement, TextKit 2 layout fragment measurement, and common sizing mistakes.
Use when measuring text size, calculating bounding rects, sizing text views to fit content, or getting line-level metrics. Covers NSString/NSAttributedString measurement, NSStringDrawingOptions, NSStringDrawingContext, TextKit 1 glyph-range measurement, TextKit 2 layout fragment measurement, and common sizing mistakes.
Family: TextKit Runtime And Layout
Use this skill when you need to know how big text will be before (or after) rendering it.
When to Use
Section titled “When to Use”- You need
boundingRectorsize(withAttributes:)and it’s returning wrong values. - You’re sizing a view to fit text content.
- You need line-by-line metrics (line heights, fragment rects).
- You’re calculating
intrinsicContentSizefor a custom text view. - Text is clipping, truncating unexpectedly, or leaving extra space.
Quick Decision
Section titled “Quick Decision”- Quick single-line measurement ->
NSAttributedString.size() - Multi-line measurement in a constrained width ->
boundingRect(with:options:context:) - Per-line metrics in TextKit 1 ->
NSLayoutManager.enumerateLineFragments - Per-line metrics in TextKit 2 ->
NSTextLayoutManager.enumerateTextLayoutFragments - “How tall should my text view be?” -> use the text system, not manual calculation
The #1 Mistake
Section titled “The #1 Mistake”// WRONG — returns single-line size, ignores line wrappinglet size = myString.size(withAttributes: attrs)
// RIGHT — constrains to width, enables multi-line measurementlet rect = myString.boundingRect( with: CGSize(width: maxWidth, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attrs, context: nil)let measuredSize = CGSize(width: ceil(rect.width), height: ceil(rect.height))You must pass .usesLineFragmentOrigin for multi-line measurement. Without it, boundingRect measures as if the text is a single line.
NSString / NSAttributedString Measurement
Section titled “NSString / NSAttributedString Measurement”size(withAttributes:) / size()
Section titled “size(withAttributes:) / size()”// NSString — single line onlylet size = "Hello".size(withAttributes: [.font: UIFont.systemFont(ofSize: 17)])
// NSAttributedString — single line onlylet size = attributedString.size()Limitations: Always returns the single-line size. No width constraint. Useless for multi-line text.
Use for: Badge labels, single-line metrics, width-only calculations.
boundingRect (the workhorse)
Section titled “boundingRect (the workhorse)”// NSString versionlet rect = string.boundingRect( with: CGSize(width: containerWidth, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font, .paragraphStyle: paragraphStyle], context: nil)
// NSAttributedString version (attributes come from the string itself)let rect = attributedString.boundingRect( with: CGSize(width: containerWidth, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)Always ceil() the result. boundingRect returns fractional values. Passing them directly to layout causes 1-pixel clipping:
let height = ceil(rect.height) // Not rect.heightlet width = ceil(rect.width) // Not rect.widthNSStringDrawingOptions
Section titled “NSStringDrawingOptions”| Option | Effect | When to use |
|---|---|---|
.usesLineFragmentOrigin | Measures multi-line text using line fragment origins | Almost always. Without this, you get single-line measurement. |
.usesFontLeading | Includes font leading (inter-line spacing from the font) in height | Almost always. Matches what UILabel/UITextView actually renders. |
.usesDeviceMetrics | Uses actual glyph bounds instead of typographic bounds | Pixel-perfect rendering. Rarely needed. |
.truncatesLastVisibleLine | Accounts for truncation ellipsis in height-constrained measurement | When you’re constraining height and want accurate truncated size. |
The standard combo: [.usesLineFragmentOrigin, .usesFontLeading] — use this by default.
NSStringDrawingContext
Section titled “NSStringDrawingContext”For auto-shrinking text (like UILabel’s adjustsFontSizeToFitWidth):
let context = NSStringDrawingContext()context.minimumScaleFactor = 0.5 // Allow shrinking to 50%
let rect = attributedString.boundingRect( with: constrainedSize, options: [.usesLineFragmentOrigin, .usesFontLeading], context: context)
// After measurement:let actualScale = context.actualScaleFactor // What scale was appliedlet actualBounds = context.totalBounds // Where text actually landedTextKit 1 Measurement (NSLayoutManager)
Section titled “TextKit 1 Measurement (NSLayoutManager)”When you need per-line metrics, not just total size.
Total content size
Section titled “Total content size”// Force layout for entire containerlayoutManager.ensureLayout(for: textContainer)
// Get used rect — the actual area text occupieslet usedRect = layoutManager.usedRect(for: textContainer)let contentHeight = ceil(usedRect.height)Specific range size
Section titled “Specific range size”let glyphRange = layoutManager.glyphRange(forCharacterRange: charRange, actualCharacterRange: nil)let boundingRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)Line-by-line enumeration
Section titled “Line-by-line enumeration”let fullGlyphRange = layoutManager.glyphRange(for: textContainer)layoutManager.enumerateLineFragments(forGlyphRange: fullGlyphRange) { rect, usedRect, container, glyphRange, stop in // rect: full line fragment rectangle (includes padding) // usedRect: actual area used by glyphs (tighter) // glyphRange: which glyphs are on this line print("Line height: \(usedRect.height), y: \(usedRect.origin.y)")}Line count
Section titled “Line count”func lineCount(for layoutManager: NSLayoutManager, in textContainer: NSTextContainer) -> Int { layoutManager.ensureLayout(for: textContainer) var count = 0 let fullRange = layoutManager.glyphRange(for: textContainer) layoutManager.enumerateLineFragments(forGlyphRange: fullRange) { _, _, _, _, _ in count += 1 } return count}TextKit 2 Measurement (NSTextLayoutManager)
Section titled “TextKit 2 Measurement (NSTextLayoutManager)”Total content size
Section titled “Total content size”textLayoutManager.ensureLayout(for: textLayoutManager.documentRange)let usageBounds = textLayoutManager.usageBoundsForTextContainerlet contentHeight = ceil(usageBounds.height)Layout fragment enumeration
Section titled “Layout fragment enumeration”textLayoutManager.enumerateTextLayoutFragments( from: textLayoutManager.documentRange.location, options: [.ensuresLayout]) { fragment in let frame = fragment.layoutFragmentFrame // Position in container let surface = fragment.renderingSurfaceBounds // Actual rendering area
for lineFragment in fragment.textLineFragments { let lineOrigin = lineFragment.typographicBounds.origin let lineHeight = lineFragment.typographicBounds.height print("Line at y=\(frame.origin.y + lineOrigin.y), height=\(lineHeight)") } return true // continue enumeration}NSTextLineFragment metrics
Section titled “NSTextLineFragment metrics”Each NSTextLineFragment within a layout fragment gives you:
lineFragment.typographicBounds // Bounds based on font metricslineFragment.glyphOrigin // Where glyphs startlineFragment.characterRange // Character range for this lineCommon Sizing Patterns
Section titled “Common Sizing Patterns””Size text view to fit content”
Section titled “”Size text view to fit content””UITextView:
// Let the text system do the worklet fittingSize = textView.sizeThatFits( CGSize(width: maxWidth, height: .greatestFiniteMagnitude))textView.frame.size.height = fittingSize.heightWith Auto Layout (preferred):
textView.isScrollEnabled = false // CRITICAL — enables intrinsicContentSize// Auto Layout handles the rest via intrinsicContentSizeisScrollEnabled = false is the key. When scrolling is enabled, intrinsicContentSize returns (.noIntrinsicMetric, .noIntrinsicMetric). When disabled, it returns the full content size.
”How tall is N lines of text?"
Section titled “”How tall is N lines of text?"”func heightForLines(_ n: Int, font: UIFont, width: CGFloat) -> CGFloat { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineBreakMode = .byWordWrapping
// Build a string with N-1 newlines let sampleText = String(repeating: "Wy\n", count: n).dropLast() let attrs: [NSAttributedString.Key: Any] = [ .font: font, .paragraphStyle: paragraphStyle ] let rect = (sampleText as NSString).boundingRect( with: CGSize(width: width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attrs, context: nil ) return ceil(rect.height)}"Does text fit in this rect?”
Section titled “"Does text fit in this rect?””func textFits(_ text: NSAttributedString, in size: CGSize) -> Bool { let rect = text.boundingRect( with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil ) return ceil(rect.height) <= size.height && ceil(rect.width) <= size.width}Pitfalls
Section titled “Pitfalls”-
Forgetting
.usesLineFragmentOrigin— Without it, multi-line text is measured as one line. This is the single most common measurement bug. -
Not calling
ceil()— Fractional heights cause 1px clipping. Always round up. -
Mismatched attributes — Measuring with font A but rendering with font B. Ensure the same paragraph style, font, and line height are used for both measurement and display.
-
textContainer.lineFragmentPadding— Default is 5pt. This adds 10pt total width (5 each side). If you measure without accounting for it, your widths are 10pt off:let effectiveWidth = containerWidth - 2 * textContainer.lineFragmentPadding -
textContainerInset— UITextView default isUIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0). You must add these to the measured text height for the actual view height. -
Measuring before layout — In TextKit, measurements are only valid after layout. Call
ensureLayout(for:)before reading rects. -
Thread safety — All measurement APIs that touch NSLayoutManager or NSTextLayoutManager must be called from the main thread (or the layout queue for TK2).
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-measurement reference skill. Use it when the subsystem is already known and you need mechanics, behavior, or API detail.
Related
Section titled “Related”apple-text-textkit1-ref: Use when the user is already on TextKit 1 and needs exact NSLayoutManager, NSTextStorage, or NSTextContainer APIs, glyph and layout lifecycle details, temporary attributes, exclusion paths, or multi-container behavior. Reach for this when the stack choice is already made and the task is reference-level TextKit 1 mechanics, not stack selection or symptom-first debugging.apple-text-textkit2-ref: Use when the user is already on TextKit 2 and needs exact NSTextLayoutManager, NSTextContentManager, NSTextContentStorage, viewport layout, fragment, rendering-attribute, or migration details. Reach for this when the stack choice is already made and the task is reference-level TextKit 2 mechanics, not stack selection or generic text-system debugging.apple-text-viewport-rendering: Use when the user needs to understand how Apple text actually renders on screen: viewport layout, line-fragment geometry, rendering attributes, font substitution, fixAttributes, scroll-driven layout, or TextKit versus Core Text drawing differences. Reach for this when the issue is rendering mechanics, not generic layout invalidation.
Full SKILL.md source
---name: apple-text-measurementdescription: > Use when measuring text size, calculating bounding rects, sizing text views to fit content, or getting line-level metrics. Covers NSString/NSAttributedString measurement, NSStringDrawingOptions, NSStringDrawingContext, TextKit 1 glyph-range measurement, TextKit 2 layout fragment measurement, and common sizing mistakes.license: MIT---
# Text Measurement & Sizing Reference
Use this skill when you need to know how big text will be before (or after) rendering it.
## When to Use
- You need `boundingRect` or `size(withAttributes:)` and it's returning wrong values.- You're sizing a view to fit text content.- You need line-by-line metrics (line heights, fragment rects).- You're calculating `intrinsicContentSize` for a custom text view.- Text is clipping, truncating unexpectedly, or leaving extra space.
## Quick Decision
- Quick single-line measurement -> `NSAttributedString.size()`- Multi-line measurement in a constrained width -> `boundingRect(with:options:context:)`- Per-line metrics in TextKit 1 -> `NSLayoutManager.enumerateLineFragments`- Per-line metrics in TextKit 2 -> `NSTextLayoutManager.enumerateTextLayoutFragments`- "How tall should my text view be?" -> use the text system, not manual calculation
## The #1 Mistake
```swift// WRONG — returns single-line size, ignores line wrappinglet size = myString.size(withAttributes: attrs)
// RIGHT — constrains to width, enables multi-line measurementlet rect = myString.boundingRect( with: CGSize(width: maxWidth, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attrs, context: nil)let measuredSize = CGSize(width: ceil(rect.width), height: ceil(rect.height))```
**You must pass `.usesLineFragmentOrigin`** for multi-line measurement. Without it, `boundingRect` measures as if the text is a single line.
## NSString / NSAttributedString Measurement
### size(withAttributes:) / size()
```swift// NSString — single line onlylet size = "Hello".size(withAttributes: [.font: UIFont.systemFont(ofSize: 17)])
// NSAttributedString — single line onlylet size = attributedString.size()```
**Limitations:** Always returns the single-line size. No width constraint. Useless for multi-line text.
**Use for:** Badge labels, single-line metrics, width-only calculations.
### boundingRect (the workhorse)
```swift// NSString versionlet rect = string.boundingRect( with: CGSize(width: containerWidth, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font, .paragraphStyle: paragraphStyle], context: nil)
// NSAttributedString version (attributes come from the string itself)let rect = attributedString.boundingRect( with: CGSize(width: containerWidth, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)```
**Always `ceil()` the result.** `boundingRect` returns fractional values. Passing them directly to layout causes 1-pixel clipping:
```swiftlet height = ceil(rect.height) // Not rect.heightlet width = ceil(rect.width) // Not rect.width```
### NSStringDrawingOptions
| Option | Effect | When to use ||--------|--------|-------------|| `.usesLineFragmentOrigin` | Measures multi-line text using line fragment origins | **Almost always.** Without this, you get single-line measurement. || `.usesFontLeading` | Includes font leading (inter-line spacing from the font) in height | **Almost always.** Matches what UILabel/UITextView actually renders. || `.usesDeviceMetrics` | Uses actual glyph bounds instead of typographic bounds | Pixel-perfect rendering. Rarely needed. || `.truncatesLastVisibleLine` | Accounts for truncation ellipsis in height-constrained measurement | When you're constraining height and want accurate truncated size. |
**The standard combo:** `[.usesLineFragmentOrigin, .usesFontLeading]` — use this by default.
### NSStringDrawingContext
For auto-shrinking text (like UILabel's `adjustsFontSizeToFitWidth`):
```swiftlet context = NSStringDrawingContext()context.minimumScaleFactor = 0.5 // Allow shrinking to 50%
let rect = attributedString.boundingRect( with: constrainedSize, options: [.usesLineFragmentOrigin, .usesFontLeading], context: context)
// After measurement:let actualScale = context.actualScaleFactor // What scale was appliedlet actualBounds = context.totalBounds // Where text actually landed```
## TextKit 1 Measurement (NSLayoutManager)
When you need per-line metrics, not just total size.
### Total content size
```swift// Force layout for entire containerlayoutManager.ensureLayout(for: textContainer)
// Get used rect — the actual area text occupieslet usedRect = layoutManager.usedRect(for: textContainer)let contentHeight = ceil(usedRect.height)```
### Specific range size
```swiftlet glyphRange = layoutManager.glyphRange(forCharacterRange: charRange, actualCharacterRange: nil)let boundingRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)```
### Line-by-line enumeration
```swiftlet fullGlyphRange = layoutManager.glyphRange(for: textContainer)layoutManager.enumerateLineFragments(forGlyphRange: fullGlyphRange) { rect, usedRect, container, glyphRange, stop in // rect: full line fragment rectangle (includes padding) // usedRect: actual area used by glyphs (tighter) // glyphRange: which glyphs are on this line print("Line height: \(usedRect.height), y: \(usedRect.origin.y)")}```
### Line count
```swiftfunc lineCount(for layoutManager: NSLayoutManager, in textContainer: NSTextContainer) -> Int { layoutManager.ensureLayout(for: textContainer) var count = 0 let fullRange = layoutManager.glyphRange(for: textContainer) layoutManager.enumerateLineFragments(forGlyphRange: fullRange) { _, _, _, _, _ in count += 1 } return count}```
## TextKit 2 Measurement (NSTextLayoutManager)
### Total content size
```swifttextLayoutManager.ensureLayout(for: textLayoutManager.documentRange)let usageBounds = textLayoutManager.usageBoundsForTextContainerlet contentHeight = ceil(usageBounds.height)```
### Layout fragment enumeration
```swifttextLayoutManager.enumerateTextLayoutFragments( from: textLayoutManager.documentRange.location, options: [.ensuresLayout]) { fragment in let frame = fragment.layoutFragmentFrame // Position in container let surface = fragment.renderingSurfaceBounds // Actual rendering area
for lineFragment in fragment.textLineFragments { let lineOrigin = lineFragment.typographicBounds.origin let lineHeight = lineFragment.typographicBounds.height print("Line at y=\(frame.origin.y + lineOrigin.y), height=\(lineHeight)") } return true // continue enumeration}```
### NSTextLineFragment metrics
Each `NSTextLineFragment` within a layout fragment gives you:
```swiftlineFragment.typographicBounds // Bounds based on font metricslineFragment.glyphOrigin // Where glyphs startlineFragment.characterRange // Character range for this line```
## Common Sizing Patterns
### "Size text view to fit content"
**UITextView:**```swift// Let the text system do the worklet fittingSize = textView.sizeThatFits( CGSize(width: maxWidth, height: .greatestFiniteMagnitude))textView.frame.size.height = fittingSize.height```
**With Auto Layout (preferred):**```swifttextView.isScrollEnabled = false // CRITICAL — enables intrinsicContentSize// Auto Layout handles the rest via intrinsicContentSize```
**`isScrollEnabled = false` is the key.** When scrolling is enabled, `intrinsicContentSize` returns `(.noIntrinsicMetric, .noIntrinsicMetric)`. When disabled, it returns the full content size.
### "How tall is N lines of text?"
```swiftfunc heightForLines(_ n: Int, font: UIFont, width: CGFloat) -> CGFloat { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineBreakMode = .byWordWrapping
// Build a string with N-1 newlines let sampleText = String(repeating: "Wy\n", count: n).dropLast() let attrs: [NSAttributedString.Key: Any] = [ .font: font, .paragraphStyle: paragraphStyle ] let rect = (sampleText as NSString).boundingRect( with: CGSize(width: width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attrs, context: nil ) return ceil(rect.height)}```
### "Does text fit in this rect?"
```swiftfunc textFits(_ text: NSAttributedString, in size: CGSize) -> Bool { let rect = text.boundingRect( with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil ) return ceil(rect.height) <= size.height && ceil(rect.width) <= size.width}```
## Pitfalls
1. **Forgetting `.usesLineFragmentOrigin`** — Without it, multi-line text is measured as one line. This is the single most common measurement bug.
2. **Not calling `ceil()`** — Fractional heights cause 1px clipping. Always round up.
3. **Mismatched attributes** — Measuring with font A but rendering with font B. Ensure the same paragraph style, font, and line height are used for both measurement and display.
4. **`textContainer.lineFragmentPadding`** — Default is 5pt. This adds 10pt total width (5 each side). If you measure without accounting for it, your widths are 10pt off: ```swift let effectiveWidth = containerWidth - 2 * textContainer.lineFragmentPadding ```
5. **`textContainerInset`** — UITextView default is `UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)`. You must add these to the measured text height for the actual view height.
6. **Measuring before layout** — In TextKit, measurements are only valid after layout. Call `ensureLayout(for:)` before reading rects.
7. **Thread safety** — All measurement APIs that touch NSLayoutManager or NSTextLayoutManager must be called from the main thread (or the layout queue for TK2).
## Related Skills
- For text colors and Dynamic Type scaling -> `/skill apple-text-colors`, `/skill apple-text-dynamic-type`- For viewport-based lazy measurement -> `/skill apple-text-viewport-rendering`- For layout invalidation after content changes -> `/skill apple-text-layout-invalidation`- For Core Text glyph-level measurement -> `/skill apple-text-core-text`