Skip to content

Core Text for TextKit Developers

Use when dropping below TextKit to Core Text for glyph-level access, custom typesetting, hit testing, font table access, or performance-critical text rendering. Reach for this when TextKit is no longer the right abstraction, not when the user just needs normal TextKit reference APIs.

Reference Skills

Use when dropping below TextKit to Core Text for glyph-level access, custom typesetting, hit testing, font table access, or performance-critical text rendering. Reach for this when TextKit is no longer the right abstraction, not when the user just needs normal TextKit reference APIs.

Family: Text Model And Foundation Utilities

Use this skill when TextKit cannot do what you need and you must drop to Core Text for glyph-level control.

  • You need glyph positions, advances, or bounding boxes that TextKit 2 hides
  • You are doing custom typesetting or non-standard line breaking
  • You need font table access or OpenType feature control
  • You are rendering text with custom Core Graphics effects per glyph
  • You need hit testing or caret positioning outside of a TextKit text container
Need glyph-level access?
TextKit 1 available? → Use NSLayoutManager glyph APIs
TextKit 2 only? → Drop to Core Text (this skill)
Need custom line breaking?
→ CTTypesetter
Need to draw text into a CGContext directly?
→ CTLine or CTFrame
Need font metrics, tables, or OpenType features?
→ CTFont
Need inline non-text elements with custom metrics?
→ CTRunDelegate
CTFramesetter (factory)
→ CTTypesetter (line breaking)
→ CTFrame (laid-out region)
→ CTLine (one visual line)
→ CTRun (contiguous glyphs, same attributes)
→ CGGlyph[] (actual glyph IDs)
→ CGPoint[] (positions)
→ CGSize[] (advances)

Core Text sits directly above Core Graphics. It is a C API using Core Foundation types. Available since iOS 3.2 / macOS 10.5. Both TextKit 1 and TextKit 2 render through Core Text internally — it is the foundation under both, not part of either.

Thread safety: Font objects (CTFont, CTFontDescriptor, CTFontCollection) are thread-safe and can be shared across threads. Layout objects (CTTypesetter, CTFramesetter, CTRun, CTLine, CTFrame) are NOT thread-safe — use them on a single thread only.

For most TextKit developers, CTLine is the entry point. Create one from an attributed string to get glyph information:

let attributedString = NSAttributedString(string: "Hello 👋🏽",
attributes: [.font: UIFont.systemFont(ofSize: 16)])
let line = CTLineCreateWithAttributedString(attributedString)
// Typographic bounds
var ascent: CGFloat = 0, descent: CGFloat = 0, leading: CGFloat = 0
let width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
let height = ascent + descent + leading
// Hit testing: point → string index
let index = CTLineGetStringIndexForPosition(line, CGPoint(x: 50, y: 0))
// Caret positioning: string index → x offset
let offset = CTLineGetOffsetForStringIndex(line, 3, nil)
// Image bounds (actual rendered pixels, not typographic)
let ctx = UIGraphicsGetCurrentContext()!
let imageBounds = CTLineGetImageBounds(line, ctx)

Each CTRun is a contiguous sequence of glyphs sharing the same attributes:

let runs = CTLineGetGlyphRuns(line) as! [CTRun]
for run in runs {
let glyphCount = CTRunGetGlyphCount(run)
// Get all glyphs
var glyphs = [CGGlyph](repeating: 0, count: glyphCount)
CTRunGetGlyphs(run, CFRange(location: 0, length: glyphCount), &glyphs)
// Get positions (relative to line origin)
var positions = [CGPoint](repeating: .zero, count: glyphCount)
CTRunGetPositions(run, CFRange(location: 0, length: glyphCount), &positions)
// Get advances (width of each glyph)
var advances = [CGSize](repeating: .zero, count: glyphCount)
CTRunGetAdvances(run, CFRange(location: 0, length: glyphCount), &advances)
// Map glyph indices back to string indices (UTF-16)
var stringIndices = [CFIndex](repeating: 0, count: glyphCount)
CTRunGetStringIndices(run, CFRange(location: 0, length: glyphCount), &stringIndices)
// Get the run's attributes
let attrs = CTRunGetAttributes(run) as! [NSAttributedString.Key: Any]
let font = attrs[.font] as! CTFont
}

CTFramesetter / CTFrame — Multi-Line Layout

Section titled “CTFramesetter / CTFrame — Multi-Line Layout”

For laying out text into a rectangular (or custom-shaped) region:

let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
// Suggest frame size for constrained width
let constraint = CGSize(width: 300, height: .greatestFiniteMagnitude)
let suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(
framesetter, CFRange(location: 0, length: 0), nil, constraint, nil)
// Create frame in a path
let path = CGPath(rect: CGRect(origin: .zero, size: suggestedSize), transform: nil)
let frame = CTFramesetterCreateFrame(framesetter, CFRange(location: 0, length: 0), path, nil)
// Get lines and origins
let lines = CTFrameGetLines(frame) as! [CTLine]
var origins = [CGPoint](repeating: .zero, count: lines.count)
CTFrameGetLineOrigins(frame, CFRange(location: 0, length: lines.count), &origins)
// Draw the entire frame
CTFrameDraw(frame, context)

For control over where lines break:

let typesetter = CTTypesetterCreateWithAttributedString(attributedString)
var start: CFIndex = 0
let stringLength = CFAttributedStringGetLength(attributedString)
while start < stringLength {
// Suggest line break for a given width
let count = CTTypesetterSuggestLineBreak(typesetter, start, 300.0)
// Or suggest with cluster breaking (for CJK)
// let count = CTTypesetterSuggestClusterBreak(typesetter, start, 300.0)
let line = CTTypesetterCreateLine(typesetter, CFRange(location: start, length: count))
// Position and draw the line
start += count
}
// Create from UIFont
let uiFont = UIFont.systemFont(ofSize: 16)
let ctFont = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
// Metrics
let ascent = CTFontGetAscent(ctFont)
let descent = CTFontGetDescent(ctFont)
let leading = CTFontGetLeading(ctFont)
let unitsPerEm = CTFontGetUnitsPerEm(ctFont)
// Get glyphs for characters
var characters: [UniChar] = Array("A".utf16)
var glyphs = [CGGlyph](repeating: 0, count: characters.count)
CTFontGetGlyphsForCharacters(ctFont, &characters, &glyphs, characters.count)
// Glyph bounding boxes
var boundingRects = [CGRect](repeating: .zero, count: glyphs.count)
CTFontGetBoundingRectsForGlyphs(ctFont, .default, glyphs, &boundingRects, glyphs.count)
// Glyph path (for custom rendering)
if let path = CTFontCreatePathForGlyph(ctFont, glyphs[0], nil) {
// Draw the glyph outline
context.addPath(path)
context.fillPath()
}
// OpenType features
let features = CTFontCopyFeatures(ctFont) as? [[String: Any]] ?? []

Reserve space in a line for non-text content (images, custom views):

var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateCurrentVersion,
dealloc: { _ in },
getAscent: { _ in 20 }, // Height above baseline
getDescent: { _ in 5 }, // Depth below baseline
getWidth: { _ in 30 } // Width of the space
)
let delegate = CTRunDelegateCreate(&callbacks, nil)!
let attrs: [NSAttributedString.Key: Any] = [
kCTRunDelegateAttributeName as NSAttributedString.Key: delegate
]
let placeholder = NSAttributedString(string: "\u{FFFC}", attributes: attrs)
// Insert into your attributed string
// Then draw your custom content at the run's position after layout

Core Text uses bottom-left origin (Core Graphics). UIKit uses top-left origin.

override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// REQUIRED: Flip coordinate system for Core Text
context.textMatrix = .identity
context.translateBy(x: 0, y: bounds.height)
context.scaleBy(x: 1, y: -1)
// Now draw
CTLineDraw(line, context)
// or CTFrameDraw(frame, context)
}

Forgetting the flip is the #1 Core Text mistake. Text renders upside-down or at the wrong position.

CTFrameGetLineOrigins returns origins in Core Text coordinates (bottom-left). To use in UIKit:

let lines = CTFrameGetLines(frame) as! [CTLine]
var origins = [CGPoint](repeating: .zero, count: lines.count)
CTFrameGetLineOrigins(frame, CFRange(location: 0, length: lines.count), &origins)
for (i, line) in lines.enumerated() {
// Flip y: UIKit y = frameHeight - CoreText y
let uikitY = frameRect.height - origins[i].y
// uikitY is now the BASELINE position in UIKit coordinates
}

TextKit 2: Getting Glyph Info from a Layout Fragment

Section titled “TextKit 2: Getting Glyph Info from a Layout Fragment”
// In a custom NSTextLayoutFragment or delegate callback:
let attributedString = textElement.attributedString
let line = CTLineCreateWithAttributedString(attributedString as CFAttributedString)
let runs = CTLineGetGlyphRuns(line) as! [CTRun]
for run in runs {
let glyphCount = CTRunGetGlyphCount(run)
var positions = [CGPoint](repeating: .zero, count: glyphCount)
CTRunGetPositions(run, CFRange(location: 0, length: glyphCount), &positions)
// positions are relative to the line origin
// Add layoutFragmentFrame.origin to get document coordinates
// Remember to handle the coordinate flip if drawing in UIKit
}
// macOS: NSFont ↔ CTFont is toll-free bridged
let ctFont = nsFont as CTFont
let nsFont = ctFont as NSFont
// iOS: UIFont ↔ CTFont is NOT toll-free bridged
// UIFont → CTFont
let ctFont = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
// CTFont → UIFont
let uiFont = UIFont(name: CTFontCopyPostScriptName(ctFont) as String,
size: CTFontGetSize(ctFont))!
// NSAttributedString ↔ CFAttributedString IS toll-free bridged (both platforms)
let cfAttrStr = attributedString as CFAttributedString

Core Text uses its own attribute keys that differ from UIKit/AppKit:

PurposeCore Text KeyUIKit/AppKit Key
FontkCTFontAttributeName (CTFont).font (UIFont/NSFont)
Foreground colorkCTForegroundColorAttributeName (CGColor).foregroundColor (UIColor/NSColor)
Paragraph stylekCTParagraphStyleAttributeName (CTParagraphStyle).paragraphStyle (NSParagraphStyle)
KernkCTKernAttributeName.kern

When creating attributed strings for Core Text directly, use the kCT* keys. Mixing Core Text keys and UIKit keys in the same attributed string can cause subtle rendering differences.

In practice, UIKit’s .font attribute (UIFont) works with Core Text because UIFont wraps a CTFont internally. But .foregroundColor (UIColor) does NOT — Core Text needs CGColor.

  1. Forgetting to flip coordinates — Core Text is bottom-left origin. UIKit is top-left. Text appears upside-down or at wrong position. Always set context.textMatrix = .identity and flip.
  2. Not resetting text matrixCGContext.textMatrix persists between drawing calls. If a previous operation set a non-identity matrix, your Core Text drawing will be transformed unexpectedly.
  3. String indices are UTF-16CTRunGetStringIndices returns UTF-16 code unit indices (matching NSString), not Swift Character indices. A single emoji can span 2-4 UTF-16 units.
  4. CTFont ≠ UIFont on iOS — They are NOT toll-free bridged on iOS. Create CTFont explicitly.
  5. CTFrameGetLines returns non-retained array — In Swift this is usually managed automatically, but be careful with the CFArray if you bridge to C.
  6. Attribute key mismatchkCTForegroundColorAttributeName expects CGColor, not UIColor. Passing UIColor silently fails (no color rendered).
  7. Character-glyph mapping is not 1:1 — Ligatures produce fewer glyphs than characters. Complex scripts (Arabic, Devanagari) can produce more glyphs than characters. Always use CTRunGetStringIndices for the mapping.
  8. CTParagraphStyle is not NSParagraphStyle — They are related but not interchangeable. CTParagraphStyle uses a C struct API; NSParagraphStyle has Objective-C properties. NSParagraphStyle internally wraps CTParagraphStyle.

This page documents the apple-text-core-text reference skill. Use it when the subsystem is already known and you need mechanics, behavior, or API detail.

  • 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
SKILL.md
---
name: apple-text-core-text
description: Use when dropping below TextKit to Core Text for glyph-level access, custom typesetting, hit testing, font table access, or performance-critical text rendering. Reach for this when TextKit is no longer the right abstraction, not when the user just needs normal TextKit reference APIs.
license: MIT
---
# Core Text for TextKit Developers
Use this skill when TextKit cannot do what you need and you must drop to Core Text for glyph-level control.
## When to Use
- You need glyph positions, advances, or bounding boxes that TextKit 2 hides
- You are doing custom typesetting or non-standard line breaking
- You need font table access or OpenType feature control
- You are rendering text with custom Core Graphics effects per glyph
- You need hit testing or caret positioning outside of a TextKit text container
## Quick Decision
```
Need glyph-level access?
TextKit 1 available? → Use NSLayoutManager glyph APIs
TextKit 2 only? → Drop to Core Text (this skill)
Need custom line breaking?
→ CTTypesetter
Need to draw text into a CGContext directly?
→ CTLine or CTFrame
Need font metrics, tables, or OpenType features?
→ CTFont
Need inline non-text elements with custom metrics?
→ CTRunDelegate
```
## Core Guidance
## Architecture
```
CTFramesetter (factory)
→ CTTypesetter (line breaking)
→ CTFrame (laid-out region)
→ CTLine (one visual line)
→ CTRun (contiguous glyphs, same attributes)
→ CGGlyph[] (actual glyph IDs)
→ CGPoint[] (positions)
→ CGSize[] (advances)
```
Core Text sits directly above Core Graphics. It is a C API using Core Foundation types. Available since iOS 3.2 / macOS 10.5. Both TextKit 1 and TextKit 2 render through Core Text internally — it is the foundation under both, not part of either.
**Thread safety:** Font objects (CTFont, CTFontDescriptor, CTFontCollection) are thread-safe and can be shared across threads. Layout objects (CTTypesetter, CTFramesetter, CTRun, CTLine, CTFrame) are **NOT thread-safe** — use them on a single thread only.
## CTLine — The Most Common Escape Hatch
For most TextKit developers, `CTLine` is the entry point. Create one from an attributed string to get glyph information:
```swift
let attributedString = NSAttributedString(string: "Hello 👋🏽",
attributes: [.font: UIFont.systemFont(ofSize: 16)])
let line = CTLineCreateWithAttributedString(attributedString)
// Typographic bounds
var ascent: CGFloat = 0, descent: CGFloat = 0, leading: CGFloat = 0
let width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
let height = ascent + descent + leading
// Hit testing: point → string index
let index = CTLineGetStringIndexForPosition(line, CGPoint(x: 50, y: 0))
// Caret positioning: string index → x offset
let offset = CTLineGetOffsetForStringIndex(line, 3, nil)
// Image bounds (actual rendered pixels, not typographic)
let ctx = UIGraphicsGetCurrentContext()!
let imageBounds = CTLineGetImageBounds(line, ctx)
```
## CTRun — Glyph-Level Access
Each `CTRun` is a contiguous sequence of glyphs sharing the same attributes:
```swift
let runs = CTLineGetGlyphRuns(line) as! [CTRun]
for run in runs {
let glyphCount = CTRunGetGlyphCount(run)
// Get all glyphs
var glyphs = [CGGlyph](repeating: 0, count: glyphCount)
CTRunGetGlyphs(run, CFRange(location: 0, length: glyphCount), &glyphs)
// Get positions (relative to line origin)
var positions = [CGPoint](repeating: .zero, count: glyphCount)
CTRunGetPositions(run, CFRange(location: 0, length: glyphCount), &positions)
// Get advances (width of each glyph)
var advances = [CGSize](repeating: .zero, count: glyphCount)
CTRunGetAdvances(run, CFRange(location: 0, length: glyphCount), &advances)
// Map glyph indices back to string indices (UTF-16)
var stringIndices = [CFIndex](repeating: 0, count: glyphCount)
CTRunGetStringIndices(run, CFRange(location: 0, length: glyphCount), &stringIndices)
// Get the run's attributes
let attrs = CTRunGetAttributes(run) as! [NSAttributedString.Key: Any]
let font = attrs[.font] as! CTFont
}
```
## CTFramesetter / CTFrame — Multi-Line Layout
For laying out text into a rectangular (or custom-shaped) region:
```swift
let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
// Suggest frame size for constrained width
let constraint = CGSize(width: 300, height: .greatestFiniteMagnitude)
let suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(
framesetter, CFRange(location: 0, length: 0), nil, constraint, nil)
// Create frame in a path
let path = CGPath(rect: CGRect(origin: .zero, size: suggestedSize), transform: nil)
let frame = CTFramesetterCreateFrame(framesetter, CFRange(location: 0, length: 0), path, nil)
// Get lines and origins
let lines = CTFrameGetLines(frame) as! [CTLine]
var origins = [CGPoint](repeating: .zero, count: lines.count)
CTFrameGetLineOrigins(frame, CFRange(location: 0, length: lines.count), &origins)
// Draw the entire frame
CTFrameDraw(frame, context)
```
## CTTypesetter — Custom Line Breaking
For control over where lines break:
```swift
let typesetter = CTTypesetterCreateWithAttributedString(attributedString)
var start: CFIndex = 0
let stringLength = CFAttributedStringGetLength(attributedString)
while start < stringLength {
// Suggest line break for a given width
let count = CTTypesetterSuggestLineBreak(typesetter, start, 300.0)
// Or suggest with cluster breaking (for CJK)
// let count = CTTypesetterSuggestClusterBreak(typesetter, start, 300.0)
let line = CTTypesetterCreateLine(typesetter, CFRange(location: start, length: count))
// Position and draw the line
start += count
}
```
## CTFont — Font Metrics and Features
```swift
// Create from UIFont
let uiFont = UIFont.systemFont(ofSize: 16)
let ctFont = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
// Metrics
let ascent = CTFontGetAscent(ctFont)
let descent = CTFontGetDescent(ctFont)
let leading = CTFontGetLeading(ctFont)
let unitsPerEm = CTFontGetUnitsPerEm(ctFont)
// Get glyphs for characters
var characters: [UniChar] = Array("A".utf16)
var glyphs = [CGGlyph](repeating: 0, count: characters.count)
CTFontGetGlyphsForCharacters(ctFont, &characters, &glyphs, characters.count)
// Glyph bounding boxes
var boundingRects = [CGRect](repeating: .zero, count: glyphs.count)
CTFontGetBoundingRectsForGlyphs(ctFont, .default, glyphs, &boundingRects, glyphs.count)
// Glyph path (for custom rendering)
if let path = CTFontCreatePathForGlyph(ctFont, glyphs[0], nil) {
// Draw the glyph outline
context.addPath(path)
context.fillPath()
}
// OpenType features
let features = CTFontCopyFeatures(ctFont) as? [[String: Any]] ?? []
```
## CTRunDelegate — Inline Custom Elements
Reserve space in a line for non-text content (images, custom views):
```swift
var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateCurrentVersion,
dealloc: { _ in },
getAscent: { _ in 20 }, // Height above baseline
getDescent: { _ in 5 }, // Depth below baseline
getWidth: { _ in 30 } // Width of the space
)
let delegate = CTRunDelegateCreate(&callbacks, nil)!
let attrs: [NSAttributedString.Key: Any] = [
kCTRunDelegateAttributeName as NSAttributedString.Key: delegate
]
let placeholder = NSAttributedString(string: "\u{FFFC}", attributes: attrs)
// Insert into your attributed string
// Then draw your custom content at the run's position after layout
```
## Critical: Coordinate System
**Core Text uses bottom-left origin (Core Graphics). UIKit uses top-left origin.**
### Drawing Core Text in UIKit
```swift
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// REQUIRED: Flip coordinate system for Core Text
context.textMatrix = .identity
context.translateBy(x: 0, y: bounds.height)
context.scaleBy(x: 1, y: -1)
// Now draw
CTLineDraw(line, context)
// or CTFrameDraw(frame, context)
}
```
**Forgetting the flip is the #1 Core Text mistake.** Text renders upside-down or at the wrong position.
### Converting Line Origins from CTFrame
`CTFrameGetLineOrigins` returns origins in Core Text coordinates (bottom-left). To use in UIKit:
```swift
let lines = CTFrameGetLines(frame) as! [CTLine]
var origins = [CGPoint](repeating: .zero, count: lines.count)
CTFrameGetLineOrigins(frame, CFRange(location: 0, length: lines.count), &origins)
for (i, line) in lines.enumerated() {
// Flip y: UIKit y = frameHeight - CoreText y
let uikitY = frameRect.height - origins[i].y
// uikitY is now the BASELINE position in UIKit coordinates
}
```
## Bridging TextKit ↔ Core Text
### TextKit 2: Getting Glyph Info from a Layout Fragment
```swift
// In a custom NSTextLayoutFragment or delegate callback:
let attributedString = textElement.attributedString
let line = CTLineCreateWithAttributedString(attributedString as CFAttributedString)
let runs = CTLineGetGlyphRuns(line) as! [CTRun]
for run in runs {
let glyphCount = CTRunGetGlyphCount(run)
var positions = [CGPoint](repeating: .zero, count: glyphCount)
CTRunGetPositions(run, CFRange(location: 0, length: glyphCount), &positions)
// positions are relative to the line origin
// Add layoutFragmentFrame.origin to get document coordinates
// Remember to handle the coordinate flip if drawing in UIKit
}
```
### Font Bridging
```swift
// macOS: NSFont ↔ CTFont is toll-free bridged
let ctFont = nsFont as CTFont
let nsFont = ctFont as NSFont
// iOS: UIFont ↔ CTFont is NOT toll-free bridged
// UIFont → CTFont
let ctFont = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
// CTFont → UIFont
let uiFont = UIFont(name: CTFontCopyPostScriptName(ctFont) as String,
size: CTFontGetSize(ctFont))!
// NSAttributedString ↔ CFAttributedString IS toll-free bridged (both platforms)
let cfAttrStr = attributedString as CFAttributedString
```
### Attribute Key Differences
Core Text uses its own attribute keys that differ from UIKit/AppKit:
| Purpose | Core Text Key | UIKit/AppKit Key |
|---------|--------------|-----------------|
| Font | `kCTFontAttributeName` (CTFont) | `.font` (UIFont/NSFont) |
| Foreground color | `kCTForegroundColorAttributeName` (CGColor) | `.foregroundColor` (UIColor/NSColor) |
| Paragraph style | `kCTParagraphStyleAttributeName` (CTParagraphStyle) | `.paragraphStyle` (NSParagraphStyle) |
| Kern | `kCTKernAttributeName` | `.kern` |
**When creating attributed strings for Core Text directly, use the `kCT*` keys.** Mixing Core Text keys and UIKit keys in the same attributed string can cause subtle rendering differences.
In practice, UIKit's `.font` attribute (UIFont) works with Core Text because UIFont wraps a CTFont internally. But `.foregroundColor` (UIColor) does NOT — Core Text needs CGColor.
## Common Pitfalls
1. **Forgetting to flip coordinates** — Core Text is bottom-left origin. UIKit is top-left. Text appears upside-down or at wrong position. Always set `context.textMatrix = .identity` and flip.
2. **Not resetting text matrix**`CGContext.textMatrix` persists between drawing calls. If a previous operation set a non-identity matrix, your Core Text drawing will be transformed unexpectedly.
3. **String indices are UTF-16**`CTRunGetStringIndices` returns UTF-16 code unit indices (matching NSString), not Swift Character indices. A single emoji can span 2-4 UTF-16 units.
4. **CTFont ≠ UIFont on iOS** — They are NOT toll-free bridged on iOS. Create CTFont explicitly.
5. **CTFrameGetLines returns non-retained array** — In Swift this is usually managed automatically, but be careful with the CFArray if you bridge to C.
6. **Attribute key mismatch**`kCTForegroundColorAttributeName` expects CGColor, not UIColor. Passing UIColor silently fails (no color rendered).
7. **Character-glyph mapping is not 1:1** — Ligatures produce fewer glyphs than characters. Complex scripts (Arabic, Devanagari) can produce more glyphs than characters. Always use `CTRunGetStringIndices` for the mapping.
8. **CTParagraphStyle is not NSParagraphStyle** — They are related but not interchangeable. CTParagraphStyle uses a C struct API; NSParagraphStyle has Objective-C properties. NSParagraphStyle internally wraps CTParagraphStyle.
## Related Skills
- Use `/skill apple-text-textkit2-ref` for the TextKit 2 APIs that sit above Core Text.
- Use `/skill apple-text-textkit1-ref` when NSLayoutManager's glyph APIs are sufficient (no need to drop lower).
- Use `/skill apple-text-viewport-rendering` for how Core Text fits into the rendering pipeline.
- Use `/skill apple-text-attachments-ref` when CTRunDelegate is used for inline non-text content.