ai/ios-skills/ios-axiom-typography-ref/SKILL.md
Apple platform typography reference (San Francisco fonts, text styles, Dynamic Type, tracking, leading, internationalization) through iOS 26
npx skillsauth add kurko/dotfiles axiom-typography-refInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Complete reference for typography on Apple platforms including San Francisco font system, text styles, Dynamic Type, tracking, leading, and internationalization through iOS 26.
SF Pro and SF Pro Rounded (iOS, iPadOS, macOS, tvOS)
SF Compact and SF Compact Rounded (watchOS, narrow columns)
SF Mono (Code environments, monospaced text)
New York (Serif system font)
Access via:
// iOS/macOS
let descriptor = UIFontDescriptor(fontAttributes: [
.family: "SF Pro",
kCTFontWidthTrait: 1.0 // 1.0 = Expanded
])
SF Arabic (WWDC 2022)
Variable fonts automatically adjust optical size based on point size:
From WWDC 2020:
"TextKit 2 abstracts away glyph handling to provide a consistent experience for international text."
| Text Style | Default Size (iOS) | Use Case |
|------------|-------------------|----------|
| .largeTitle | 34pt | Primary page headings |
| .title | 28pt | Secondary headings |
| .title2 | 22pt | Tertiary headings |
| .title3 | 20pt | Quaternary headings |
| .headline | 17pt (Semibold) | Emphasized body text |
| .body | 17pt | Primary body text |
| .callout | 16pt | Secondary body text |
| .subheadline | 15pt | Tertiary body text |
| .footnote | 13pt | Footnotes, captions |
| .caption | 12pt | Small annotations |
| .caption2 | 11pt | Smallest annotations |
Apply .bold symbolic trait to get emphasized variants:
// UIKit
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1)
let boldDescriptor = descriptor.withSymbolicTraits(.traitBold)!
let font = UIFont(descriptor: boldDescriptor, size: 0)
// SwiftUI
Text("Bold Title")
.font(.title.bold())
Actual weights by text style:
Tight Leading (reduces line height by 2pt on iOS, 1pt on watchOS):
// UIKit
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
let tightDescriptor = descriptor.withSymbolicTraits(.traitTightLeading)!
// SwiftUI
Text("Compact text")
.font(.body.leading(.tight))
Loose Leading (increases line height by 2pt on iOS, 1pt on watchOS):
// SwiftUI
Text("Spacious paragraph")
.font(.body.leading(.loose))
Automatic Scaling (iOS): Text styles scale automatically based on user preferences from Settings → Display & Brightness → Text Size.
Custom Fonts with Dynamic Type:
// UIKit - UIFontMetrics
let customFont = UIFont(name: "Avenir-Medium", size: 34)!
let bodyMetrics = UIFontMetrics(forTextStyle: .body)
let scaledFont = bodyMetrics.scaledFont(for: customFont)
// Also scale constants
let spacing = bodyMetrics.scaledValue(for: 20.0)
// SwiftUI - .font(.custom(_:relativeTo:))
Text("Custom scaled text")
.font(.custom("Avenir-Medium", size: 34, relativeTo: .body))
// @ScaledMetric for values
@ScaledMetric(relativeTo: .body) var padding: CGFloat = 20
macOS
watchOS
visionOS
Tracking adjusts space between letters. Essential for optical size behavior.
Size-Specific Tracking Tables:
SF Pro includes tracking values that vary by point size to maintain optimal spacing:
Example from Apple Design Resources:
Tight Tracking API (for fitting text):
// UIKit
textView.allowsDefaultTightening(for: .byTruncatingTail)
// SwiftUI
Text("Long text that needs to fit")
.lineLimit(1)
.minimumScaleFactor(0.5) // Allows tight tracking
Manual Tracking:
// UIKit
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.preferredFont(forTextStyle: .body),
.kern: 2.0 // 2pt tracking
]
// SwiftUI
Text("Tracked text")
.tracking(2.0)
.kerning(2.0) // Alternative API
Important: Use .tracking() not .kerning() API for semantic correctness. Tracking disables ligatures when necessary; kerning does not.
Default Line Height: Calculated from font's built-in metrics (ascender + descender + line gap).
Language-Aware Adjustments: iOS 17+ automatically increases line height for scripts with tall ascenders/descenders:
From WWDC 2023:
"Automatic line height adjustment for scripts with variable heights"
Manual Leading:
// UIKit
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 8.0 // 8pt additional space
// SwiftUI
Text("Custom spacing")
.lineSpacing(8.0)
New in iOS 18: Font vendors can embed tracking tables in custom fonts using STAT table + CTFont optical size attribute.
let attributes: [String: Any] = [
kCTFontOpticalSizeAttribute as String: pointSize
]
let descriptor = CTFontDescriptorCreateWithAttributes(attributes as CFDictionary)
let font = CTFontCreateWithFontDescriptor(descriptor, pointSize, nil)
Critical Pattern When using AttributedString with SwiftUI's Text, paragraph styles (like lineHeightMultiple) can be lost if fonts come from the environment instead of the attributed content.
From WWDC 2025-280:
"TextEditor substitutes the default value calculated from the environment for any AttributedStringKeys with a value of nil."
This same principle applies to Text—when your AttributedString doesn't specify a font, SwiftUI applies the environment font, which can cause it to rebuild text runs and drop or normalize paragraph style details.
// ❌ WRONG - .font() modifier can override and drop paragraph styles
var s = AttributedString(longString)
// Set paragraph style
var p = AttributedString.ParagraphStyle()
p.lineHeightMultiple = 0.92
s.paragraphStyle = p
// ⚠️ No font set in AttributedString
Text(s)
.font(.body) // ⚠️ May rebuild runs, lose lineHeightMultiple
Why this fails:
AttributedString has no font attribute set (value is nil).font(.body) modifier tells it "use this font for the whole run"Keep typography inside the AttributedString when you need fine control:
// ✅ CORRECT - Font in AttributedString, no environment override
var s = AttributedString(longString)
// Set font INSIDE the attributed content
s.font = .system(.body) // ✅ Typography inside AttributedString
// Set paragraph style
var p = AttributedString.ParagraphStyle()
p.lineHeightMultiple = 0.92
s.paragraphStyle = p
Text(s) // ✅ No .font() modifier
Why this works:
nil).font() modifiervar s = AttributedString("Carefully styled text")
s.font = .system(.body)
var p = AttributedString.ParagraphStyle()
p.lineHeightMultiple = 0.92
p.alignment = .leading
s.paragraphStyle = p
Text(s) // No modifier
When to use:
Text and TextEditorText("Simple text")
.font(.body)
.lineSpacing(4.0) // SwiftUI-level spacing
When to use:
var s = AttributedString("Title")
s.font = .system(.title).bold()
var body = AttributedString(" and body text")
body.font = .system(.body)
s.append(body)
Text(s) // ✅ No .font() modifier preserves both fonts
// ❌ WRONG mental model: "Create AttributedString first"
var s = AttributedString(text)
var p = AttributedString.ParagraphStyle()
p.lineHeightMultiple = 0.92
s.paragraphStyle = p
s.font = .system(.body) // ⚠️ Setting font last doesn't help if you use .font() modifier
Text(s).font(.body) // Still breaks!
The issue isn't when you set the font in AttributedString. The issue is whether the attributed content carries its own font attributes versus relying on SwiftUI's .font(...) environment.
When using AttributedString with paragraph styles:
AttributedString (not nil).font() modifier on Text view (unless intentionally overriding)Complex Script Example (from WWDC 2021):
Kannada word "October":
This is why TextKit 2 uses NSTextLocation instead of integer indices.
Hebrew/Arabic Selection: Single visual selection = multiple NSRanges in AttributedString due to right-to-left layout.
Language-Aware (iOS 17+):
Even Line Breaking (TextKit 2): Justified paragraphs use improved line breaking algorithm:
Best Practices:
.lineLimit(nil) or .lineLimit(2...5) in SwiftUI.minimumScaleFactor() for constrained single-line textSystem UI Font Families:
font-family: system-ui; /* SF Pro */
font-family: ui-rounded; /* SF Pro Rounded */
font-family: ui-serif; /* New York */
font-family: ui-monospace; /* SF Mono */
Legacy:
font-family: -apple-system; /* deprecated, use system-ui */
Text("Recipe Editor")
.font(.largeTitle.bold()) // Emphasized variant
let customFont = UIFont(name: "Avenir-Medium", size: 17)!
let metrics = UIFontMetrics(forTextStyle: .body)
label.font = metrics.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true
let descriptor = UIFontDescriptor
.preferredFontDescriptor(withTextStyle: .largeTitle)
.withDesign(.rounded)!
let font = UIFont(descriptor: descriptor, size: 0)
Text("Today")
.font(.largeTitle.bold())
.fontDesign(.rounded)
struct RecipeView: View {
@ScaledMetric(relativeTo: .body) var padding: CGFloat = 20
var body: some View {
Text("Recipe")
.padding(padding) // Scales with Dynamic Type
}
}
WWDC: 2020-10175, 2022-110381, 2023-10058
Docs: /uikit/uifontdescriptor, /uikit/uifontmetrics, /swiftui/font
data-ai
Merge the current worktree branch into main and sync main back. Use when the user says "merge to main", "ship it", "merge and continue", or after completing a task in a worktree and wanting to continue with the next one.
tools
Synchronize AI agent skills, commands, configs, permissions, hooks, and instructions across Claude Code, Codex CLI, and other Agent Skills-compatible tools. Use when the user asks to pull skills from Claude into Codex, sync Codex work back to Claude, migrate agent commands, reconcile frontmatter, update permissions, or keep agent setup files in parity.
testing
Write or update UI-independent use cases for QA. Use when the user says "write use cases", "add use cases", "QA use cases", "update use cases", "compose use cases", or when starting implementation of a new feature (after plan approval). Also activates for "what should we test", "regression cases", or "use cases for QA".
documentation
Skill on how to write a task. Use when user asks you to write a task (for Asana, Linear, Jira, Notion and equivalent). Also activates when user says "create task", "write task", or similar task creation workflow requests.