Exclusion Paths, Multi-Container & Text Tables
Use when wrapping text around images or shapes, creating multi-column text layout, linking text containers for magazine/book layout, configuring NSTextContainer exclusion paths, or building non-rectangular text regions. Covers exclusionPaths, linked containers, multi-container layout in TextKit 1 and 2, and NSTextBlock/NSTextTable for in-text tables.
Use when wrapping text around images or shapes, creating multi-column text layout, linking text containers for magazine/book layout, configuring NSTextContainer exclusion paths, or building non-rectangular text regions. Covers exclusionPaths, linked containers, multi-container layout in TextKit 1 and 2, and NSTextBlock/NSTextTable for in-text tables.
Family: TextKit Runtime And Layout
Use this skill when text needs to flow around objects, across columns, or render as tables.
When to Use
Section titled “When to Use”- You need text to wrap around an image or arbitrary shape.
- You need multi-column or multi-page text layout.
- You need linked text containers (text flows from one to the next).
- You need in-text tables using NSTextTable/NSTextBlock (AppKit).
- You need non-rectangular text containers.
Quick Decision
Section titled “Quick Decision”- Text wraps around a shape ->
exclusionPaths - Text flows across columns/pages -> linked
NSTextContainerarray - Table of data inside a text view ->
NSTextTable+NSTextTableBlock(AppKit) orNSTextAttachmentViewProvider(UIKit) - Non-rectangular text region -> subclass
NSTextContainerand overridelineFragmentRect(forProposedRect:...)
Exclusion Paths
Section titled “Exclusion Paths”What They Are
Section titled “What They Are”NSTextContainer.exclusionPaths is an array of UIBezierPath/NSBezierPath objects that define “holes” where text cannot appear. The text system flows text around these shapes.
Basic Usage
Section titled “Basic Usage”// Create a circular exclusion in the top-right cornerlet circlePath = UIBezierPath( ovalIn: CGRect(x: 200, y: 20, width: 120, height: 120))textView.textContainer.exclusionPaths = [circlePath]Text will wrap around the circle. Multiple paths are supported:
textView.textContainer.exclusionPaths = [imageRect, pullQuoteRect, sidebarRect]Coordinate System
Section titled “Coordinate System”Exclusion paths use the text container’s coordinate system, not the text view’s:
// Convert from text view coordinates to text container coordinateslet containerOrigin = textView.textContainerOrigin // UITextView// orlet containerInset = textView.textContainerInset // UITextViewlet containerPoint = CGPoint( x: viewPoint.x - containerInset.left, y: viewPoint.y - containerInset.top)Dynamic Exclusion Paths (Image Follows Scroll)
Section titled “Dynamic Exclusion Paths (Image Follows Scroll)”func updateExclusionForFloatingImage(_ imageView: UIImageView) { let imageFrame = textView.convert(imageView.frame, from: imageView.superview) let containerInset = textView.textContainerInset let exclusionRect = CGRect( x: imageFrame.origin.x - containerInset.left, y: imageFrame.origin.y - containerInset.top, width: imageFrame.width + 8, // padding height: imageFrame.height + 8 ) textView.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionRect)]}Performance warning: Changing exclusionPaths invalidates the entire layout. For frequently-moving exclusions (e.g., during scroll), batch updates and avoid per-frame changes.
Complex Shapes
Section titled “Complex Shapes”// L-shaped exclusionlet path = UIBezierPath()path.move(to: CGPoint(x: 150, y: 0))path.addLine(to: CGPoint(x: 300, y: 0))path.addLine(to: CGPoint(x: 300, y: 200))path.addLine(to: CGPoint(x: 200, y: 200))path.addLine(to: CGPoint(x: 200, y: 100))path.addLine(to: CGPoint(x: 150, y: 100))path.close()textView.textContainer.exclusionPaths = [path]TextKit 1 vs TextKit 2
Section titled “TextKit 1 vs TextKit 2”| Behavior | TextKit 1 | TextKit 2 |
|---|---|---|
exclusionPaths property | Yes | Yes |
| Re-layout on change | Full relayout | Viewport relayout |
| Performance with many paths | Degrades | Better (viewport-scoped) |
| Custom container subclass | Override lineFragmentRect(forProposedRect:...) | Same method |
Multi-Container (Linked) Layout
Section titled “Multi-Container (Linked) Layout”What It Is
Section titled “What It Is”A single NSLayoutManager (TK1) or NSTextLayoutManager (TK2) can manage text across multiple NSTextContainer instances. When text overflows the first container, it flows into the second, and so on. This is how you build multi-column, multi-page, or magazine-style layouts.
TextKit 1 — Multiple Containers
Section titled “TextKit 1 — Multiple Containers”let textStorage = NSTextStorage(attributedString: content)let layoutManager = NSLayoutManager()textStorage.addLayoutManager(layoutManager)
// Column 1let container1 = NSTextContainer(size: CGSize(width: 300, height: 500))layoutManager.addTextContainer(container1)let textView1 = UITextView(frame: .zero, textContainer: container1)
// Column 2let container2 = NSTextContainer(size: CGSize(width: 300, height: 500))layoutManager.addTextContainer(container2)let textView2 = UITextView(frame: .zero, textContainer: container2)
// Text automatically flows from container1 -> container2Key rules:
- Container order matters —
layoutManager.textContainersis an ordered array - Text fills containers in order; overflow goes to the next
- Each container can have its own
exclusionPaths - Each container gets its own
UITextView/NSTextView - You manage the views’ frames yourself (the text system only handles text flow)
TextKit 2 — Multiple Containers
Section titled “TextKit 2 — Multiple Containers”TextKit 2 uses a slightly different model. NSTextLayoutManager manages a single NSTextContainer by default, but you can use NSTextContentManager with multiple layout managers:
let contentManager = NSTextContentStorage()contentManager.attributedString = content
// Layout manager per columnlet layoutManager1 = NSTextLayoutManager()let container1 = NSTextContainer(size: CGSize(width: 300, height: 500))layoutManager1.textContainer = container1contentManager.addTextLayoutManager(layoutManager1)
let layoutManager2 = NSTextLayoutManager()let container2 = NSTextContainer(size: CGSize(width: 300, height: 500))layoutManager2.textContainer = container2contentManager.addTextLayoutManager(layoutManager2)Detecting Overflow
Section titled “Detecting Overflow”// TextKit 1: Check if text overflows a containerlet glyphRange = layoutManager.glyphRange(for: container1)let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)let hasOverflow = charRange.upperBound < textStorage.lengthPractical: Two-Column Layout
Section titled “Practical: Two-Column Layout”class TwoColumnView: UIView { let textStorage = NSTextStorage() let layoutManager = NSLayoutManager() var leftTextView: UITextView! var rightTextView: UITextView!
func setup() { textStorage.addLayoutManager(layoutManager)
let leftContainer = NSTextContainer(size: .zero) leftContainer.widthTracksTextView = true leftContainer.heightTracksTextView = true layoutManager.addTextContainer(leftContainer) leftTextView = UITextView(frame: .zero, textContainer: leftContainer) leftTextView.isEditable = false addSubview(leftTextView)
let rightContainer = NSTextContainer(size: .zero) rightContainer.widthTracksTextView = true rightContainer.heightTracksTextView = true layoutManager.addTextContainer(rightContainer) rightTextView = UITextView(frame: .zero, textContainer: rightContainer) rightTextView.isEditable = false addSubview(rightTextView) }
override func layoutSubviews() { super.layoutSubviews() let columnWidth = (bounds.width - 16) / 2 // 16pt gap leftTextView.frame = CGRect(x: 0, y: 0, width: columnWidth, height: bounds.height) rightTextView.frame = CGRect(x: columnWidth + 16, y: 0, width: columnWidth, height: bounds.height) }}NSTextContainer Subclassing
Section titled “NSTextContainer Subclassing”For truly non-rectangular text regions (circular, triangular, path-based):
class CircularTextContainer: NSTextContainer { override func lineFragmentRect( forProposedRect proposedRect: CGRect, at characterIndex: Int, writingDirection baseWritingDirection: NSWritingDirection, remaining remainingRect: UnsafeMutablePointer<CGRect>? ) -> CGRect { // Start with the standard rect var result = super.lineFragmentRect( forProposedRect: proposedRect, at: characterIndex, writingDirection: baseWritingDirection, remaining: remainingRect )
// Constrain to a circle let center = CGPoint(x: size.width / 2, y: size.height / 2) let radius = min(size.width, size.height) / 2 let y = proposedRect.origin.y + proposedRect.height / 2 let dy = y - center.y
guard abs(dy) < radius else { return .zero }
let dx = sqrt(radius * radius - dy * dy) let minX = center.x - dx + lineFragmentPadding let maxX = center.x + dx - lineFragmentPadding
result.origin.x = minX result.size.width = maxX - minX
return result }
override var isSimpleRectangularTextContainer: Bool { false }}isSimpleRectangularTextContainer — Return false when you override lineFragmentRect(forProposedRect:...). This tells the text system it can’t take layout shortcuts.
NSTextTable / NSTextBlock (AppKit)
Section titled “NSTextTable / NSTextBlock (AppKit)”What They Are
Section titled “What They Are”AppKit provides NSTextTable and NSTextTableBlock for rendering tables directly inside attributed strings. These are paragraph-level attributes — each table cell is a paragraph whose NSParagraphStyle.textBlocks includes an NSTextTableBlock.
Platform availability: Primarily AppKit (NSTextView). UIKit has the classes but rendering support is limited.
Creating a Table
Section titled “Creating a Table”// Create a 3-column tablelet table = NSTextTable()table.numberOfColumns = 3table.collapsesBorders = true
// Create a cell: row 0, column 0let cell = NSTextTableBlock(table: table, startingRow: 0, rowSpan: 1, startingColumn: 0, columnSpan: 1)cell.setContentWidth(33.33, type: .percentageValueType)cell.backgroundColor = .controlBackgroundColorcell.setWidth(0.5, type: .absoluteValueType, for: .border)cell.setBorderColor(.separatorColor)cell.setValue(4, type: .absoluteValueType, for: .padding)
// Attach to paragraph stylelet style = NSMutableParagraphStyle()style.textBlocks = [cell]
// Create the cell contentlet cellText = NSAttributedString( string: "Cell content\n", // Note: must end with newline attributes: [ .paragraphStyle: style, .font: NSFont.systemFont(ofSize: 13) ])Building a Full Table
Section titled “Building a Full Table”func makeTable(rows: Int, columns: Int, data: [[String]]) -> NSAttributedString { let table = NSTextTable() table.numberOfColumns = columns table.collapsesBorders = true
let result = NSMutableAttributedString()
for row in 0..<rows { for col in 0..<columns { let cell = NSTextTableBlock(table: table, startingRow: row, rowSpan: 1, startingColumn: col, columnSpan: 1) cell.setContentWidth(CGFloat(100 / columns), type: .percentageValueType) cell.setValue(4, type: .absoluteValueType, for: .padding) cell.setWidth(0.5, type: .absoluteValueType, for: .border) cell.setBorderColor(.separatorColor)
if row == 0 { cell.backgroundColor = .controlAccentColor.withAlphaComponent(0.1) }
let style = NSMutableParagraphStyle() style.textBlocks = [cell]
let text = data[row][col] + "\n" // Each cell ends with newline result.append(NSAttributedString(string: text, attributes: [ .paragraphStyle: style, .font: row == 0 ? NSFont.boldSystemFont(ofSize: 13) : NSFont.systemFont(ofSize: 13) ])) } }
return result}NSTextBlock Properties
Section titled “NSTextBlock Properties”| Property | Purpose |
|---|---|
backgroundColor | Cell background color |
setBorderColor(_:for:) | Per-edge border color |
setWidth(_:type:for:edge:) | Margin, border, or padding per edge |
setWidth(_:type:for:) | Margin, border, or padding for all edges of a layer |
setContentWidth(_:type:) | Cell content width (absolute or percentage) |
verticalAlignment | .top, .middle, .bottom, .baseline |
setValue(_:type:for:) | Set dimension values (minWidth, maxWidth, minHeight, maxHeight) |
NSTextBlock.Layer
Section titled “NSTextBlock.Layer”cell.setWidth(1, type: .absoluteValueType, for: .border) // Border layercell.setValue(8, type: .absoluteValueType, for: .padding) // Padding layercell.setValue(4, type: .absoluteValueType, for: .margin) // Margin layerUIKit Alternative: Tables via Attachments
Section titled “UIKit Alternative: Tables via Attachments”Since UIKit doesn’t fully support NSTextTable rendering, use NSTextAttachmentViewProvider (TextKit 2) to embed a UITableView or custom view:
// See /skill apple-text-attachments-ref for full NSTextAttachmentViewProvider patternclass TableAttachmentViewProvider: NSTextAttachmentViewProvider { override func loadView() { let tableView = MyCompactTableView(data: extractData(from: textAttachment)) view = tableView }
override func attachmentBounds( for attributes: [NSAttributedString.Key: Any], location: NSTextLocation, textContainer: NSTextContainer?, proposedLineFragment: CGRect, position: CGPoint ) -> CGRect { // Full-width, calculated height let width = proposedLineFragment.width let height = calculateTableHeight(for: width) return CGRect(x: 0, y: 0, width: width, height: height) }}NSTextList
Section titled “NSTextList”For ordered/unordered lists inside attributed strings:
let list = NSTextList(markerFormat: .decimal, options: 0)list.startingItemNumber = 1
let style = NSMutableParagraphStyle()style.textLists = [list]style.headIndent = 24 // Indent for wrapped linesstyle.firstLineHeadIndent = 0 // Marker hangs in the margin
let item = NSAttributedString( string: "\t\(list.marker(forItemNumber: 1))\tFirst item\n", attributes: [.paragraphStyle: style, .font: UIFont.systemFont(ofSize: 15)])Marker Formats
Section titled “Marker Formats”| Format | Appearance |
|---|---|
.decimal | 1. 2. 3. |
.octal | 1. 2. 3. (base 8) |
.lowercaseAlpha | a. b. c. |
.uppercaseAlpha | A. B. C. |
.lowercaseRoman | i. ii. iii. |
.uppercaseRoman | I. II. III. |
.disc | bullet (filled circle) |
.circle | open circle |
.square | filled square |
.diamond | diamond |
.hyphen | hyphen |
Nested Lists
Section titled “Nested Lists”let outerList = NSTextList(markerFormat: .decimal, options: 0)let innerList = NSTextList(markerFormat: .lowercaseAlpha, options: 0)
let innerStyle = NSMutableParagraphStyle()innerStyle.textLists = [outerList, innerList] // Nesting = array orderinnerStyle.headIndent = 48 // Double indentPitfalls
Section titled “Pitfalls”-
Exclusion path coordinates — Must be in text container coordinates, not view coordinates. Account for
textContainerInsetandtextContainerOrigin. -
Exclusion path performance — Each change triggers full relayout in TextKit 1. Batch changes; don’t update per-frame during animations.
-
NSTextTable on UIKit — The classes exist but rendering is incomplete. Use attachment view providers on iOS instead.
-
Each table cell must end with
\n— NSTextTable cells are paragraph-level. Missing the trailing newline merges cells. -
Multi-container editing — Editing in linked containers is fragile. Works well for read-only; editing across container boundaries requires careful cursor management.
-
isSimpleRectangularTextContainer— If you subclassNSTextContainerand overridelineFragmentRect, you must returnfalseor layout may use incorrect fast paths.
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-exclusion-paths reference skill. Use it when the subsystem is already known and you need mechanics, behavior, or API detail.
Related
Section titled “Related”apple-text-attachments-ref: Use when embedding inline non-text content such as images, custom views, Genmoji, or attachment-backed runs inside Apple text systems. Reach for this when the problem is attachment APIs, layout, bounds, baseline alignment, or lifecycle, not broader rich-text architecture.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.apple-text-formatting-ref: Use when the user already knows the formatting problem and needs exact text-formatting attributes such as NSAttributedString.Key values, underline styles, shadows, lists, tables, or view-compatibility rules. Reach for this when the job is verifying concrete formatting APIs, not choosing the text model.
Full SKILL.md source
---name: apple-text-exclusion-pathsdescription: > Use when wrapping text around images or shapes, creating multi-column text layout, linking text containers for magazine/book layout, configuring NSTextContainer exclusion paths, or building non-rectangular text regions. Covers exclusionPaths, linked containers, multi-container layout in TextKit 1 and 2, and NSTextBlock/NSTextTable for in-text tables.license: MIT---
# Exclusion Paths, Multi-Container & Text Tables
Use this skill when text needs to flow around objects, across columns, or render as tables.
## When to Use
- You need text to wrap around an image or arbitrary shape.- You need multi-column or multi-page text layout.- You need linked text containers (text flows from one to the next).- You need in-text tables using NSTextTable/NSTextBlock (AppKit).- You need non-rectangular text containers.
## Quick Decision
- Text wraps around a shape -> `exclusionPaths`- Text flows across columns/pages -> linked `NSTextContainer` array- Table of data inside a text view -> `NSTextTable` + `NSTextTableBlock` (AppKit) or `NSTextAttachmentViewProvider` (UIKit)- Non-rectangular text region -> subclass `NSTextContainer` and override `lineFragmentRect(forProposedRect:...)`
## Exclusion Paths
### What They Are
`NSTextContainer.exclusionPaths` is an array of `UIBezierPath`/`NSBezierPath` objects that define "holes" where text cannot appear. The text system flows text around these shapes.
### Basic Usage
```swift// Create a circular exclusion in the top-right cornerlet circlePath = UIBezierPath( ovalIn: CGRect(x: 200, y: 20, width: 120, height: 120))textView.textContainer.exclusionPaths = [circlePath]```
Text will wrap around the circle. Multiple paths are supported:
```swifttextView.textContainer.exclusionPaths = [imageRect, pullQuoteRect, sidebarRect]```
### Coordinate System
Exclusion paths use the **text container's coordinate system**, not the text view's:
```swift// Convert from text view coordinates to text container coordinateslet containerOrigin = textView.textContainerOrigin // UITextView// orlet containerInset = textView.textContainerInset // UITextViewlet containerPoint = CGPoint( x: viewPoint.x - containerInset.left, y: viewPoint.y - containerInset.top)```
### Dynamic Exclusion Paths (Image Follows Scroll)
```swiftfunc updateExclusionForFloatingImage(_ imageView: UIImageView) { let imageFrame = textView.convert(imageView.frame, from: imageView.superview) let containerInset = textView.textContainerInset let exclusionRect = CGRect( x: imageFrame.origin.x - containerInset.left, y: imageFrame.origin.y - containerInset.top, width: imageFrame.width + 8, // padding height: imageFrame.height + 8 ) textView.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionRect)]}```
**Performance warning:** Changing `exclusionPaths` invalidates the entire layout. For frequently-moving exclusions (e.g., during scroll), batch updates and avoid per-frame changes.
### Complex Shapes
```swift// L-shaped exclusionlet path = UIBezierPath()path.move(to: CGPoint(x: 150, y: 0))path.addLine(to: CGPoint(x: 300, y: 0))path.addLine(to: CGPoint(x: 300, y: 200))path.addLine(to: CGPoint(x: 200, y: 200))path.addLine(to: CGPoint(x: 200, y: 100))path.addLine(to: CGPoint(x: 150, y: 100))path.close()textView.textContainer.exclusionPaths = [path]```
### TextKit 1 vs TextKit 2
| Behavior | TextKit 1 | TextKit 2 ||----------|-----------|-----------|| `exclusionPaths` property | Yes | Yes || Re-layout on change | Full relayout | Viewport relayout || Performance with many paths | Degrades | Better (viewport-scoped) || Custom container subclass | Override `lineFragmentRect(forProposedRect:...)` | Same method |
## Multi-Container (Linked) Layout
### What It Is
A single `NSLayoutManager` (TK1) or `NSTextLayoutManager` (TK2) can manage text across **multiple** `NSTextContainer` instances. When text overflows the first container, it flows into the second, and so on. This is how you build multi-column, multi-page, or magazine-style layouts.
### TextKit 1 — Multiple Containers
```swiftlet textStorage = NSTextStorage(attributedString: content)let layoutManager = NSLayoutManager()textStorage.addLayoutManager(layoutManager)
// Column 1let container1 = NSTextContainer(size: CGSize(width: 300, height: 500))layoutManager.addTextContainer(container1)let textView1 = UITextView(frame: .zero, textContainer: container1)
// Column 2let container2 = NSTextContainer(size: CGSize(width: 300, height: 500))layoutManager.addTextContainer(container2)let textView2 = UITextView(frame: .zero, textContainer: container2)
// Text automatically flows from container1 -> container2```
**Key rules:**- Container order matters — `layoutManager.textContainers` is an ordered array- Text fills containers in order; overflow goes to the next- Each container can have its own `exclusionPaths`- Each container gets its own `UITextView`/`NSTextView`- You manage the views' frames yourself (the text system only handles text flow)
### TextKit 2 — Multiple Containers
TextKit 2 uses a slightly different model. `NSTextLayoutManager` manages a single `NSTextContainer` by default, but you can use `NSTextContentManager` with multiple layout managers:
```swiftlet contentManager = NSTextContentStorage()contentManager.attributedString = content
// Layout manager per columnlet layoutManager1 = NSTextLayoutManager()let container1 = NSTextContainer(size: CGSize(width: 300, height: 500))layoutManager1.textContainer = container1contentManager.addTextLayoutManager(layoutManager1)
let layoutManager2 = NSTextLayoutManager()let container2 = NSTextContainer(size: CGSize(width: 300, height: 500))layoutManager2.textContainer = container2contentManager.addTextLayoutManager(layoutManager2)```
### Detecting Overflow
```swift// TextKit 1: Check if text overflows a containerlet glyphRange = layoutManager.glyphRange(for: container1)let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)let hasOverflow = charRange.upperBound < textStorage.length```
### Practical: Two-Column Layout
```swiftclass TwoColumnView: UIView { let textStorage = NSTextStorage() let layoutManager = NSLayoutManager() var leftTextView: UITextView! var rightTextView: UITextView!
func setup() { textStorage.addLayoutManager(layoutManager)
let leftContainer = NSTextContainer(size: .zero) leftContainer.widthTracksTextView = true leftContainer.heightTracksTextView = true layoutManager.addTextContainer(leftContainer) leftTextView = UITextView(frame: .zero, textContainer: leftContainer) leftTextView.isEditable = false addSubview(leftTextView)
let rightContainer = NSTextContainer(size: .zero) rightContainer.widthTracksTextView = true rightContainer.heightTracksTextView = true layoutManager.addTextContainer(rightContainer) rightTextView = UITextView(frame: .zero, textContainer: rightContainer) rightTextView.isEditable = false addSubview(rightTextView) }
override func layoutSubviews() { super.layoutSubviews() let columnWidth = (bounds.width - 16) / 2 // 16pt gap leftTextView.frame = CGRect(x: 0, y: 0, width: columnWidth, height: bounds.height) rightTextView.frame = CGRect(x: columnWidth + 16, y: 0, width: columnWidth, height: bounds.height) }}```
## NSTextContainer Subclassing
For truly non-rectangular text regions (circular, triangular, path-based):
```swiftclass CircularTextContainer: NSTextContainer { override func lineFragmentRect( forProposedRect proposedRect: CGRect, at characterIndex: Int, writingDirection baseWritingDirection: NSWritingDirection, remaining remainingRect: UnsafeMutablePointer<CGRect>? ) -> CGRect { // Start with the standard rect var result = super.lineFragmentRect( forProposedRect: proposedRect, at: characterIndex, writingDirection: baseWritingDirection, remaining: remainingRect )
// Constrain to a circle let center = CGPoint(x: size.width / 2, y: size.height / 2) let radius = min(size.width, size.height) / 2 let y = proposedRect.origin.y + proposedRect.height / 2 let dy = y - center.y
guard abs(dy) < radius else { return .zero }
let dx = sqrt(radius * radius - dy * dy) let minX = center.x - dx + lineFragmentPadding let maxX = center.x + dx - lineFragmentPadding
result.origin.x = minX result.size.width = maxX - minX
return result }
override var isSimpleRectangularTextContainer: Bool { false }}```
**`isSimpleRectangularTextContainer`** — Return `false` when you override `lineFragmentRect(forProposedRect:...)`. This tells the text system it can't take layout shortcuts.
## NSTextTable / NSTextBlock (AppKit)
### What They Are
AppKit provides `NSTextTable` and `NSTextTableBlock` for rendering tables directly inside attributed strings. These are **paragraph-level attributes** — each table cell is a paragraph whose `NSParagraphStyle.textBlocks` includes an `NSTextTableBlock`.
**Platform availability:** Primarily AppKit (NSTextView). UIKit has the classes but rendering support is limited.
### Creating a Table
```swift// Create a 3-column tablelet table = NSTextTable()table.numberOfColumns = 3table.collapsesBorders = true
// Create a cell: row 0, column 0let cell = NSTextTableBlock(table: table, startingRow: 0, rowSpan: 1, startingColumn: 0, columnSpan: 1)cell.setContentWidth(33.33, type: .percentageValueType)cell.backgroundColor = .controlBackgroundColorcell.setWidth(0.5, type: .absoluteValueType, for: .border)cell.setBorderColor(.separatorColor)cell.setValue(4, type: .absoluteValueType, for: .padding)
// Attach to paragraph stylelet style = NSMutableParagraphStyle()style.textBlocks = [cell]
// Create the cell contentlet cellText = NSAttributedString( string: "Cell content\n", // Note: must end with newline attributes: [ .paragraphStyle: style, .font: NSFont.systemFont(ofSize: 13) ])```
### Building a Full Table
```swiftfunc makeTable(rows: Int, columns: Int, data: [[String]]) -> NSAttributedString { let table = NSTextTable() table.numberOfColumns = columns table.collapsesBorders = true
let result = NSMutableAttributedString()
for row in 0..<rows { for col in 0..<columns { let cell = NSTextTableBlock(table: table, startingRow: row, rowSpan: 1, startingColumn: col, columnSpan: 1) cell.setContentWidth(CGFloat(100 / columns), type: .percentageValueType) cell.setValue(4, type: .absoluteValueType, for: .padding) cell.setWidth(0.5, type: .absoluteValueType, for: .border) cell.setBorderColor(.separatorColor)
if row == 0 { cell.backgroundColor = .controlAccentColor.withAlphaComponent(0.1) }
let style = NSMutableParagraphStyle() style.textBlocks = [cell]
let text = data[row][col] + "\n" // Each cell ends with newline result.append(NSAttributedString(string: text, attributes: [ .paragraphStyle: style, .font: row == 0 ? NSFont.boldSystemFont(ofSize: 13) : NSFont.systemFont(ofSize: 13) ])) } }
return result}```
### NSTextBlock Properties
| Property | Purpose ||----------|---------|| `backgroundColor` | Cell background color || `setBorderColor(_:for:)` | Per-edge border color || `setWidth(_:type:for:edge:)` | Margin, border, or padding per edge || `setWidth(_:type:for:)` | Margin, border, or padding for all edges of a layer || `setContentWidth(_:type:)` | Cell content width (absolute or percentage) || `verticalAlignment` | `.top`, `.middle`, `.bottom`, `.baseline` || `setValue(_:type:for:)` | Set dimension values (minWidth, maxWidth, minHeight, maxHeight) |
### NSTextBlock.Layer
```swiftcell.setWidth(1, type: .absoluteValueType, for: .border) // Border layercell.setValue(8, type: .absoluteValueType, for: .padding) // Padding layercell.setValue(4, type: .absoluteValueType, for: .margin) // Margin layer```
### UIKit Alternative: Tables via Attachments
Since UIKit doesn't fully support NSTextTable rendering, use `NSTextAttachmentViewProvider` (TextKit 2) to embed a `UITableView` or custom view:
```swift// See /skill apple-text-attachments-ref for full NSTextAttachmentViewProvider patternclass TableAttachmentViewProvider: NSTextAttachmentViewProvider { override func loadView() { let tableView = MyCompactTableView(data: extractData(from: textAttachment)) view = tableView }
override func attachmentBounds( for attributes: [NSAttributedString.Key: Any], location: NSTextLocation, textContainer: NSTextContainer?, proposedLineFragment: CGRect, position: CGPoint ) -> CGRect { // Full-width, calculated height let width = proposedLineFragment.width let height = calculateTableHeight(for: width) return CGRect(x: 0, y: 0, width: width, height: height) }}```
## NSTextList
For ordered/unordered lists inside attributed strings:
```swiftlet list = NSTextList(markerFormat: .decimal, options: 0)list.startingItemNumber = 1
let style = NSMutableParagraphStyle()style.textLists = [list]style.headIndent = 24 // Indent for wrapped linesstyle.firstLineHeadIndent = 0 // Marker hangs in the margin
let item = NSAttributedString( string: "\t\(list.marker(forItemNumber: 1))\tFirst item\n", attributes: [.paragraphStyle: style, .font: UIFont.systemFont(ofSize: 15)])```
### Marker Formats
| Format | Appearance ||--------|-----------|| `.decimal` | 1. 2. 3. || `.octal` | 1. 2. 3. (base 8) || `.lowercaseAlpha` | a. b. c. || `.uppercaseAlpha` | A. B. C. || `.lowercaseRoman` | i. ii. iii. || `.uppercaseRoman` | I. II. III. || `.disc` | bullet (filled circle) || `.circle` | open circle || `.square` | filled square || `.diamond` | diamond || `.hyphen` | hyphen |
### Nested Lists
```swiftlet outerList = NSTextList(markerFormat: .decimal, options: 0)let innerList = NSTextList(markerFormat: .lowercaseAlpha, options: 0)
let innerStyle = NSMutableParagraphStyle()innerStyle.textLists = [outerList, innerList] // Nesting = array orderinnerStyle.headIndent = 48 // Double indent```
## Pitfalls
1. **Exclusion path coordinates** — Must be in text container coordinates, not view coordinates. Account for `textContainerInset` and `textContainerOrigin`.
2. **Exclusion path performance** — Each change triggers full relayout in TextKit 1. Batch changes; don't update per-frame during animations.
3. **NSTextTable on UIKit** — The classes exist but rendering is incomplete. Use attachment view providers on iOS instead.
4. **Each table cell must end with `\n`** — NSTextTable cells are paragraph-level. Missing the trailing newline merges cells.
5. **Multi-container editing** — Editing in linked containers is fragile. Works well for read-only; editing across container boundaries requires careful cursor management.
6. **`isSimpleRectangularTextContainer`** — If you subclass `NSTextContainer` and override `lineFragmentRect`, you must return `false` or layout may use incorrect fast paths.
## Related Skills
- For embedding interactive views inline -> `/skill apple-text-attachments-ref`- For viewport-scoped layout with exclusions -> `/skill apple-text-viewport-rendering`- For paragraph style formatting details -> `/skill apple-text-formatting-ref`- For layout invalidation after changing exclusion paths -> `/skill apple-text-layout-invalidation`