txt-dynamic-type — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited txt-dynamic-type (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Authored against iOS 26.x / Swift 6.x / Xcode 26.x.
This skill covers how to make text scale correctly across all twelve content-size categories — from xSmall through the five accessibility sizes (AX1-AX5) — across UIKit, AppKit, and SwiftUI. The mental model: semantic text styles scale automatically; raw point sizes don't; custom fonts need UIFontMetrics/relativeTo: to scale; attributed strings need a manual reapply on size change because the font is baked in. Before treating any specific point-size table or scale curve as authoritative, fetch the current Apple docs via Sosumi (sosumi.ai/documentation/uikit/uifont/textstyle) — Apple has tweaked the AX-size curve and added new text styles in past releases.
For VoiceOver, accessibility traits, UIAccessibilityReadingContent, and announcement posting in custom editors, see txt-accessibility. Dynamic Type is one accessibility surface among several; the boundary is intentional.
Apple ships thirteen semantic text styles. Each one carries a default size at the Large category and a documented scale curve up through AX5. The styles below are listed at their default (Large) size:
| Text Style | UIKit / SwiftUI | Weight | Default |
|---|---|---|---|
| Extra Large Title 2 | .extraLargeTitle2 | Bold | 28pt |
| Extra Large Title | .extraLargeTitle | Bold | 36pt |
| Large Title | .largeTitle | Regular | 34pt |
| Title 1 | .title1 / .title | Regular | 28pt |
| Title 2 | .title2 | Regular | 22pt |
| Title 3 | .title3 | Regular | 20pt |
| Headline | .headline | Semibold | 17pt |
| Body | .body | Regular | 17pt |
| Callout | .callout | Regular | 16pt |
| Subheadline | .subheadline | Regular | 15pt |
| Footnote | .footnote | Regular | 13pt |
| Caption 1 | .caption1 / .caption | Regular | 12pt |
| Caption 2 | .caption2 | Regular | 11pt |
The Body curve at each content-size category — the shape every other style follows, scaled relative to its own default:
| Category | Body | Constant |
|---|---|---|
| xSmall | 14pt | .extraSmall |
| Small | 15pt | .small |
| Medium | 16pt | .medium |
| Large (default) | 17pt | .large |
| xLarge | 19pt | .extraLarge |
| xxLarge | 21pt | .extraExtraLarge |
| xxxLarge | 23pt | .extraExtraExtraLarge |
| AX1 | 28pt | .accessibilityMedium |
| AX2 | 33pt | .accessibilityLarge |
| AX3 | 40pt | .accessibilityExtraLarge |
| AX4 | 47pt | .accessibilityExtraExtraLarge |
| AX5 | 53pt | .accessibilityExtraExtraExtraLarge |
At AX5, Body text is roughly 3× the default. Layouts that look fine at xxxLarge often break catastrophically at AX1 because the jump from xxxLarge (23pt) to AX1 (28pt) is the largest single step in the curve and is where horizontal layouts start to overflow.
UILabel, UITextView, UITextField, and UIButton all support Dynamic Type the same way: assign a preferred font and opt into auto-update.
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = trueWithout adjustsFontForContentSizeCategory = true the font is a snapshot at the current category — it won't follow live changes from Settings or Control Center. The flag defaults to false for backwards compatibility, so it must be set explicitly on every Dynamic Type-aware control.
UIFont.systemFont(ofSize: 17) is a fixed 17pt font and won't scale, regardless of adjustsFontForContentSizeCategory. The flag re-asks for the preferred font; a non-preferred font has nothing to re-ask for. Always start from preferredFont(forTextStyle:) or UIFontMetrics.scaledFont(for:).
For non-system fonts, UIFontMetrics translates a base size at the Large category into a category-aware scaled size. Pick the text style whose curve you want the font to follow.
let custom = UIFont(name: "Avenir-Medium", size: 17)!
let metrics = UIFontMetrics(forTextStyle: .body)
label.font = metrics.scaledFont(for: custom)
// Optionally cap the maximum point size:
label.font = metrics.scaledFont(for: custom, maximumPointSize: 28)
label.adjustsFontForContentSizeCategory = trueThe base size passed to UIFont(name:size:) should be the size you want at the Large (default) category. Passing a tiny base size and expecting AX scaling to compensate produces text that's still too small at Medium.
UIFontMetrics.scaledValue(for:) applies the same scale curve to non-font dimensions — padding, icon sizes, spacing, line-height multiples. Wrap any constant that should grow with text to keep iconography proportional with the surrounding type.
adjustsFontForContentSizeCategory only re-asks for the font property. attributedText carries fonts inside .font attributes that are baked into the storage; the flag does nothing for them. After a content-size change, the attributed string still has the original-size font runs.
The fix is to listen for the size-category change and re-apply fonts to the attributed string.
NotificationCenter.default.addObserver(
forName: UIContentSizeCategory.didChangeNotification,
object: nil, queue: .main
) { [weak self] _ in
self?.reapplyDynamicFonts()
}For iOS 17+, the trait-change observer is the modern equivalent and gives you typed access:
registerForTraitChanges([UITraitPreferredContentSizeCategory.self]) { (self: EditorView, _) in
self.updateFonts()
}reapplyDynamicFonts() walks the storage, replaces each font run with UIFontMetrics.scaledFont(for: baseFont) for the appropriate style, and writes the result back. For syntax-highlighted code editors, the practical pattern is to keep the base fonts in a small dictionary keyed by token type and recompute the scaled font for each token type once per category change.
Semantic fonts in SwiftUI scale automatically — there's no opt-in flag.
Text("Scales").font(.body)
Text("Scales").font(.headline)Custom fonts use relativeTo: to attach to a curve:
Text("Custom").font(.custom("Avenir", size: 17, relativeTo: .body)).font(.custom("X", fixedSize: 17)) and .font(.system(size: 17)) are the non-scaling forms. They're the right choice for typographic ornaments that shouldn't grow (a logo glyph, a fixed-grid label) and the wrong choice for body content.
@ScaledMetric scales non-font dimensions:
@ScaledMetric(relativeTo: .body) var iconSize: CGFloat = 24
@ScaledMetric var padding: CGFloat = 16 // defaults to .body curve
Image(systemName: "star")
.frame(width: iconSize, height: iconSize)
.padding(padding)The wrapper recomputes whenever the SwiftUI environment's size category changes, propagating through the view tree without explicit observers.
iOS 15 added per-view scale clamps for situations where the content can't honor the full AX range — fixed-height widgets, in-line glyph runs, dense table cells.
// UIKit
view.minimumContentSizeCategory = .medium
view.maximumContentSizeCategory = .accessibilityLarge
// SwiftUI
view.dynamicTypeSize(.medium ... .accessibility3)The clamp affects the view and its descendants. Use it to enforce a lower bound (so text never shrinks below a readable size) and an upper bound (so a constrained container doesn't have to handle AX5). Don't use it as a quick fix for layouts that should be made flexible — clamping accessibility sizes hurts users who actually need them.
For UI elements that genuinely cannot grow (tab bars, toolbars, segmented controls, compact glyphs), the Large Content Viewer presents a HUD with the enlarged content on long-press:
button.showsLargeContentViewer = true
button.largeContentTitle = "Settings"
button.largeContentImage = UIImage(systemName: "gear")iOS 17+ adjusts line height per-line for scripts with taller ascenders or descenders than Latin (Thai, Arabic, Devanagari, Tibetan). Lines containing only Latin keep the standard height; lines containing tall scripts grow.
If NSParagraphStyle sets explicit minimumLineHeight or maximumLineHeight, the system honors those values and may clip tall-script glyphs anyway. For multilingual content, prefer lineHeightMultiple (a ratio that scales with the font) over fixed heights. If the editor must support arbitrary scripts, leaving line height unconstrained and letting the system pick is safer than guessing.
The recurring layout failures and their fixes:
adjustsFontSizeToFitWidth only as a last resort (it shrinks text below the user's preference). Prefer self-sizing layouts.UIStackView.axis from a trait observer, SwiftUI ViewThatFits or a dynamicTypeSize-conditional layout.@ScaledMetric (SwiftUI) or UIFontMetrics.scaledValue(for:) (UIKit). SF Symbols configured with a text-style font scale automatically.tableView.rowHeight = UITableView.automaticDimension plus estimatedRowHeight, or in SwiftUI rely on List defaults.button.titleLabel?.numberOfLines = 0, SwiftUI is multi-line by default unless .lineLimit(1) is applied.Four ways to exercise size categories:
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge) to the preview, or wrap multiple previews in a Group with one preview per AX size.Test through AX5, not just to AX1 — the layout differences between AX1 and AX5 are larger than the differences between Small and AX1.
.system(size: 17) and UIFont.systemFont(ofSize: 17) are permanently 17pt and never scale. Use semantic styles or wrap a custom font in UIFontMetrics.adjustsFontForContentSizeCategory is a no-op on attributed strings. Re-apply fonts on UIContentSizeCategory.didChangeNotification (or registerForTraitChanges on iOS 17+).adjustsFontForContentSizeCategory = true. Either alone is silently inert. Most "I set adjustsFontForContentSizeCategory but my custom font doesn't scale" reports are missing the UIFontMetrics step; reports that the custom font scales once but not on live category changes are missing the auto-update flag. For attributed runs, scale each run with the matching text style — different runs may want different curves (a .headline run vs a .body run). When caching laid-out attributed strings, observe UIContentSizeCategory.didChangeNotification and rebuild the cache, otherwise the cached string keeps its frozen-size font. The SwiftUI equivalent is Font.custom("X", size: 17, relativeTo: .body) — Font.custom("X", size: 17) (no relativeTo:) is fixed.minimumLineHeight/maximumLineHeight clip tall scripts even with the iOS 17+ per-line adjustment. Prefer lineHeightMultiple or leave line height unconstrained./skill txt-accessibility — VoiceOver, accessibility traits, announcements in custom text editors/skill txt-view-picker — choosing a text view that supports Dynamic Type out of the box/skill txt-line-breaking — paragraph-style line height interactions with Dynamic Type/skill txt-colors — semantic colors and contrast at large sizes~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.