@ScaledMetric — SwiftUI 属性包装器,可根据用户的 Dynamic Type 设置自动缩放数值。该值被包装在 @ScaledMetric 中,并在系统字体大小改变时重新计算,从而确保界面对于视障人士的可访问性。根据 Apple Developer Documentation(2026),@ScaledMetric 使用 UIFontMetrics 标度基于 preferred content size category 计算相对比例。有关可访问性的更多信息,请阅读 SwiftUI 可访问性专题。
要点
@ScaledMetric — 在 iOS 14 中引入的 SwiftUI 属性包装器,可自动将数值(CGFloat、Int、Double)缩放到当前的 Dynamic Type 字体大小。与用于字体的 .font(.body) 不同,@ScaledMetric 可缩放任何数值参数:padding、spacing、cornerRadius、iconSize — 所有在大文本时应按比例增加的元素。
@ScaledMetric 的主要任务是为界面的非文本元素提供可访问性缩放。当用户在 iOS 设置中增大字体时,按钮、图标和间距应按比例增加,以保持界面平衡。@ScaledMetric 自动完成此任务,无需手动计算乘数。
基本语法 @ScaledMetric 使用默认值和可选参数 relativeTo。如果指定了 relativeTo,则缩放绑定到特定的文本样式(UIFontTextStyle)。如果未指定 — 则使用 .body 标度。
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 机制 基于 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 机制自动更新。
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) 示例 |
|---|---|---|
| extraSmall | 0.85 | ~10 pt |
| small | 0.93 | ~11 pt |
| medium(默认) | 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 |
选择 relativeTo:对于与主文本相关的值(列表中的 padding、spacing)使用 .body,对于大元素(iconSize、imageSize)使用 .title,对于小元素(badge 大小)使用 .caption。这可确保元素与周围文本协调缩放。
Dynamic Type — iOS 功能,允许用户在 Settings → Display & Brightness → Text Size 中调整系统字体大小。更改全局应用于所有应用程序。@ScaledMetric 自动响应此更改:当 UIContentSizeCategory 改变时,SwiftUI 更新所有 @ScaledMetric 变量。
重要提示:@ScaledMetric 仅缩放数值,不直接管理字体。对于字体,请使用 .font() 配合文本样式(.body、.title、.headline)— SwiftUI 自动缩放字体。@ScaledMetric 补充字体缩放,用于 padding、spacing 和元素大小。
Canvas Preview 支持 Dynamic Type:在 Canvas 工具栏中有一个 Text Size(A–A)滑块,用于在不同字体大小下检查界面。与 @ScaledMetric 一起使用,以确保间距和大小的正确缩放。
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。这可确保卡片在任何字体大小下保持视觉平衡。
示例:支持 Dynamic Type 的图标。Image(systemName:) 图标的大小默认不在 Dynamic Type 下缩放。@ScaledMetric 解决此问题:根据当前缩放因子更改 imageScale 或 frame 大小。
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 在大字体下保持可见。
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 和 @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。
错误 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)。
@Environment(\.sizeCategory) — 在 View 中获取当前 Dynamic Type 的替代方法。当需要更多控制时使用:计算自定义乘数、将 sizeCategory 传递给 ViewModel 或与 @ScaledMetric 结合实现灵活缩放。
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 — 用于缩放数字的官方 SwiftUI 属性包装器。@ScaledFont 不作为标准 API 存在 — 它是社区实现的自定义包装器。对于字体,始终使用内置的 .font() 配合文本样式(.body、.title),而 @ScaledMetric 用于 padding、spacing 和大小。
@ScaledMetric 适用于 iOS 14+、watchOS 7+、tvOS 14+ 和 macOS 11+。在 watchOS 上,Dynamic Type 限制在较小的范围内 — 从 .extraSmall 到 .extraLarge,没有可访问性大小。在 tvOS 上,Dynamic Type 不存在 — @ScaledMetric 始终返回基础值。
可以。要测试 @ScaledMetric,创建包含 @ScaledMetric 的 View,并通过 .environment(\.sizeCategory, .extraExtraLarge) 传递 .sizeCategory 环境值。然后通过 GeometryReader 或 SwiftUI Inspector 获取元素大小。或者,在单独模块中通过 UIFontMetrics 检查缩放逻辑。
.dynamicTypeSize — 一个 View 修饰符,用于限制层次结构的最大 Dynamic Type(例如 .dynamicTypeSize(...large))。@ScaledMetric 考虑此限制:如果设置了 .dynamicTypeSize,缩放值不会超过相应大小。结合两个 API 以实现精确控制。
确保 View 在其内部使用 @ScaledMetric(而不是在 ViewModel 中)。检查 View 是否订阅了 Dynamic Type:@ScaledMetric 自动触发 body 刷新,但如果 View 使用 .equatable() 或 .id(),该机制可能会失效。使用 @Environment(\.sizeCategory) 作为后备方案。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。