The @ScaledMetric Property Wrapper

How can you scale other metrics, like spacing, as the dynamic type content size changes? In iOS 14, SwiftUI gained the @ScaledMetric property wrapper that can scale any numeric value.

This works well to scale other view metrics like margin sizes and the spacing between text:

@ScaledMetric
private var margin: CGFloat = 16

@ScaledMetric(relativeTo: .largeTitle)
private var spacing: CGFloat = 8

Let’s look at an example and then dig into how the scaling works.

ScaledMetric (iOS 14)

Here’s my SwiftUI BookView showing a title and some body text. I’m setting some spacing in the vertical stack view to separate the two text views. I’m also surrounding the stack view with some padding to create a margin:

struct BookView: View {
  let book: Book

  private let margin: CGFloat = 16
  private let spacing: CGFloat = 8
    
  var body: some View {
    ScrollView {
      VStack(alignment: .leading, spacing: spacing) {
        Text("\(book.title) by \(book.author)")
          .font(Font.system(.largeTitle, design: .rounded)
            .leading(.tight))
        
        Text(book.text)
          .font(Font.system(.body, design: .serif)
            .leading(.loose))
      }
      .padding(margin)
    }
  }
}

The title is using the .largeTitle text style and the book text is using the .body text style. Here’s how it looks at the L, XXXL, and accessibility XXXL text sizes previewed on an iPhone 12 mini:

Fixed spacing, margin and spacing do not change as dynamic type size increases

The fixed size margins and spacing makes the text look a little cramped at the larger text sizes. I would like to scale those properties in proportion to the scaling of the dynamic text size.

Scaling Metrics

Apple has made the font metrics it uses to scale dynamic type available since iOS 11. You typically use these to scale your own custom fonts but we can also scale any numeric value.

For example, to scale 8 points of body text spacing:

let bodyFontMetrics = UIFontMetrics(forTextStyle: .body)
let scaledSpacing = bodyFontMetrics.scaledValue(for: 8.0)

If I’m using UIKit, I would need to update my margin and spacing when the view loads but also when the user changes their preferred text size. My view controller would probably end up with a lot of boilerplate:

final class BookViewController: UIViewController {
  @IBOutlet private var stackView: UIStackView?
  // ...

  private enum ViewMetrics {
    static let margin: CGFloat = 16
    static let spacing: CGFloat = 8
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    updateView()
  }

  override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    if traitCollection.preferredContentSizeCategory != 
      previousTraitCollection?.preferredContentSizeCategory {
      updateView()
    }
  }

  private func updateView() {
    let bodyFontMetrics = UIFontMetrics(forTextStyle: .body)
    let scaledSpacing = bodyFontMetrics.scaledValue(for: ViewMetrics.spacing)
        
    let titleFontMetrics = UIFontMetrics(forTextStyle: .largeTitle)
    let scaledMargin = titleFontMetrics.scaledValue(for: ViewMetrics.margin)
        
    stackView?.spacing = scaledSpacing
    stackView?.directionalLayoutMargins = NSDirectionalEdgeInsets(top: scaledMargin, leading: scaledMargin, bottom: scaledMargin, trailing: scaledMargin)
  }
}

It’s perhaps no surprise that this gets more concise with SwiftUI and iOS 14. I only need to add the @ScaledMetric property wrapper to my two view properties (and change them to be var’s):

@ScaledMetric private var margin: CGFloat = 16
@ScaledMetric private var spacing: CGFloat = 8

SwiftUI takes care of updating the properties and refreshing the layout when the user changes their preferred text size. Here’s how it looks again at the L, XXXL and Accessibility XXL text sizes with scaling:

Scaling relative to body so that margin and spacing increase with dynamic type size

At the XXXL size, the 16 point margin has increased to 21 points and the 8 point spacing between the title and body text to 10.667 points. At the much larger accessibility XXXL size the margin has become 45 points and the spacing 22.667 points. That’s an improvement over the fixed margin and spacing but we can make one more slight adjustment.

Relative Scaling

If you look back at the UIKit example we scaled the margin using a font metric obtained for the .body text style. But we used a font metric for the .largeTitle style for the spacing below the large title text. How do we do that for SwiftUI with the @ScaledMetric property wrapper?

Well, it turns out the property wrapper has a parameter indicating the text style to use. It defaults to .body if you don’t specify a text style. Using .largeTitle my title view metric property becomes:

@ScaledMetric(relativeTo: .largeTitle)
private var spacing: CGFloat = 8

The effect is subtle at the smaller sizes. The 8 point spacing scales to 9.333 points at XXXL and to 13.667 at the accessibility XXXL text size. That compares to 10.667 points and 22.667 points when scaling relative to the default body text style:

Scaling relative to body and title

Scaling relative to .largeTitle gives us a smaller increase compared to scaling relative to .body. That surprised me at first as I’d make the mistake of thinking that the larger text style would have a bigger scale factor. That’s not how it works. Here’s a graph showing how my 16 point margin at the Large text size scales relative to each text style:

Graph showing how 16 points at large category size scales for each text style

That’s a little difficult to see so let me summarise the main points:

  • The body text style scales from 13.667 points at the extra small (XS) text size up to 45 points at the accessibility XXXL text size.
  • The large title text style scales from 14.667 (XS) up to 27.333 (accessibility XXXL). In general, the title text styles scale up and down the least (they’re already pretty big).
  • The headline, callout, subheadline and footnote styles closely follow the scaling of the body style. For reasons unknown, the headline scaling only differs from body at the accessibility XXL size.
  • The caption styles scale up the most. The caption 2 style reaches 59 points at the accessibility XXXL text size but never scales down below 16 points (presumably because it’s already fairly small).

Further Details