Core Text for TextKit Developers
Use when working with Core Text for glyph-level access, custom typesetting, hit testing, font tables, or when TextKit 2 lacks the glyph APIs you need.
Use when working with Core Text for glyph-level access, custom typesetting, hit testing, font tables, or when TextKit 2 lacks the glyph APIs you need.
Family: Text Model And Foundation Utilities
Use this skill when you need glyph-level control — either because TextKit 2 has no glyph APIs, or because your use case (custom typesetting, font tables, per-glyph rendering) requires the Core Text layer directly.
When to Use
Section titled “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
Section titled “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? → CTRunDelegateCore Guidance
Section titled “Core Guidance”Architecture
Section titled “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
Section titled “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:
let attributedString = NSAttributedString(string: "Hello 👋🏽", attributes: [.font: UIFont.systemFont(ofSize: 16)])let line = CTLineCreateWithAttributedString(attributedString)
// Typographic boundsvar ascent: CGFloat = 0, descent: CGFloat = 0, leading: CGFloat = 0let width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading)let height = ascent + descent + leading
// Hit testing: point → string indexlet index = CTLineGetStringIndexForPosition(line, CGPoint(x: 50, y: 0))
// Caret positioning: string index → x offsetlet offset = CTLineGetOffsetForStringIndex(line, 3, nil)
// Image bounds (actual rendered pixels, not typographic)let ctx = UIGraphicsGetCurrentContext()!let imageBounds = CTLineGetImageBounds(line, ctx)CTRun — Glyph-Level Access
Section titled “CTRun — Glyph-Level Access”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 widthlet constraint = CGSize(width: 300, height: .greatestFiniteMagnitude)let suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints( framesetter, CFRange(location: 0, length: 0), nil, constraint, nil)
// Create frame in a pathlet path = CGPath(rect: CGRect(origin: .zero, size: suggestedSize), transform: nil)let frame = CTFramesetterCreateFrame(framesetter, CFRange(location: 0, length: 0), path, nil)
// Get lines and originslet 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 frameCTFrameDraw(frame, context)CTTypesetter — Custom Line Breaking
Section titled “CTTypesetter — Custom Line Breaking”For control over where lines break:
let typesetter = CTTypesetterCreateWithAttributedString(attributedString)
var start: CFIndex = 0let 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
Section titled “CTFont — Font Metrics and Features”// Create from UIFontlet uiFont = UIFont.systemFont(ofSize: 16)let ctFont = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
// Metricslet ascent = CTFontGetAscent(ctFont)let descent = CTFontGetDescent(ctFont)let leading = CTFontGetLeading(ctFont)let unitsPerEm = CTFontGetUnitsPerEm(ctFont)
// Get glyphs for charactersvar characters: [UniChar] = Array("A".utf16)var glyphs = [CGGlyph](repeating: 0, count: characters.count)CTFontGetGlyphsForCharacters(ctFont, &characters, &glyphs, characters.count)
// Glyph bounding boxesvar 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 featureslet features = CTFontCopyFeatures(ctFont) as? [[String: Any]] ?? []CTRunDelegate — Inline Custom Elements
Section titled “CTRunDelegate — Inline Custom Elements”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 layoutCritical: Coordinate System
Section titled “Critical: Coordinate System”Core Text uses bottom-left origin (Core Graphics). UIKit uses top-left origin.
Drawing Core Text in UIKit
Section titled “Drawing Core Text in UIKit”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
Section titled “Converting Line Origins from CTFrame”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}Bridging TextKit ↔ Core Text
Section titled “Bridging TextKit ↔ Core Text”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.attributedStringlet 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
Section titled “Font Bridging”// macOS: NSFont ↔ CTFont is toll-free bridgedlet ctFont = nsFont as CTFontlet nsFont = ctFont as NSFont
// iOS: UIFont ↔ CTFont is NOT toll-free bridged// UIFont → CTFontlet ctFont = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
// CTFont → UIFontlet uiFont = UIFont(name: CTFontCopyPostScriptName(ctFont) as String, size: CTFontGetSize(ctFont))!
// NSAttributedString ↔ CFAttributedString IS toll-free bridged (both platforms)let cfAttrStr = attributedString as CFAttributedStringAttribute Key Differences
Section titled “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
Section titled “Common Pitfalls”- 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 = .identityand flip. - Not resetting text matrix —
CGContext.textMatrixpersists between drawing calls. If a previous operation set a non-identity matrix, your Core Text drawing will be transformed unexpectedly. - String indices are UTF-16 —
CTRunGetStringIndicesreturns UTF-16 code unit indices (matching NSString), not Swift Character indices. A single emoji can span 2-4 UTF-16 units. - CTFont ≠ UIFont on iOS — They are NOT toll-free bridged on iOS. Create CTFont explicitly.
- CTFrameGetLines returns non-retained array — In Swift this is usually managed automatically, but be careful with the CFArray if you bridge to C.
- Attribute key mismatch —
kCTForegroundColorAttributeNameexpects CGColor, not UIColor. Passing UIColor silently fails (no color rendered). - 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
CTRunGetStringIndicesfor the mapping. - 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.
Documentation Scope
Section titled “Documentation Scope”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.
Related
Section titled “Related”apple-text-textkit1-ref: Use when working with TextKit 1 and you need NSLayoutManager, NSTextStorage, or NSTextContainer APIs — glyphs, temporary attributes, multi-container.apple-text-textkit2-ref: Use when working with TextKit 2 and you need NSTextLayoutManager or NSTextContentManager APIs — viewport layout, fragments, rendering attributes.apple-text-viewport-rendering: Use when working with viewport layout, line-fragment geometry, rendering attributes, font substitution, or scroll-driven layout.
Full SKILL.md source
---name: apple-text-core-textdescription: Use when working with Core Text for glyph-level access, custom typesetting, hit testing, font tables, or when TextKit 2 lacks the glyph APIs you needlicense: MIT---
# Core Text for TextKit Developers
Use this skill when you need glyph-level control — either because TextKit 2 has no glyph APIs, or because your use case (custom typesetting, font tables, per-glyph rendering) requires the Core Text layer directly.
## 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:
```swiftlet attributedString = NSAttributedString(string: "Hello 👋🏽", attributes: [.font: UIFont.systemFont(ofSize: 16)])let line = CTLineCreateWithAttributedString(attributedString)
// Typographic boundsvar ascent: CGFloat = 0, descent: CGFloat = 0, leading: CGFloat = 0let width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading)let height = ascent + descent + leading
// Hit testing: point → string indexlet index = CTLineGetStringIndexForPosition(line, CGPoint(x: 50, y: 0))
// Caret positioning: string index → x offsetlet 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:
```swiftlet 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:
```swiftlet framesetter = CTFramesetterCreateWithAttributedString(attributedString)
// Suggest frame size for constrained widthlet constraint = CGSize(width: 300, height: .greatestFiniteMagnitude)let suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints( framesetter, CFRange(location: 0, length: 0), nil, constraint, nil)
// Create frame in a pathlet path = CGPath(rect: CGRect(origin: .zero, size: suggestedSize), transform: nil)let frame = CTFramesetterCreateFrame(framesetter, CFRange(location: 0, length: 0), path, nil)
// Get lines and originslet 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 frameCTFrameDraw(frame, context)```
## CTTypesetter — Custom Line Breaking
For control over where lines break:
```swiftlet typesetter = CTTypesetterCreateWithAttributedString(attributedString)
var start: CFIndex = 0let 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 UIFontlet uiFont = UIFont.systemFont(ofSize: 16)let ctFont = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
// Metricslet ascent = CTFontGetAscent(ctFont)let descent = CTFontGetDescent(ctFont)let leading = CTFontGetLeading(ctFont)let unitsPerEm = CTFontGetUnitsPerEm(ctFont)
// Get glyphs for charactersvar characters: [UniChar] = Array("A".utf16)var glyphs = [CGGlyph](repeating: 0, count: characters.count)CTFontGetGlyphsForCharacters(ctFont, &characters, &glyphs, characters.count)
// Glyph bounding boxesvar 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 featureslet features = CTFontCopyFeatures(ctFont) as? [[String: Any]] ?? []```
## CTRunDelegate — Inline Custom Elements
Reserve space in a line for non-text content (images, custom views):
```swiftvar 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
```swiftoverride 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:
```swiftlet 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.attributedStringlet 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 bridgedlet ctFont = nsFont as CTFontlet nsFont = ctFont as NSFont
// iOS: UIFont ↔ CTFont is NOT toll-free bridged// UIFont → CTFontlet ctFont = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
// CTFont → UIFontlet 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.