@ScaledMetric — what it is, SwiftUI property wrapper and Dynamic Type

Author: IT Sectr Published: 2026-06-27 Reading time: 10 min

@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 — a SwiftUI property wrapper for scaling values to Dynamic Type.
  • Dynamic Type — an iOS system setting that changes font size from UIFontTextStyle.
  • Scaling — @ScaledMetric takes a base value and a relativeTo multiplier.
  • Auto-update — when Dynamic Type changes, @ScaledMetric recalculates and the UI updates.
  • Accessibility — using @ScaledMetric improves interface accessibility without additional code.

What is @ScaledMetric?

@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.

@ScaledMetric syntax

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.

swift
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.

How @ScaledMetric works

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.

Dynamic Type scaling scale

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 CategoryFactor (body)Example @ScaledMetric(12)
extraSmall0.85~10 pt
small0.93~11 pt
medium (default)1.0012 pt
large1.07~13 pt
extraLarge1.15~14 pt
extraExtraLarge1.28~15 pt
accessibilityExtraLarge1.47~18 pt
accessibilityXXXL1.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.

@ScaledMetric and Dynamic Type

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.

Accessibility testing via Canvas

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.

swift
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.

@ScaledMetric examples

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.

swift
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.

swift
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 vs @State — differences

@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 sourceDynamic Type (system)Programmatic (developer)
Value typeCGFloat, Int, DoubleAny
Code changeNot allowedAllowed via binding
View redrawOn Dynamic Type changeOn value change
iOS versioniOS 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.

Common mistakes with @ScaledMetric

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.

Getting the current size category in code

@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.

swift
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

How is @ScaledMetric different from @ScaledFont?

@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.

Does @ScaledMetric work on watchOS and tvOS?

@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.

Can @ScaledMetric be tested in unit tests?

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.

How does @ScaledMetric interact with .dynamicTypeSize?

.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.

What if @ScaledMetric does not update the UI?

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

  • @ScaledMetric — a SwiftUI property wrapper for automatic number scaling to Dynamic Type.
  • Binding — relativeTo ties the scale to a specific text style (body, title, caption).
  • Accessibility — @ScaledMetric improves interface accessibility without manual code.
  • Numbers only — the wrapper scales CGFloat, Int, Double, but not fonts.
  • Range — from 0.85 (XS) to 1.71 (XXXL) relative to the base value.
  • Read-only — @ScaledMetric cannot be changed from code, only through the system.

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.

Discuss the project

Read also