@ScaledMetricは、ユーザーのDynamic Type設定に従って数値を自動的にスケーリングするSwiftUIのproperty wrapperです。値は@ScaledMetricでラップされ、システムフォントサイズが変更されると再計算され、視覚障碍を持つ人々にとってインターフェースのアクセシビリティを保証します。Apple Developer Documentation(2026年)によると、@ScaledMetricはUIFontMetricsスケールを使用して、preferred content size categoryに基づいた相対スケールを計算します。アクセシビリティの詳細については、SwiftUIアクセシビリティの記事をご覧ください。
主なポイント
@ScaledMetricは、iOS 14で追加されたSwiftUIのproperty wrapperで、数値(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("Submit", systemImage: "checkmark.circle.fill")
.font(.body)
.padding(padding)
.imageScale(.init(rawValue: iconSize / 24) ?? .medium)
}
}
paddingは.body(デフォルト)に対して相対的にスケーリングされ、iconSizeは.titleに対して相対的にスケーリングされます。大きなテキストでは、スペーシングとアイコンが比例して拡大します。@ScaledMetricがない場合、paddingはフォントサイズに関係なく12ptに留まり、視覚的な不均衡を引き起こします。
@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のbodyを再作成し、@ScaledMetricが新しいscaledValueを計算し、UIは@StateのようなPropertyWrappersのメカニズムを通じて自動的に更新されます。
iOSの尺度には11のサイズがあります:.extraSmall(5 pt)から.accessibilityExtraExtraExtraLarge(.bodyの場合は77 pt)まで。.bodyのスケーリング係数は、基本値に対して0.85(XS)から1.71(XXXL)の範囲です。@ScaledMetricは正確にこの尺度を使用するため、12 ptのpadding値は最大アクセシビリティサイズで約20 ptになります。
| コンテンツサイズカテゴリ | 係数(body) | 例 @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 |
relativeToの選択:本文テキストに関連する値(リスト内のpadding、spacing)には.bodyを、大きな要素(iconSize、imageSize)には.titleを、小さな要素(バッジサイズ)には.captionを使用します。これにより、要素が周囲のテキストと同期してスケーリングされます。
Dynamic Typeは、ユーザーが設定 → ディスプレイと明るさ → テキストサイズでシステムフォントサイズを調整できるiOSの機能です。変更はすべてのアプリにグローバルに適用されます。@ScaledMetricはこの変更に自動的に反応します:UIContentSizeCategoryが変更されると、SwiftUIはすべての@ScaledMetric変数を更新します。
重要:@ScaledMetricは数値のみをスケーリングし、フォントを直接管理しません。フォントには、テキストスタイル(.body、.title、.headline)を指定した.font()を使用してください — SwiftUIが自動的にフォントをスケーリングします。@ScaledMetricは、padding、spacing、要素サイズのフォントスケーリングを補完します。
Canvas PreviewはDynamic Typeをサポートしています:Canvasツールバーには、異なるフォントサイズでUIをテストするためのテキストサイズスライダー(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("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は最大アクセシビリティサイズで16 ptから約27 ptにスケーリングされます。spacingは8から約14 ptになります。これにより、カードはどのフォントサイズでも視覚的にバランスが保たれます。
例:Dynamic Type対応アイコン。Image(systemName:)のアイコンサイズはデフォルトではDynamic Typeにスケーリングされません。@ScaledMetricは、現在のスケール係数に基づいてimageScaleまたはフレームサイズを変更することでこの問題を解決します。
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)
}
}
}
例:アクセシブルなバッジコンポーネント。数字付きバッジはテキストに比例してスケーリングされる必要があります。最小バッジサイズに@ScaledMetricを使用することで、丸いバッジが大きなテキストでも視認できる状態を保ちます。
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は、拡大されたbadgeSizeに合わせてCircleのコンテンツをさらにスケーリングします。fontScaleがないと、大きなテキストの場合、バッジ内のテキストが収まらない可能性があります。
@ScaledMetricと@Stateはどちらも変更を追跡するproperty wrapperですが、更新ソースが異なります。@Stateはプログラムによる変更($stateBinding経由)で値を更新します。@ScaledMetricはシステムのDynamic Typeが変更されると自動的に値を更新しますが、コードから直接値を変更することはできません。
主な違い:@ScaledMetricは開発者にとって読み取り専用で、システムにとって書き込み専用です。setterを介してscaledValueを変更することはできません — 基本値と現在のDynamic Typeに基づいてSwiftUIによって計算されます。一方、@Stateは完全に開発者が制御できます。Dynamic Typeにスケーリングされつつプログラム的にも変更される値が必要な場合は、@ScaledMetricを@Stateと組み合わせるか、計算プロパティを使用してください。
| 特性 | @ScaledMetric | @State |
|---|---|---|
| 更新ソース | Dynamic Type(システム) | プログラム(開発者) |
| 値の型 | CGFloat、Int、Double | 任意 |
| コードからの変更 | 不可 | binding経由で可能 |
| Viewの再描画 | Dynamic Type変更時 | 値変更時 |
| iOSバージョン | iOS 14+ | iOS 13+ |
組み合わせパターン:Dynamic Typeにスケーリングしつつ、プログラム的にpaddingを変更する必要がある場合(タップアニメーションなど)、基本スケール値用の@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のproperty wrapperです。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("Custom scaled content")
.font(.body)
.padding(basePadding + extraPadding)
}
}
追加のpadding extraPaddingはアクセシビリティサイズでのみ追加され、基本の@ScaledMetricロジックを変更せずに大きなテキストにより多くの余白を提供します。
よくある質問
@ScaledMetricは数値をスケーリングするための公式のSwiftUI property wrapperです。@ScaledFontは標準APIとしては存在しません — コミュニティによって実装されたカスタムwrapperです。フォントには、常にテキストスタイル(.body、.title)を指定した組み込みの.font()を使用し、padding、spacing、サイズには@ScaledMetricを使用してください。
@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は、階層の最大Dynamic Typeを制限するView修飾子です(例:.dynamicTypeSize(...large))。@ScaledMetricはこの制限を尊重します:.dynamicTypeSizeが設定されている場合、スケーリングされた値は対応するサイズを超えません。正確な制御のために両方のAPIを組み合わせてください。
Viewが内部で@ScaledMetricを使用していることを確認してください(ViewModel内ではありません)。ViewがDynamic Typeを購読していることを確認してください:@ScaledMetricは自動的にbodyのリフレッシュをトリガーしますが、Viewが.equatable()や.id()を使用している場合、メカニズムが壊れる可能性があります。フォールバックとして@Environment(\.sizeCategory)を使用してください。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。