Text Attachments Reference
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.
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.
Family: Rich Text And Formatting
Use this skill when the main question is how inline non-text content should behave inside Apple text views.
When to Use
Section titled “When to Use”- You are working with
NSTextAttachment. - You need attachment view providers, Genmoji, or baseline/bounds behavior.
- You need compatibility rules across TextKit 1, TextKit 2, UIKit, and AppKit.
Quick Decision
Section titled “Quick Decision”- Static inline image attachment ->
NSTextAttachment - Interactive inline view in TextKit 2 ->
NSTextAttachmentViewProvider - Adaptive inline glyph on supported systems ->
NSAdaptiveImageGlyph
Core Guidance
Section titled “Core Guidance”Keep this file for the attachment model, view-provider lifecycle, and adaptive glyph choice. For protocol signatures, insertion patterns, copy-paste rules, and support matrices, use protocols-and-patterns.md.
NSTextAttachment
Section titled “NSTextAttachment”What It Is
Section titled “What It Is”The core class for inline non-text content in attributed strings. Lives in UIFoundation (shared UIKit/AppKit).
Initializers
Section titled “Initializers”// From image (iOS 13+) — most commonlet attachment = NSTextAttachment(image: myImage)
// From data + UTIlet attachment = NSTextAttachment(data: pngData, ofType: UTType.png.identifier)
// From file wrapper (macOS-oriented)let attachment = NSTextAttachment(fileWrapper: fileWrapper)macOS gotcha: When using init(data:ofType:), you must manually set attachmentCell or the image won’t display. Use init(fileWrapper:) or set image directly.
The Attachment Character (U+FFFC)
Section titled “The Attachment Character (U+FFFC)”Attachments are represented in attributed strings by:
- Unicode Object Replacement Character (U+FFFC) as placeholder
.attachmentattribute holding the NSTextAttachment instance
let attachmentString = NSAttributedString(attachment: attachment)// Creates: 1 character (U+FFFC) with .attachment attribute
let full = NSMutableAttributedString(string: "Hello ")full.append(attachmentString)full.append(NSAttributedString(string: " World"))// Result: "Hello \u{FFFC} World" — attachment renders at position 6Bounds (Size + Baseline Alignment)
Section titled “Bounds (Size + Baseline Alignment)”attachment.bounds = CGRect(x: 0, y: yOffset, width: width, height: height)The y-axis origin is at the text baseline. Negative y moves the attachment below the baseline:
// Centered on text line (common pattern)let font = UIFont.preferredFont(forTextStyle: .body)let ratio = image.size.width / image.size.heightlet height = font.capHeightattachment.bounds = CGRect( x: 0, y: (font.capHeight - height) / 2, // Center vertically width: height * ratio, height: height)
// Baseline-aligned (sits on baseline)attachment.bounds = CGRect(x: 0, y: 0, width: 20, height: 20)
// Descender-aligned (extends below baseline)attachment.bounds = CGRect(x: 0, y: font.descender, width: 20, height: 20)CGRect.zero (default): Uses the image’s natural size. If image is nil, renders as missing-image placeholder.
Properties (iOS 15+)
Section titled “Properties (iOS 15+)”attachment.lineLayoutPadding = 4.0 // Horizontal padding around attachmentattachment.allowsTextAttachmentView = true // Allow view-based rendering (default: true)attachment.usesTextAttachmentView // Read-only: is view-based rendering active?NSTextAttachmentViewProvider (TextKit 2)
Section titled “NSTextAttachmentViewProvider (TextKit 2)”What It Is
Section titled “What It Is”Provides a live UIView/NSView for rendering an attachment. Unlike image-based attachments, view providers can have interactive controls, animations, and dynamic content.
Registration
Section titled “Registration”// Register a view provider for a file typeNSTextAttachment.registerViewProviderClass( CheckboxAttachmentViewProvider.self, forFileType: "com.myapp.checkbox")Implementation
Section titled “Implementation”class CheckboxAttachmentViewProvider: NSTextAttachmentViewProvider { override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) { super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location) // MUST set tracksTextAttachmentViewBounds HERE, not in loadView tracksTextAttachmentViewBounds = true }
override func loadView() { let checkbox = UISwitch() checkbox.isOn = (textAttachment.contents?.first == 1) view = checkbox }
override func attachmentBounds( for attributes: [NSAttributedString.Key: Any], location: NSTextLocation, textContainer: NSTextContainer?, proposedLineFragment: CGRect, position: CGPoint ) -> CGRect { return CGRect(x: 0, y: -4, width: 30, height: 30) }}Lifecycle
Section titled “Lifecycle”- Registration —
registerViewProviderClassmaps file type → provider class - Creation — TextKit 2 creates provider when attachment enters viewport
loadView()— Called to create the view. Setself.view.- Display — View is added to the text view’s subview hierarchy
- Removal — When attachment scrolls out of viewport, view may be removed
- Reuse — Views are NOT reused like table cells. New provider per appearance.
Critical Rules
Section titled “Critical Rules”tracksTextAttachmentViewBoundsmust be set ininit, NOTloadView— Setting it in loadView is too late and the bounds won’t track correctly.- View-based attachments are LOST on TextKit 1 fallback — The moment anything triggers fallback, all NSTextAttachmentViewProvider views disappear because TextKit 1 uses NSTextAttachmentCellProtocol instead.
- Only works with TextKit 2 —
usesTextAttachmentViewreturns false if TextKit 1 is active.
NSAdaptiveImageGlyph (iOS 18+)
Section titled “NSAdaptiveImageGlyph (iOS 18+)”What It Is
Section titled “What It Is”A special inline image that automatically adapts its size to match surrounding text. Used for Genmoji, stickers, and Memoji.
let glyph = NSAdaptiveImageGlyph(imageContent: imageData)
glyph.contentIdentifier // Unique IDglyph.contentDescription // Accessibility descriptionglyph.imageContent // Raw image data (multi-resolution)glyph.contentType // UTTypeHow It Differs from NSTextAttachment
Section titled “How It Differs from NSTextAttachment”| Aspect | NSTextAttachment | NSAdaptiveImageGlyph |
|---|---|---|
| Sizing | Manual (bounds property) | Automatic (matches text size) |
| Aspect ratio | Any | Always square |
| Resolutions | Single image | Multiple resolutions embedded |
| Dynamic Type | Manual handling | Automatic scaling |
| User insertion | Programmatic | Emoji keyboard |
| Available | iOS 7+ | iOS 18+ |
Enabling in Text Views
Section titled “Enabling in Text Views”textView.supportsAdaptiveImageGlyph = true // UITextView// Users can now insert Genmoji from the emoji keyboardExtracting from Attributed String
Section titled “Extracting from Attributed String”attributedString.enumerateAttribute( .adaptiveImageGlyph, in: NSRange(location: 0, length: attributedString.length)) { value, range, _ in if let glyph = value as? NSAdaptiveImageGlyph { print("Genmoji: \(glyph.contentDescription)") }}Common Pitfalls
Section titled “Common Pitfalls”- View attachments lost on TK1 fallback — The instant anything triggers fallback, all NSTextAttachmentViewProvider views vanish.
tracksTextAttachmentViewBoundsset in loadView — Too late. Must set in init.- macOS: image not displaying — With
init(data:ofType:), must setattachmentCellmanually or useinit(fileWrapper:). - Copy/paste loses attachment —
contentsmust be non-nil. Image-only attachments don’t survive paste. - Bounds y-coordinate confusion — Negative y goes below baseline. Use
font.descenderfor bottom-aligned. - Not setting accessibility — VoiceOver announces U+FFFC as “replacement character” without a description.
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-attachments-ref reference skill. Use it when the subsystem is already known and you need mechanics, behavior, or API detail.
Related
Section titled “Related”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.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-fallback-triggers: Use when the user needs to know exactly what makes TextKit 2 fall back to TextKit 1, or wants to audit code for fallback risk before it ships. Reach for this when the question is specifically about compatibility-mode triggers, not general text-system debugging.
Sidecar Files
Section titled “Sidecar Files”skills/apple-text-attachments-ref/protocols-and-patterns.md
Full SKILL.md source
---name: apple-text-attachments-refdescription: 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.license: MIT---
# Text Attachments Reference
Use this skill when the main question is how inline non-text content should behave inside Apple text views.
## When to Use
- You are working with `NSTextAttachment`.- You need attachment view providers, Genmoji, or baseline/bounds behavior.- You need compatibility rules across TextKit 1, TextKit 2, UIKit, and AppKit.
## Quick Decision
- Static inline image attachment -> `NSTextAttachment`- Interactive inline view in TextKit 2 -> `NSTextAttachmentViewProvider`- Adaptive inline glyph on supported systems -> `NSAdaptiveImageGlyph`
## Core Guidance
Keep this file for the attachment model, view-provider lifecycle, and adaptive glyph choice. For protocol signatures, insertion patterns, copy-paste rules, and support matrices, use [protocols-and-patterns.md](protocols-and-patterns.md).
## NSTextAttachment
### What It Is
The core class for inline non-text content in attributed strings. Lives in UIFoundation (shared UIKit/AppKit).
### Initializers
```swift// From image (iOS 13+) — most commonlet attachment = NSTextAttachment(image: myImage)
// From data + UTIlet attachment = NSTextAttachment(data: pngData, ofType: UTType.png.identifier)
// From file wrapper (macOS-oriented)let attachment = NSTextAttachment(fileWrapper: fileWrapper)```
**macOS gotcha:** When using `init(data:ofType:)`, you must manually set `attachmentCell` or the image won't display. Use `init(fileWrapper:)` or set `image` directly.
### The Attachment Character (U+FFFC)
Attachments are represented in attributed strings by:1. Unicode Object Replacement Character (U+FFFC) as placeholder2. `.attachment` attribute holding the NSTextAttachment instance
```swiftlet attachmentString = NSAttributedString(attachment: attachment)// Creates: 1 character (U+FFFC) with .attachment attribute
let full = NSMutableAttributedString(string: "Hello ")full.append(attachmentString)full.append(NSAttributedString(string: " World"))// Result: "Hello \u{FFFC} World" — attachment renders at position 6```
### Bounds (Size + Baseline Alignment)
```swiftattachment.bounds = CGRect(x: 0, y: yOffset, width: width, height: height)```
**The y-axis origin is at the text baseline.** Negative y moves the attachment below the baseline:
```swift// Centered on text line (common pattern)let font = UIFont.preferredFont(forTextStyle: .body)let ratio = image.size.width / image.size.heightlet height = font.capHeightattachment.bounds = CGRect( x: 0, y: (font.capHeight - height) / 2, // Center vertically width: height * ratio, height: height)
// Baseline-aligned (sits on baseline)attachment.bounds = CGRect(x: 0, y: 0, width: 20, height: 20)
// Descender-aligned (extends below baseline)attachment.bounds = CGRect(x: 0, y: font.descender, width: 20, height: 20)```
**`CGRect.zero` (default):** Uses the image's natural size. If image is nil, renders as missing-image placeholder.
### Properties (iOS 15+)
```swiftattachment.lineLayoutPadding = 4.0 // Horizontal padding around attachmentattachment.allowsTextAttachmentView = true // Allow view-based rendering (default: true)attachment.usesTextAttachmentView // Read-only: is view-based rendering active?```
## NSTextAttachmentViewProvider (TextKit 2)
### What It Is
Provides a live `UIView`/`NSView` for rendering an attachment. Unlike image-based attachments, view providers can have interactive controls, animations, and dynamic content.
### Registration
```swift// Register a view provider for a file typeNSTextAttachment.registerViewProviderClass( CheckboxAttachmentViewProvider.self, forFileType: "com.myapp.checkbox")```
### Implementation
```swiftclass CheckboxAttachmentViewProvider: NSTextAttachmentViewProvider { override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) { super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location) // MUST set tracksTextAttachmentViewBounds HERE, not in loadView tracksTextAttachmentViewBounds = true }
override func loadView() { let checkbox = UISwitch() checkbox.isOn = (textAttachment.contents?.first == 1) view = checkbox }
override func attachmentBounds( for attributes: [NSAttributedString.Key: Any], location: NSTextLocation, textContainer: NSTextContainer?, proposedLineFragment: CGRect, position: CGPoint ) -> CGRect { return CGRect(x: 0, y: -4, width: 30, height: 30) }}```
### Lifecycle
1. **Registration** — `registerViewProviderClass` maps file type → provider class2. **Creation** — TextKit 2 creates provider when attachment enters viewport3. **`loadView()`** — Called to create the view. Set `self.view`.4. **Display** — View is added to the text view's subview hierarchy5. **Removal** — When attachment scrolls out of viewport, view may be removed6. **Reuse** — Views are NOT reused like table cells. New provider per appearance.
### Critical Rules
- **`tracksTextAttachmentViewBounds` must be set in `init`, NOT `loadView`** — Setting it in loadView is too late and the bounds won't track correctly.- **View-based attachments are LOST on TextKit 1 fallback** — The moment anything triggers fallback, all NSTextAttachmentViewProvider views disappear because TextKit 1 uses NSTextAttachmentCellProtocol instead.- **Only works with TextKit 2** — `usesTextAttachmentView` returns false if TextKit 1 is active.
## NSAdaptiveImageGlyph (iOS 18+)
### What It Is
A special inline image that automatically adapts its size to match surrounding text. Used for Genmoji, stickers, and Memoji.
```swiftlet glyph = NSAdaptiveImageGlyph(imageContent: imageData)
glyph.contentIdentifier // Unique IDglyph.contentDescription // Accessibility descriptionglyph.imageContent // Raw image data (multi-resolution)glyph.contentType // UTType```
### How It Differs from NSTextAttachment
| Aspect | NSTextAttachment | NSAdaptiveImageGlyph ||--------|-----------------|---------------------|| Sizing | Manual (bounds property) | Automatic (matches text size) || Aspect ratio | Any | Always square || Resolutions | Single image | Multiple resolutions embedded || Dynamic Type | Manual handling | Automatic scaling || User insertion | Programmatic | Emoji keyboard || Available | iOS 7+ | iOS 18+ |
### Enabling in Text Views
```swifttextView.supportsAdaptiveImageGlyph = true // UITextView// Users can now insert Genmoji from the emoji keyboard```
### Extracting from Attributed String
```swiftattributedString.enumerateAttribute( .adaptiveImageGlyph, in: NSRange(location: 0, length: attributedString.length)) { value, range, _ in if let glyph = value as? NSAdaptiveImageGlyph { print("Genmoji: \(glyph.contentDescription)") }}```
## Common Pitfalls
1. **View attachments lost on TK1 fallback** — The instant anything triggers fallback, all NSTextAttachmentViewProvider views vanish.2. **`tracksTextAttachmentViewBounds` set in loadView** — Too late. Must set in init.3. **macOS: image not displaying** — With `init(data:ofType:)`, must set `attachmentCell` manually or use `init(fileWrapper:)`.4. **Copy/paste loses attachment** — `contents` must be non-nil. Image-only attachments don't survive paste.5. **Bounds y-coordinate confusion** — Negative y goes below baseline. Use `font.descender` for bottom-aligned.6. **Not setting accessibility** — VoiceOver announces U+FFFC as "replacement character" without a description.
## Related Skills
- For protocol signatures, insertion recipes, and support matrices, see [protocols-and-patterns.md](protocols-and-patterns.md).- Use `/skill apple-text-formatting-ref` for non-attachment attributed-text formatting.- Use `/skill apple-text-textkit2-ref` for fragment and viewport behavior around attachments.- Use `/skill apple-text-fallback-triggers` when attachment choices may force compatibility mode.