@ScaledMetric là một property wrapper SwiftUI tự động chia tỷ lệ giá trị số theo cài đặt Dynamic Type của người dùng. Giá trị được bọc trong @ScaledMetric và được tính lại khi kích thước phông hệ thống thay đổi, đảm bảo khả năng tiếp cận giao diện cho người khiếm thị. Theo Tài liệu dành cho nhà phát triển Apple (2026), @ScaledMetric sử dụng thang đo UIFontMetrics để tính toán tỷ lệ tương đối dựa trên danh mục kích thước nội dung ưu tiên. Tìm hiểu thêm về khả năng tiếp cận trong bài viết về khả năng tiếp cận SwiftUI.
Các điểm chính
@ScaledMetric là một property wrapper SwiftUI, được thêm vào iOS 14, tự động chia tỷ lệ giá trị số (CGFloat, Int, Double) theo kích thước phông Dynamic Type hiện tại. Không giống .font(.body) cho phông chữ, @ScaledMetric chia tỷ lệ bất kỳ tham số số nào: padding, spacing, cornerRadius, iconSize — mọi thứ cần tăng tỷ lệ thuận với văn bản lớn hơn.
Mục đích chính của @ScaledMetric là cung cấp khả năng chia tỷ lệ tiếp cận cho các yếu tố giao diện không phải văn bản. Khi người dùng tăng kích thước phông trong cài đặt iOS, các nút, biểu tượng và khoảng cách phải chia tỷ lệ tương ứng để giữ giao diện cân bằng. @ScaledMetric xử lý điều này tự động mà không cần tính toán hệ số thủ công.
Cú pháp cơ bản của @ScaledMetric sử dụng giá trị mặc định và tham số tùy chọn relativeTo. Nếu relativeTo được chỉ định, việc chia tỷ lệ được gắn với một kiểu văn bản cụ thể (UIFontTextStyle). Nếu không, thang đo .body được sử dụng.
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 sẽ được chia tỷ lệ theo .body (mặc định), iconSize theo .title. Với văn bản lớn hơn, khoảng cách và biểu tượng sẽ tăng tỷ lệ. Nếu không có @ScaledMetric, padding sẽ giữ nguyên 12 pt bất kể kích thước phông, gây mất cân bằng thị giác.
Cơ chế @ScaledMetric dựa trên UIFontMetrics từ UIKit. Khi SwiftUI tạo một thể hiện @ScaledMetric, nó tính toán hệ số dựa trên danh mục kích thước nội dung ưu tiên hiện tại (UIContentSizeCategory). Giá trị cơ sở được nhân với scaledValue từ UIFontMetrics cho kiểu văn bản đã chỉ định.
Toán học: ScaledMetricValue = baseValue × UIFontMetrics.scaledValue(for: relativeTo). Nếu relativeTo không được chỉ định, UIFontMetrics.default được sử dụng, gắn với .body. Khi Dynamic Type thay đổi, SwiftUI tạo lại phần thân View, @ScaledMetric tính scaledValue mới và giao diện được cập nhật tự động thông qua cơ chế PropertyWrappers giống @State.
Thang đo iOS bao gồm 11 kích thước: từ .extraSmall (5 pt) đến .accessibilityExtraExtraExtraLarge (77 pt cho .body). Hệ số chia tỷ lệ cho .body dao động từ 0.85 (XS) đến 1.71 (XXXL) so với giá trị cơ sở. @ScaledMetric sử dụng chính xác thang đo này, vì vậy giá trị padding 12 pt có thể trở thành ~20 pt ở kích thước tiếp cận tối đa.
| Danh mục kích thước nội dung | Hệ số (body) | Ví dụ @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 |
Chọn relativeTo: sử dụng .body cho các giá trị liên quan đến văn bản nội dung (padding, spacing trong danh sách), .title cho các yếu tố lớn (iconSize, imageSize), .caption cho các yếu tố nhỏ (kích thước huy hiệu). Điều này đảm bảo các yếu tố chia tỷ lệ đồng bộ với văn bản xung quanh.
Dynamic Type là tính năng iOS cho phép người dùng điều chỉnh kích thước phông hệ thống trong Cài đặt → Màn hình & Độ sáng → Cỡ chữ. Thay đổi áp dụng toàn cục cho tất cả ứng dụng. @ScaledMetric phản ứng với sự thay đổi này tự động: SwiftUI cập nhật tất cả các biến @ScaledMetric khi UIContentSizeCategory thay đổi.
Quan trọng: @ScaledMetric chỉ chia tỷ lệ các giá trị số, không quản lý phông chữ trực tiếp. Đối với phông chữ, hãy sử dụng .font() với kiểu văn bản (.body, .title, .headline) — SwiftUI tự động chia tỷ lệ phông. @ScaledMetric bổ sung cho việc chia tỷ lệ phông cho padding, spacing và kích thước yếu tố.
Canvas Preview hỗ trợ Dynamic Type: thanh công cụ Canvas có thanh trượt kích thước văn bản (A–A) để kiểm tra giao diện ở các kích thước phông khác nhau. Sử dụng nó với @ScaledMetric để đảm bảo khoảng cách và kích thước chia tỷ lệ chính xác.
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 chia tỷ lệ từ 16 pt lên ~27 pt ở kích thước tiếp cận tối đa. spacing — từ 8 đến ~14 pt. Điều này đảm bảo thẻ luôn cân bằng trực quan ở mọi kích thước phông.
Ví dụ: Biểu tượng hỗ trợ Dynamic Type. Kích thước biểu tượng Image(systemName:) không chia tỷ lệ theo Dynamic Type theo mặc định. @ScaledMetric giải quyết vấn đề này bằng cách thay đổi imageScale hoặc kích thước khung dựa trên hệ số tỷ lệ hiện tại.
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)
}
}
}
Ví dụ: Thành phần huy hiệu có thể tiếp cận. Huy hiệu có số phải chia tỷ lệ tương ứng với văn bản. @ScaledMetric cho kích thước huy hiệu tối thiểu đảm bảo huy hiệu hình tròn vẫn hiển thị với văn bản lớn.
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 chia tỷ lệ thêm nội dung Circle để phù hợp với badgeSize đã mở rộng. Nếu không có fontScale, văn bản bên trong huy hiệu có thể không vừa với văn bản lớn.
@ScaledMetric và @State đều là property wrappers theo dõi các thay đổi, nhưng có nguồn cập nhật khác nhau. @State cập nhật giá trị khi thay đổi lập trình (qua $stateBinding). @ScaledMetric cập nhật giá trị tự động khi Dynamic Type hệ thống thay đổi, nhưng không cho phép thay đổi giá trị trực tiếp từ mã.
Sự khác biệt chính: @ScaledMetric chỉ đọc cho nhà phát triển và chỉ ghi cho hệ thống. Bạn không thể thay đổi scaledValue thông qua setter — nó được SwiftUI tính toán dựa trên giá trị cơ sở và Dynamic Type hiện tại. @State, mặt khác, hoàn toàn do nhà phát triển kiểm soát. Nếu bạn cần một giá trị vừa chia tỷ lệ theo Dynamic Type vừa thay đổi được bằng lập trình — hãy kết hợp @ScaledMetric với @State hoặc sử dụng thuộc tính được tính toán.
| Đặc điểm | @ScaledMetric | @State |
|---|---|---|
| Nguồn cập nhật | Dynamic Type (hệ thống) | Lập trình (nhà phát triển) |
| Loại giá trị | CGFloat, Int, Double | Bất kỳ |
| Thay đổi từ mã | Không được phép | Được phép qua binding |
| Vẽ lại View | Khi Dynamic Type thay đổi | Khi giá trị thay đổi |
| Phiên bản iOS | iOS 14+ | iOS 13+ |
Mẫu kết hợp: nếu bạn cần thay đổi padding bằng lập trình (ví dụ, hiệu ứng chạm) trong khi chia tỷ lệ theo Dynamic Type, hãy tạo @ScaledMetric cho giá trị chia tỷ lệ cơ sở và @State cho hệ số hiệu ứng. Giá trị cuối cùng = scaledValue × animationMultiplier.
Lỗi 1: Sử dụng @ScaledMetric cho phông chữ. @ScaledMetric chia tỷ lệ số, không phải phông chữ. Đối với phông chữ, hãy sử dụng .font(.body) — SwiftUI tự động áp dụng Dynamic Type. Không bao giờ sử dụng @ScaledMetric với font(.system(size: scaledSize)) — điều này phá vỡ khả năng tiếp cận hệ thống.
Lỗi 2: Thiếu relativeTo cho các yếu tố không đồng nhất. Nếu bạn có padding (gắn với .body) và iconSize (gắn với .title), hãy chỉ định relativeTo chính xác cho từng cái. Nếu không có relativeTo, cả hai sẽ chia tỷ lệ theo .body, dẫn đến tỷ lệ biểu tượng không tương xứng so với ngữ cảnh văn bản của nó.
Lỗi 3: @ScaledMetric trong ViewModel/@ObservableObject. @ScaledMetric là property wrapper SwiftUI chỉ hoạt động bên trong View. Nó không thể được sử dụng trong ViewModels hoặc dịch vụ. Để chia tỷ lệ trong ViewModel, hãy truyền giá trị chia tỷ lệ từ View dưới dạng tham số hoặc sử dụng @Environment(\.sizeCategory) trong View.
@Environment(\.sizeCategory) là một cách thay thế để lấy Dynamic Type hiện tại trong View. Sử dụng nó khi bạn cần kiểm soát nhiều hơn: tính hệ số tùy chỉnh, truyền sizeCategory cho ViewModel hoặc kết hợp với @ScaledMetric để chia tỷ lệ linh hoạt.
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 bổ sung extraPadding chỉ được thêm vào ở kích thước tiếp cận, cung cấp nhiều không gian hơn cho văn bản lớn mà không thay đổi logic @ScaledMetric cơ sở.
Câu hỏi thường gặp
@ScaledMetric là property wrapper chính thức của SwiftUI để chia tỷ lệ số. @ScaledFont không tồn tại như API tiêu chuẩn — nó là wrapper tùy chỉnh do cộng đồng thực hiện. Đối với phông chữ, luôn sử dụng .font() tích hợp sẵn với các kiểu văn bản (.body, .title) và @ScaledMetric cho padding, spacing và kích thước.
@ScaledMetric có sẵn trên iOS 14+, watchOS 7+, tvOS 14+ và macOS 11+. Trên watchOS, Dynamic Type bị giới hạn ở phạm vi nhỏ hơn — các kích thước từ .extraSmall đến .extraLarge không có kích thước tiếp cận. Trên tvOS, Dynamic Type không có sẵn — @ScaledMetric luôn trả về giá trị cơ sở.
Có, để kiểm thử @ScaledMetric hãy tạo View với @ScaledMetric và truyền giá trị môi trường .sizeCategory qua .environment(\.sizeCategory, .extraExtraLarge). Sau đó lấy kích thước phần tử qua GeometryReader hoặc SwiftUI Inspector. Ngoài ra, hãy kiểm thử logic chia tỷ lệ qua UIFontMetrics trong một mô-đun riêng.
.dynamicTypeSize là một công cụ sửa đổi View giới hạn Dynamic Type tối đa cho một hệ thống phân cấp (ví dụ: .dynamicTypeSize(...large)). @ScaledMetric tôn trọng giới hạn này: nếu .dynamicTypeSize được đặt, giá trị chia tỷ lệ sẽ không vượt quá kích thước tương ứng. Kết hợp cả hai API để kiểm soát chính xác.
Đảm bảo View sử dụng @ScaledMetric nội bộ (không phải trong ViewModel). Kiểm tra xem View có đăng ký Dynamic Type không: @ScaledMetric tự động kích hoạt làm mới body, nhưng nếu View sử dụng .equatable() hoặc .id(), cơ chế có thể bị hỏng. Sử dụng @Environment(\.sizeCategory) làm phương án dự phòng.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm