@ScaledMetric — 是什么,SwiftUI 属性包装器与 Dynamic Type

作者: IT Sectr 发布日期: 2026-06-27 阅读时间: 10 分钟

@ScaledMetric — SwiftUI 属性包装器,可根据用户的 Dynamic Type 设置自动缩放数值。该值被包装在 @ScaledMetric 中,并在系统字体大小改变时重新计算,从而确保界面对于视障人士的可访问性。根据 Apple Developer Documentation(2026),@ScaledMetric 使用 UIFontMetrics 标度基于 preferred content size category 计算相对比例。有关可访问性的更多信息,请阅读 SwiftUI 可访问性专题

要点

  • @ScaledMetric — 用于在 Dynamic Type 下缩放值的 SwiftUI 属性包装器。
  • Dynamic Type — iOS 系统设置,通过 UIFontTextStyle 更改字体大小。
  • 缩放 — @ScaledMetric 接受基础值和 relativeTo 乘数。
  • 自动更新 — Dynamic Type 改变时,@ScaledMetric 重新计算,界面自动更新。
  • Accessibility — 使用 @ScaledMetric 无需额外代码即可改善界面可访问性。

什么是 @ScaledMetric?

@ScaledMetric — 在 iOS 14 中引入的 SwiftUI 属性包装器,可自动将数值(CGFloat、Int、Double)缩放到当前的 Dynamic Type 字体大小。与用于字体的 .font(.body) 不同,@ScaledMetric 可缩放任何数值参数:padding、spacing、cornerRadius、iconSize — 所有在大文本时应按比例增加的元素。

@ScaledMetric 的主要任务是为界面的非文本元素提供可访问性缩放。当用户在 iOS 设置中增大字体时,按钮、图标和间距应按比例增加,以保持界面平衡。@ScaledMetric 自动完成此任务,无需手动计算乘数。

@ScaledMetric 语法

基本语法 @ScaledMetric 使用默认值和可选参数 relativeTo。如果指定了 relativeTo,则缩放绑定到特定的文本样式(UIFontTextStyle)。如果未指定 — 则使用 .body 标度。

swift
struct AccessibleButton: View {
    @ScaledMetric private var padding: CGFloat = 12
    @ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = 24
    
    var body: some View {
        Label("提交", systemImage: "checkmark.circle.fill")
            .font(.body)
            .padding(padding)
            .imageScale(.init(rawValue: iconSize / 24) ?? .medium)
    }
}

padding 将相对于 .body(默认)缩放,iconSize — 相对于 .title。在大文本时,间距和图标将按比例增加。如果没有 @ScaledMetric,间距在任何字体大小下都将保持 12 pt,从而导致视觉不平衡。

@ScaledMetric 的工作原理

@ScaledMetric 机制 基于 UIKit 中的 UIFontMetrics。当 SwiftUI 创建 @ScaledMetric 实例时,它会根据当前的 preferred content size category(UIContentSizeCategory)计算乘数。基础值乘以指定文本样式的 UIFontMetrics 的 scaledValue。

数学上:ScaledMetricValue = baseValue × UIFontMetrics.scaledValue(for: relativeTo)。如果未指定 relativeTo,则使用绑定到 .body 的 UIFontMetrics.default。当 Dynamic Type 改变时,SwiftUI 重新创建 View 的主体,@ScaledMetric 计算新的 scaledValue,界面通过类似 @State 的 PropertyWrappers 机制自动更新。

Dynamic Type 缩放标度

iOS 标度 包含 11 种大小:从 .extraSmall(5 pt)到 .accessibilityExtraExtraExtraLarge(.body 为 77 pt)。.body 的缩放系数在 0.85(XS)到 1.71(XXXL)之间变化(相对于基础值)。@ScaledMetric 正是使用此标度,因此 padding 的 12 pt 值在最大可访问性大小下可能变为约 20 pt。

Content Size Category系数(body)@ScaledMetric(12) 示例
extraSmall0.85~10 pt
small0.93~11 pt
medium(默认)1.0012 pt
large1.07~13 pt
extraLarge1.15~14 pt
extraExtraLarge1.28~15 pt
accessibilityExtraLarge1.47~18 pt
accessibilityXXXL1.71~20 pt

选择 relativeTo:对于与主文本相关的值(列表中的 padding、spacing)使用 .body,对于大元素(iconSize、imageSize)使用 .title,对于小元素(badge 大小)使用 .caption。这可确保元素与周围文本协调缩放。

@ScaledMetric 与 Dynamic Type

Dynamic Type — iOS 功能,允许用户在 Settings → Display & Brightness → Text Size 中调整系统字体大小。更改全局应用于所有应用程序。@ScaledMetric 自动响应此更改:当 UIContentSizeCategory 改变时,SwiftUI 更新所有 @ScaledMetric 变量。

重要提示:@ScaledMetric 仅缩放数值,不直接管理字体。对于字体,请使用 .font() 配合文本样式(.body、.title、.headline)— SwiftUI 自动缩放字体。@ScaledMetric 补充字体缩放,用于 padding、spacing 和元素大小。

通过 Canvas 检查可访问性

Canvas Preview 支持 Dynamic Type:在 Canvas 工具栏中有一个 Text Size(A–A)滑块,用于在不同字体大小下检查界面。与 @ScaledMetric 一起使用,以确保间距和大小的正确缩放。

swift
struct CardView: View {
    @ScaledMetric private var cornerRadius: CGFloat = 16
    @ScaledMetric private var spacing: CGFloat = 8
    
    var body: some View {
        VStack(spacing: spacing) {
            Text("卡片标题")
                .font(.headline)
            Text("支持动态类型的描述")
                .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 从 16 pt 缩放到最大可访问性大小下的约 27 pt。spacing — 从 8 到约 14 pt。这可确保卡片在任何字体大小下保持视觉平衡。

@ScaledMetric 示例

示例:支持 Dynamic Type 的图标。Image(systemName:) 图标的大小默认不在 Dynamic Type 下缩放。@ScaledMetric 解决此问题:根据当前缩放因子更改 imageScale 或 frame 大小。

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)
        }
    }
}

示例:可访问的 badge 组件。带有数字的 Badge 应按比例随文本缩放。用于 badge 最小尺寸的 @ScaledMetric 确保圆形 badge 在大字体下保持可见。

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 还缩放 Circle 的内容以匹配增大后的 badgeSize。如果没有 fontScale,badge 中的文本在大字体下可能无法容纳。

@ScaledMetric vs @State — 区别

@ScaledMetric@State — 都是跟踪变化的属性包装器,但具有不同的更新源。@State 在程序更改时(通过 $stateBinding)更新值。@ScaledMetric 在系统 Dynamic Type 改变时自动更新值,但不允许从代码直接更改值。

关键区别:@ScaledMetric — 对开发人员只读,对系统只写。您不能通过 setter 更改 scaledValue — 它由 SwiftUI 根据基础值和当前 Dynamic Type 计算得出。而 @State 完全由开发人员控制。如果您需要一个既在 Dynamic Type 下缩放又可程序更改的值 — 将 @ScaledMetric 与 @State 结合使用,或使用计算属性。

特性@ScaledMetric@State
更新源Dynamic Type(系统)程序(开发人员)
值类型CGFloat、Int、Double任意
从代码更改不能可通过 binding
View 重绘Dynamic Type 改变时值改变时
iOS 版本iOS 14+iOS 13+

组合模式:如果需要程序更改 padding(例如点击动画)并同时在 Dynamic Type 下缩放,为基础缩放值创建 @ScaledMetric,为动画乘数创建 @State。最终值 = scaledValue × animationMultiplier。

@ScaledMetric 的常见错误

错误 1:将 @ScaledMetric 用于字体。@ScaledMetric 缩放数字,而不是字体。对于字体,请使用 .font(.body) — SwiftUI 自动应用 Dynamic Type。切勿将 @ScaledMetric 与 font(.system(size: scaledSize)) 一起使用 — 这会破坏系统的可访问性。

错误 2:不同元素缺少 relativeTo。如果有 padding(关联 .body)和 iconSize(关联 .title),请为每个指定正确的 relativeTo。没有 relativeTo,两者都将按 .body 缩放,导致图标相对于其文本上下文不成比例地增大。

错误 3:在 ViewModel/@ObservableObject 中使用 @ScaledMetric。@ScaledMetric 是仅在 View 内部工作的 SwiftUI 属性包装器。不能在 ViewModel 或服务中使用。要在 ViewModel 中进行缩放,从 View 传递缩放值作为参数,或在 View 中使用 @Environment(\.sizeCategory)。

在代码中获取当前 sizeCategory

@Environment(\.sizeCategory) — 在 View 中获取当前 Dynamic Type 的替代方法。当需要更多控制时使用:计算自定义乘数、将 sizeCategory 传递给 ViewModel 或与 @ScaledMetric 结合实现灵活缩放。

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("自定义缩放内容")
            .font(.body)
            .padding(basePadding + extraPadding)
    }
}

额外间距 extraPadding 仅在可访问性大小下添加,为大型文本提供更多空间,而不改变基础 @ScaledMetric 逻辑。

常见问题

@ScaledMetric 与 @ScaledFont 有何不同?

@ScaledMetric — 用于缩放数字的官方 SwiftUI 属性包装器。@ScaledFont 不作为标准 API 存在 — 它是社区实现的自定义包装器。对于字体,始终使用内置的 .font() 配合文本样式(.body、.title),而 @ScaledMetric 用于 padding、spacing 和大小。

@ScaledMetric 在 watchOS 和 tvOS 上有效吗?

@ScaledMetric 适用于 iOS 14+、watchOS 7+、tvOS 14+ 和 macOS 11+。在 watchOS 上,Dynamic Type 限制在较小的范围内 — 从 .extraSmall 到 .extraLarge,没有可访问性大小。在 tvOS 上,Dynamic Type 不存在 — @ScaledMetric 始终返回基础值。

可以在单元测试中测试 @ScaledMetric 吗?

可以。要测试 @ScaledMetric,创建包含 @ScaledMetric 的 View,并通过 .environment(\.sizeCategory, .extraExtraLarge) 传递 .sizeCategory 环境值。然后通过 GeometryReader 或 SwiftUI Inspector 获取元素大小。或者,在单独模块中通过 UIFontMetrics 检查缩放逻辑。

@ScaledMetric 如何与 .dynamicTypeSize 交互?

.dynamicTypeSize — 一个 View 修饰符,用于限制层次结构的最大 Dynamic Type(例如 .dynamicTypeSize(...large))。@ScaledMetric 考虑此限制:如果设置了 .dynamicTypeSize,缩放值不会超过相应大小。结合两个 API 以实现精确控制。

如果 @ScaledMetric 不更新界面怎么办?

确保 View 在其内部使用 @ScaledMetric(而不是在 ViewModel 中)。检查 View 是否订阅了 Dynamic Type:@ScaledMetric 自动触发 body 刷新,但如果 View 使用 .equatable() 或 .id(),该机制可能会失效。使用 @Environment(\.sizeCategory) 作为后备方案。

总结

  • @ScaledMetric — 用于在 Dynamic Type 下自动缩放数字的 SwiftUI 属性包装器。
  • 绑定 — relativeTo 将标度绑定到特定的文本样式(body、title、caption)。
  • Accessibility — @ScaledMetric 无需手动编码即可改善界面可访问性。
  • 仅数字 — 包装器缩放 CGFloat、Int、Double,但不缩放字体。
  • 范围 — 从 0.85(XS)到 1.71(XXXL),相对于基础值。
  • 只读 — @ScaledMetric 不能从代码更改,只能通过系统。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读