@ScaledMetric is a SwiftUI property wrapper that automatically scales a numeric value according to the user’s Dynamic Type settings. The value is wrapped in @ScaledMetric and recalculated when the system font size changes, ensuring interface accessibility for people with visual impairments. According to Apple Developer Documentation (2026), @ScaledMetric uses the UIFontMetrics scale to compute the relative scale based on the preferred content size category. Learn more about accessibility in the SwiftUI accessibility article.
Key takeaways
@ScaledMetric is a SwiftUI property wrapper, added in iOS 14, that automatically scales a numeric value (CGFloat, Int, Double) to the current Dynamic Type font size. Unlike .font(.body) for fonts, @ScaledMetric scales any numeric parameters: padding, spacing, cornerRadius, iconSize — everything that should proportionally increase with larger text.
The main purpose of @ScaledMetric is to provide accessibility scaling for non-text interface elements. When the user increases the font size in iOS settings, buttons, icons, and spacing should scale proportionally to keep the interface balanced. @ScaledMetric handles this automatically, without manual multiplier calculation.
Basic syntax of @ScaledMetric uses a default value and an optional relativeTo parameter. If relativeTo is specified, scaling is tied to a specific text style (UIFontTextStyle). If not, the .body scale is used.
struct AccessibleButton: View {
@ScaledMetric private var padding: CGFloat = 12
@ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = 24
var body: some View {
Label("Submit", systemImage: "checkmark.circle.fill")
.font(.body)
.padding(padding)
.imageScale(.init(rawValue: iconSize / 24) ?? .medium)
}
}
padding will scale relative to .body (default), iconSize relative to .title. With larger text, spacing and icons will scale proportionally. Without @ScaledMetric, padding would remain at 12 pt regardless of font size, causing visual imbalance.
The @ScaledMetric mechanism is based on UIFontMetrics from UIKit. When SwiftUI creates an @ScaledMetric instance, it computes a multiplier based on the current preferred content size category (UIContentSizeCategory). The base value is multiplied by the scaledValue from UIFontMetrics for the specified text style.
Mathematically: ScaledMetricValue = baseValue × UIFontMetrics.scaledValue(for: relativeTo). If relativeTo is not specified, UIFontMetrics.default is used, tied to .body. When Dynamic Type changes, SwiftUI recreates the View body, @ScaledMetric computes a new scaledValue, and the UI updates automatically through the @State-like PropertyWrappers mechanism.
The iOS scale includes 11 sizes: from .extraSmall (5 pt) to .accessibilityExtraExtraExtraLarge (77 pt for .body). The scaling factor for .body ranges from 0.85 (XS) to 1.71 (XXXL) relative to the base value. @ScaledMetric uses exactly this scale, so a 12 pt padding value could become ~20 pt at maximum accessibility size.
| Content Size Category | Factor (body) | Example @ScaledMetric(12) |
|---|---|---|
| extraSmall | 0.85 | ~10 pt |
| small | 0.93 | ~11 pt |
| medium (default) | 1.00 | 12 pt |
| large | 1.07 | ~13 pt |
| extraLarge | 1.15 | ~14 pt |
| extraExtraLarge | 1.28 | ~15 pt |
| accessibilityExtraLarge | 1.47 | ~18 pt |
| accessibilityXXXL | 1.71 | ~20 pt |
Choosing relativeTo: use .body for values related to body text (padding, spacing in lists), .title for large elements (iconSize, imageSize), .caption for small elements (badge size). This ensures elements scale in sync with the surrounding text.
Dynamic Type is an iOS feature that lets users adjust the system font size in Settings → Display & Brightness → Text Size. The change applies globally to all apps. @ScaledMetric reacts to this change automatically: SwiftUI updates all @ScaledMetric variables when UIContentSizeCategory changes.
Important: @ScaledMetric only scales numeric values, it does not manage fonts directly. For fonts, use .font() with a text style (.body, .title, .headline) — SwiftUI automatically scales the font. @ScaledMetric complements font scaling for padding, spacing, and element sizes.
Canvas Preview supports Dynamic Type: the Canvas toolbar has a Text Size slider (A–A) to test the UI at different font sizes. Use it with @ScaledMetric to verify that spacing and sizes scale correctly.
struct CardView: View {
@ScaledMetric private var cornerRadius: CGFloat = 16
@ScaledMetric private var spacing: CGFloat = 8
var body: some View {
VStack(spacing: spacing) {
Text("Card Title")
.font(.headline)
Text("Description with dynamic type support")
.font(.body)
}
.padding(spacing * 2)
.background(.regularMaterial)
.cornerRadius(cornerRadius)
}
}
struct CardView_Previews: PreviewProvider {
static var previews: some View {
CardView()
.dynamicTypeSize(.large)
.previewDisplayName("Large")
CardView()
.dynamicTypeSize(.accessibility5)
.previewDisplayName("Accessibility 5")
}
}
cornerRadius scales from 16 pt to ~27 pt at maximum accessibility size. spacing — from 8 to ~14 pt. This ensures the card remains visually balanced at any font size.
Example: Dynamic Type supported icon. Image(systemName:) icon sizes do not scale to Dynamic Type by default. @ScaledMetric solves this problem by changing the imageScale or frame size based on the current scale factor.
struct IconLabel: View {
let title: String
let icon: String
@ScaledMetric private var iconDimension: CGFloat = 28
@ScaledMetric(relativeTo: .body) private var spacing: CGFloat = 6
var body: some View {
HStack(spacing: spacing) {
Image(systemName: icon)
.resizable()
.frame(width: iconDimension, height: iconDimension)
Text(title)
.font(.body)
}
}
}
Example: Accessible badge component. A numbered badge should scale proportionally to the text. @ScaledMetric for the minimum badge size ensures the circular badge remains visible with large text.
struct BadgeView: View {
let count: Int
@ScaledMetric(relativeTo: .caption) private var badgeSize: CGFloat = 20
@ScaledMetric(relativeTo: .caption) private var fontScale: CGFloat = 1
var body: some View {
ZStack {
Circle()
.fill(.red)
.frame(width: badgeSize, height: badgeSize)
Text("\(count)")
.font(.caption)
.foregroundColor(.white)
.scaleEffect(fontScale)
}
.fixedSize()
}
}
fontScale additionally scales the Circle content to match the enlarged badgeSize. Without fontScale, the text inside the badge might not fit with large text.
@ScaledMetric and @State are both property wrappers that track changes, but with different update sources. @State updates the value on programmatic change (via $stateBinding). @ScaledMetric updates the value automatically when system Dynamic Type changes, but does not allow changing the value directly from code.
Key difference: @ScaledMetric is read-only for the developer and write-only for the system. You cannot change the scaledValue via a setter — it is computed by SwiftUI based on the base value and current Dynamic Type. @State, on the other hand, is fully developer-controlled. If you need a value that both scales to Dynamic Type and changes programmatically — combine @ScaledMetric with @State or use a computed property.
| Characteristic | @ScaledMetric | @State |
|---|---|---|
| Update source | Dynamic Type (system) | Programmatic (developer) |
| Value type | CGFloat, Int, Double | Any |
| Code change | Not allowed | Allowed via binding |
| View redraw | On Dynamic Type change | On value change |
| iOS version | iOS 14+ | iOS 13+ |
Combined pattern: if you need to change padding programmatically (e.g., tap animation) while also scaling to Dynamic Type, create an @ScaledMetric for the base scaled value and an @State for the animation multiplier. Final value = scaledValue × animationMultiplier.
Mistake 1: Using @ScaledMetric for fonts. @ScaledMetric scales numbers, not fonts. For fonts, use .font(.body) — SwiftUI automatically applies Dynamic Type. Never use @ScaledMetric with font(.system(size: scaledSize)) — it breaks system accessibility.
Mistake 2: Missing relativeTo for heterogeneous elements. If you have padding (tied to .body) and iconSize (tied to .title), specify the correct relativeTo for each. Without relativeTo, both will scale by .body, resulting in disproportionate icon scaling relative to its text context.
Mistake 3: @ScaledMetric in ViewModel/@ObservableObject. @ScaledMetric is a SwiftUI property wrapper that only works inside a View. It cannot be used in ViewModels or services. For scaling in ViewModel, pass the scaled value from the View as a parameter or use @Environment(\.sizeCategory) in the View.
@Environment(\.sizeCategory) is an alternative way to get the current Dynamic Type in a View. Use it when you need more control: compute a custom multiplier, pass the sizeCategory to a ViewModel, or combine it with @ScaledMetric for flexible scaling.
struct CustomScaledView: View {
@Environment(\.sizeCategory) private var sizeCategory
@ScaledMetric private var basePadding: CGFloat = 12
private var extraPadding: CGFloat {
if sizeCategory >= .accessibilityLarge {
return basePadding * 0.5
}
return 0
}
var body: some View {
Text("Custom scaled content")
.font(.body)
.padding(basePadding + extraPadding)
}
}
Extra padding extraPadding is added only at accessibility sizes, providing more breathing room for large text without changing the base @ScaledMetric logic.
Frequently asked questions
@ScaledMetric is an official SwiftUI property wrapper for scaling numbers. @ScaledFont does not exist as a standard API — it is a custom wrapper implemented by the community. For fonts, always use the built-in .font() with text styles (.body, .title), and @ScaledMetric for padding, spacing and sizes.
@ScaledMetric is available on iOS 14+, watchOS 7+, tvOS 14+ and macOS 11+. On watchOS, Dynamic Type is limited to a smaller range — sizes from .extraSmall to .extraLarge are available without accessibility sizes. On tvOS, Dynamic Type is not available — @ScaledMetric always returns the base value.
Yes, to test @ScaledMetric create a View with @ScaledMetric and pass the environment .sizeCategory value via .environment(\.sizeCategory, .extraExtraLarge). Then get the element size via GeometryReader or SwiftUI Inspector. Alternatively, test scaling logic through UIFontMetrics in a separate module.
.dynamicTypeSize is a View modifier that limits the maximum Dynamic Type for a hierarchy (e.g., .dynamicTypeSize(...large)). @ScaledMetric respects this limit: if .dynamicTypeSize is set, the scaled value will not exceed the corresponding size. Combine both APIs for precise control.
Make sure the View uses @ScaledMetric internally (not in a ViewModel). Check that the View subscribes to Dynamic Type: @ScaledMetric automatically triggers body refresh, but if the View uses .equatable() or .id(), the mechanism may break. Use @Environment(\.sizeCategory) as a fallback.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also