@ScaledMetric هو property wrapper في SwiftUI يقوم تلقائياً بقياس قيمة رقمية وفقاً لإعدادات Dynamic Type للمستخدم. يتم تغليف القيمة في @ScaledMetric وإعادة حسابها عند تغيير حجم خط النظام، مما يضمن إمكانية الوصول للواجهة للأشخاص ذوي الإعاقات البصرية. وفقاً لـ Apple Developer Documentation (2026)، يستخدم @ScaledMetric مقياس UIFontMetrics لحساب المقياس النسبي بناءً على preferred content size category. اقرأ المزيد عن إمكانية الوصول في مقال إمكانية الوصول في SwiftUI.
الملامح الرئيسية
@ScaledMetric هو property wrapper في SwiftUI، أُضيف في iOS 14، يقوم تلقائياً بقياس قيمة رقمية (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 عند 12 pt بغض النظر عن حجم الخط، مما يسبب خللاً بصرياً.
آلية @ScaledMetric تعتمد على UIFontMetrics من UIKit. عندما ينشئ SwiftUI مثيلاً لـ @ScaledMetric، يقوم بحساب مضاعف بناءً على preferred content size category الحالية (UIContentSizeCategory). يتم ضرب القيمة الأساسية في scaledValue من UIFontMetrics لنمط النص المحدد.
رياضياً: ScaledMetricValue = baseValue × UIFontMetrics.scaledValue(for: relativeTo). إذا لم يتم تحديد relativeTo، يُستخدم UIFontMetrics.default المرتبط بـ .body. عند تغيير Dynamic Type، يعيد SwiftUI إنشاء جسم View، ويحسب @ScaledMetric scaledValue جديدة، ويتم تحديث الواجهة تلقائياً عبر آلية PropertyWrappers الشبيهة بـ @State.
مقياس iOS يشمل 11 حجماً: من .extraSmall (5 pt) إلى .accessibilityExtraExtraExtraLarge (77 pt لـ .body). يتراوح عامل القياس لـ .body من 0.85 (XS) إلى 1.71 (XXXL) بالنسبة للقيمة الأساسية. يستخدم @ScaledMetric هذا المقياس بالضبط، لذلك يمكن أن تصبح قيمة padding البالغة 12 pt حوالي ~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: استخدم .body للقيم المرتبطة بنص الجسم (padding، spacing في القوائم)، .title للعناصر الكبيرة (iconSize، imageSize)، .caption للعناصر الصغيرة (حجم badge). هذا يضمن أن العناصر تُقاس بشكل متزامن مع النص المحيط.
Dynamic Type هي ميزة iOS تتيح للمستخدمين ضبط حجم خط النظام في الإعدادات → العرض والإضاءة → حجم النص. ينطبق التغيير عالمياً على جميع التطبيقات. يتفاعل @ScaledMetric مع هذا التغيير تلقائياً: يقوم SwiftUI بتحديث جميع متغيرات @ScaledMetric عند تغيير UIContentSizeCategory.
مهم: @ScaledMetric يقيس القيم الرقمية فقط، ولا يدير الخطوط مباشرة. للخطوط، استخدم .font() مع نمط نص (.body، .title، .headline) — يقوم SwiftUI تلقائياً بقياس الخط. @ScaledMetric يكمل قياس الخطوط للـ padding و spacing وأحجام العناصر.
Canvas Preview يدعم Dynamic Type: في شريط أدوات Canvas يوجد منزلق حجم النص (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)
}
}
}
مثال: مكون badge قابل للوصول. يجب أن يُقاس badge المرقّم بشكل متناسب مع النص. يضمن @ScaledMetric للحجم الأدنى للـ badge بقاء الـ 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 كلاهما property wrappers يتتبعان التغييرات، لكن بمصادر تحديث مختلفة. @State يُحدّث القيمة عند التغيير البرمجي (عبر $stateBinding). @ScaledMetric يُحدّث القيمة تلقائياً عند تغيير Dynamic Type النظام، لكنه لا يسمح بتغيير القيمة مباشرة من الكود.
الفرق الرئيسي: @ScaledMetric هو للقراءة فقط للمطور وللكتابة فقط للنظام. لا يمكنك تغيير scaledValue عبر setter — يتم حسابه بواسطة 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: @ScaledMetric في ViewModel/@ObservableObject. @ScaledMetric هو property wrapper في SwiftUI يعمل فقط داخل View. لا يمكن استخدامه في ViewModels أو الخدمات. للقياس في ViewModel، مرر القيمة المقاسة من View كمعامل أو استخدم @Environment(\.sizeCategory) في View.
@Environment(\.sizeCategory) هي طريقة بديلة للحصول على Dynamic Type الحالي في View. استخدمها عندما تحتاج إلى تحكم أكبر: حساب مضاعف مخصص، تمرير 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)
}
}
حشوة إضافية extraPadding تُضاف فقط في أحجام إمكانية الوصول، مما يعطي مساحة أكبر للنص الكبير دون تغيير منطق @ScaledMetric الأساسي.
الأسئلة الشائعة
@ScaledMetric هو property wrapper رسمي من SwiftUI لقياس الأرقام. @ScaledFont غير موجود كـ API قياسي — إنه wrapper مخصص تم تنفيذه بواسطة المجتمع. للخطوط، استخدم دائماً .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 أنشئ View مع @ScaledMetric ومرر قيمة البيئة .sizeCategory عبر .environment(\.sizeCategory, .extraExtraLarge). ثم احصل على حجم العنصر عبر GeometryReader أو SwiftUI Inspector. بدلاً من ذلك، اختبر منطق القياس عبر UIFontMetrics في وحدة منفصلة.
.dynamicTypeSize هو معدّل View يحدد Dynamic Type الأقصى لتسلسل هرمي (مثل، .dynamicTypeSize(...large)). @ScaledMetric يحترم هذا الحد: إذا تم تعيين .dynamicTypeSize، فلن تتجاوز القيمة المقاسة الحجم المقابل. ادمج كلتا APIs للتحكم الدقيق.
تأكد من أن View تستخدم @ScaledMetric داخلياً (وليس في ViewModel). تحقق من أن View مشتركة في Dynamic Type: @ScaledMetric يقوم تلقائياً بتحديث body، لكن إذا كانت View تستخدم .equatable() أو .id()، فقد تتعطل الآلية. استخدم @Environment(\.sizeCategory) كخطة بديلة.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.