@ScaledMetric — ما هو، property wrapper في SwiftUI و Dynamic Type

المؤلف: IT Sectr نُشر: 2026-06-27 وقت القراءة: 10 دق

@ScaledMetric هو property wrapper في SwiftUI يقوم تلقائياً بقياس قيمة رقمية وفقاً لإعدادات Dynamic Type للمستخدم. يتم تغليف القيمة في @ScaledMetric وإعادة حسابها عند تغيير حجم خط النظام، مما يضمن إمكانية الوصول للواجهة للأشخاص ذوي الإعاقات البصرية. وفقاً لـ Apple Developer Documentation (2026)، يستخدم @ScaledMetric مقياس UIFontMetrics لحساب المقياس النسبي بناءً على preferred content size category. اقرأ المزيد عن إمكانية الوصول في مقال إمكانية الوصول في SwiftUI.

الملامح الرئيسية

  • @ScaledMetric — property wrapper في SwiftUI لقياس القيم حسب Dynamic Type.
  • Dynamic Type — إعداد نظام iOS يغير حجم الخط من UIFontTextStyle.
  • القياس — @ScaledMetric يأخذ قيمة أساسية ومضاعف relativeTo.
  • التحديث التلقائي — عند تغيير Dynamic Type، يعيد @ScaledMetric الحساب ويتم تحديث الواجهة.
  • إمكانية الوصول — استخدام @ScaledMetric يحسن إمكانية الوصول للواجهة دون كود إضافي.

ما هو @ScaledMetric؟

@ScaledMetric هو property wrapper في SwiftUI، أُضيف في iOS 14، يقوم تلقائياً بقياس قيمة رقمية (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("Submit", systemImage: "checkmark.circle.fill")
            .font(.body)
            .padding(padding)
            .imageScale(.init(rawValue: iconSize / 24) ?? .medium)
    }
}

padding سوف يُقاس بالنسبة لـ .body (الافتراضي)، iconSize بالنسبة لـ .title. مع النص الأكبر، ستزداد المسافات والأيقونات بشكل متناسب. بدون @ScaledMetric، سيبقى padding عند 12 pt بغض النظر عن حجم الخط، مما يسبب خللاً بصرياً.

كيف يعمل @ScaledMetric

آلية @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.

مقياس Dynamic Type

مقياس 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)
extraSmall0.85~10 pt
small0.93~11 pt
medium (default)1.0012 pt
large1.07~13 pt
extraLarge1.15~14 pt
extraExtraLarge1.28~15 pt
accessibilityExtraLarge1.47~18 pt
accessibilityXXXL1.71~20 pt

اختيار relativeTo: استخدم .body للقيم المرتبطة بنص الجسم (padding، spacing في القوائم)، .title للعناصر الكبيرة (iconSize، imageSize)، .caption للعناصر الصغيرة (حجم badge). هذا يضمن أن العناصر تُقاس بشكل متزامن مع النص المحيط.

@ScaledMetric و Dynamic Type

Dynamic Type هي ميزة iOS تتيح للمستخدمين ضبط حجم خط النظام في الإعدادات → العرض والإضاءة → حجم النص. ينطبق التغيير عالمياً على جميع التطبيقات. يتفاعل @ScaledMetric مع هذا التغيير تلقائياً: يقوم SwiftUI بتحديث جميع متغيرات @ScaledMetric عند تغيير UIContentSizeCategory.

مهم: @ScaledMetric يقيس القيم الرقمية فقط، ولا يدير الخطوط مباشرة. للخطوط، استخدم .font() مع نمط نص (.body، .title، .headline) — يقوم SwiftUI تلقائياً بقياس الخط. @ScaledMetric يكمل قياس الخطوط للـ padding و spacing وأحجام العناصر.

اختبار إمكانية الوصول عبر Canvas

Canvas Preview يدعم Dynamic Type: في شريط أدوات Canvas يوجد منزلق حجم النص (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("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. هذا يضمن بقاء البطاقة متوازنة بصرياً في أي حجم خط.

أمثلة @ScaledMetric

مثال: أيقونة تدعم Dynamic Type. أحجام أيقونات Image(systemName:) لا تُقاس مع Dynamic Type افتراضياً. يحل @ScaledMetric هذه المشكلة بتغيير imageScale أو حجم الإطار بناءً على عامل القياس الحالي.

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 المرقّم بشكل متناسب مع النص. يضمن @ScaledMetric للحجم الأدنى للـ badge بقاء الـ 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 مقابل @State — الفروق

@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عند تغيير القيمة
إصدار iOSiOS 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: @ScaledMetric في ViewModel/@ObservableObject. @ScaledMetric هو property wrapper في SwiftUI يعمل فقط داخل View. لا يمكن استخدامه في ViewModels أو الخدمات. للقياس في ViewModel، مرر القيمة المقاسة من View كمعامل أو استخدم @Environment(\.sizeCategory) في View.

الحصول على فئة الحجم الحالي في الكود

@Environment(\.sizeCategory) هي طريقة بديلة للحصول على Dynamic Type الحالي في View. استخدمها عندما تحتاج إلى تحكم أكبر: حساب مضاعف مخصص، تمرير 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("Custom scaled content")
            .font(.body)
            .padding(basePadding + extraPadding)
    }
}

حشوة إضافية extraPadding تُضاف فقط في أحجام إمكانية الوصول، مما يعطي مساحة أكبر للنص الكبير دون تغيير منطق @ScaledMetric الأساسي.

الأسئلة الشائعة

كيف يختلف @ScaledMetric عن @ScaledFont؟

@ScaledMetric هو property wrapper رسمي من SwiftUI لقياس الأرقام. @ScaledFont غير موجود كـ API قياسي — إنه wrapper مخصص تم تنفيذه بواسطة المجتمع. للخطوط، استخدم دائماً .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 أنشئ View مع @ScaledMetric ومرر قيمة البيئة .sizeCategory عبر .environment(\.sizeCategory, .extraExtraLarge). ثم احصل على حجم العنصر عبر GeometryReader أو SwiftUI Inspector. بدلاً من ذلك، اختبر منطق القياس عبر UIFontMetrics في وحدة منفصلة.

كيف يتفاعل @ScaledMetric مع .dynamicTypeSize؟

.dynamicTypeSize هو معدّل View يحدد Dynamic Type الأقصى لتسلسل هرمي (مثل، .dynamicTypeSize(...large)). @ScaledMetric يحترم هذا الحد: إذا تم تعيين .dynamicTypeSize، فلن تتجاوز القيمة المقاسة الحجم المقابل. ادمج كلتا APIs للتحكم الدقيق.

ماذا تفعل إذا لم يقم @ScaledMetric بتحديث الواجهة؟

تأكد من أن View تستخدم @ScaledMetric داخلياً (وليس في ViewModel). تحقق من أن View مشتركة في Dynamic Type: @ScaledMetric يقوم تلقائياً بتحديث body، لكن إذا كانت View تستخدم .equatable() أو .id()، فقد تتعطل الآلية. استخدم @Environment(\.sizeCategory) كخطة بديلة.

الخلاصة

  • @ScaledMetric — property wrapper في SwiftUI للقياس التلقائي للأرقام حسب Dynamic Type.
  • الارتباط — relativeTo يربط المقياس بنمط نص محدد (body، title، caption).
  • إمكانية الوصول — @ScaledMetric يحسن إمكانية الوصول للواجهة بدون كود يدوي.
  • أرقام فقط — الـ wrapper يقيس CGFloat، Int، Double، وليس الخطوط.
  • النطاق — من 0.85 (XS) إلى 1.71 (XXXL) بالنسبة للقيمة الأساسية.
  • للقراءة فقط — لا يمكن تغيير @ScaledMetric من الكود، فقط عبر النظام.

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا