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.
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.
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 the user is already on TextKit 1 and needs exact NSLayoutManager, NSTextStorage, or NSTextContainer APIs, glyph and layout lifecycle details, temporary attributes, exclusion paths, or multi-container behavior. Reach for this when the stack choice is already made and the task is reference-level TextKit 1 mechanics, not stack selection or symptom-first debugging.apple-text-textkit2-ref: Use when the user is already on TextKit 2 and needs exact NSTextLayoutManager, NSTextContentManager, NSTextContentStorage, viewport layout, fragment, rendering-attribute, or migration details. Reach for this when the stack choice is already made and the task is reference-level TextKit 2 mechanics, not stack selection or generic text-system debugging.apple-text-viewport-rendering: Use when the user needs to understand how Apple text actually renders on screen: viewport layout, line-fragment geometry, rendering attributes, font substitution, fixAttributes, scroll-driven layout, or TextKit versus Core Text drawing differences. Reach for this when the issue is rendering mechanics, not generic layout invalidation.
Full SKILL.md source
---name: apple-text-core-textdescription: 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:
```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.