لإظهار شريط تقدم بتدرّج SwiftUI من دون ضغط ألوانه، أبقِ التدرّج بعرض المسار الكامل، وحرّك قناعًا محاذيًا إلى الحافة الأمامية. يطبّق مثال macOS هذا السلوك باستخدام ProgressViewStyle مخصص.

المطلوب إنشاء مؤشر تقدم لسطح المكتب قابل لتغيير الحجم لنشاط اصطناعي. عند اكتمال 25%، ينبغي إظهار الربع الأول من تسلسل ألوان ثابت. وتظهر الألوان اللاحقة مع تقدم الإنجاز، بينما تظل الألوان الظاهرة في مواضعها.

لماذا تنضغط الألوان عندما يصبح شريط التقدم أقصر؟

توضح Apple أن LinearGradient يوزّع نقطتي البداية والنهاية على المستطيل المحيط بالشكل الذي يملؤه. لذلك، فإن إعطاء التدرّج عرضًا يتناسب مع التقدم يغيّر أيضًا المساحة التي تتوزع عليها ألوانه. يظل التدرّج ذو عرض ربع المسار محتويًا على تسلسل ألوانه كاملًا. LinearGradient — توثيق مطوري Apple

لإنشاء تأثير كشف تدريجي، افصل بين بُعدين: يكون عرض التدرّج هو عرض المسار الكامل، بينما يكون العرض المرئي هو عرض المسار مضروبًا في نسبة الإنجاز. هذا تصميم مستمد من سلوك التدرّج الموثّق، وليس واجهة برمجية مستقلة لشريط التقدم.

لنفترض، على سبيل التوضيح، مسارًا بطول 320 نقطة، مع نقاط توقف زرقاء وبنفسجية وبرتقالية عند المواضع المعيارية 0 و0.5 و1. تظل نقطة المنتصف البنفسجية عند 160 نقطة طوال عملية الكشف:

التقدم عرض التدرّج الكامل عرض القناع النتيجة المرئية
0% 320 نقطة 0 نقطة المسار فقط
25% 320 نقطة 80 نقطة الربع الأول من التدرّج
50% 320 نقطة 160 نقطة التدرّج حتى منتصفه البنفسجي
100% 320 نقطة 320 نقطة تسلسل الألوان كاملًا

يوفر هذا طريقة تشخيص مفيدة: عند 25%، ينبغي أن تظل نقطة التوقف البرتقالية عند الطرف المخفي. إذا ظهرت عند الحافة المتقدمة، فتحقق مما إذا كان العرض المرتبط بالتقدم يُطبّق على التدرّج نفسه.

أنشئ عرضًا تجريبيًا كاملًا لشريط تقدم بتدرّج على macOS

استخدم هدف تطبيق macOS بـ SwiftUI، على أن يكون هدف النشر macOS 12 أو أحدث. يتوفر التحميل الزائد لـ mask(alignment:_:) الذي يراعي المحاذاة والمستخدم أدناه بدءًا من macOS 12. وتوضح Apple أنه يطبّق قناة ألفا لعرض آخر على العرض المقنّع. mask(alignment:_:) — توثيق مطوري Apple

استبدل ملف نقطة الدخول الذي أنشأه التطبيق بهذا البرنامج الكامل. ويمكن ترك ملف عرض المحتوى المُنشأ من دون استخدام. أبقِ تصريحًا واحدًا فقط لـ @main في الهدف، وفقًا لعقد نقطة دخول التطبيق الذي تحدده Apple. App — توثيق مطوري Apple

import SwiftUI

@main
struct GradientRevealDemoApp: App {
    var body: some Scene {
        WindowGroup {
            GradientRevealDemo()
        }
    }
}

struct GradientRevealDemo: View {
    @State private var progress = 0.25
    @State private var isIndeterminate = false

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            ProgressView(
                value: isIndeterminate ? nil : progress,
                total: 1.0
            ) {
                Text("Sample activity")
            } currentValueLabel: {
                if isIndeterminate {
                    Text("In progress")
                } else {
                    Text("\(Int((progress * 100).rounded()))%")
                }
            }
            .progressViewStyle(GradientRevealStyle())

            Slider(value: $progress, in: 0...1, step: 0.01) {
                Text("Sample completion")
            }
            .disabled(isIndeterminate)

            HStack {
                Button("Reset") { progress = 0 }
                Button("25%") { progress = 0.25 }
                Button("50%") { progress = 0.5 }
                Button("Complete") { progress = 1 }
            }
            .disabled(isIndeterminate)

            Toggle("Indeterminate", isOn: $isIndeterminate)
        }
        .padding(24)
        .frame(minWidth: 360, idealWidth: 520, minHeight: 240)
    }
}

@MainActor
struct GradientRevealStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        let fraction = configuration.fractionCompleted.flatMap {
            $0.isFinite ? min(max($0, 0), 1) : nil
        }

        VStack(alignment: .leading, spacing: 8) {
            configuration.label

            if let fraction = fraction {
                GeometryReader { geometry in
                    ZStack(alignment: .leading) {
                        Rectangle()
                            .fill(Color.gray.opacity(0.2))

                        LinearGradient(
                            stops: [
                                .init(color: .blue, location: 0),
                                .init(color: .purple, location: 0.5),
                                .init(color: .orange, location: 1)
                            ],
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                        .frame(width: geometry.size.width, height: 14)
                        .mask(alignment: .leading) {
                            Rectangle()
                                .frame(
                                    width: geometry.size.width
                                        * CGFloat(fraction),
                                    height: 14
                                )
                                .animation(
                                    .easeInOut(duration: 0.25),
                                    value: fraction
                                )
                        }
                    }
                    .frame(width: geometry.size.width, height: 14)
                    .clipShape(Capsule())
                }
                .frame(height: 14)
            } else {
                ProgressView()
                    .progressViewStyle(.circular)
            }

            configuration.currentValueLabel
        }
    }
}

تطبّق الواجهة المخصصة makeBody(configuration:)، وهي نقطة التوسعة الموثّقة لمظهر عرض التقدم. وتعرض كلا التسميتين المقدمتين، وتقرأ نسبة الإنجاز من الإعدادات. يحافظ وضع التنسيق في هذا البروتوكول على واجهة ProgressView لدى المستدعي. ProgressViewStyle — توثيق مطوري Apple

تبدأ العينة عند 25%. استخدم الأزرار للوصول إلى نقاط تحقق قابلة للتكرار، واسحب شريط التمرير لفحص القيم الوسيطة، وغيّر حجم النافذة لمعاينة التخطيط. تغيّر عناصر التحكم هذه حالة محلية اصطناعية؛ ولا تمثل عملًا مقاسًا. جرى فحص المصدر والتحقق من حسابات العرض باستخدام Python، لكن برنامج SwiftUI لم يُجمّع ولم يُشغّل في بيئة البحث.

كيف يحافظ التخطيط على ثبات التدرّج

يقيس GeometryReader المساحة المخصصة للمؤشر. وتصفه Apple بأنه حاوية يعتمد محتواها على حجمها ونظام إحداثياتها، ولها حجم مفضّل مرن. ويمنح تطبيق .frame(height: 14) خارجها هذه الحاوية المرنة ارتفاعًا صريحًا ضمن المكدس العمودي. GeometryReader — توثيق مطوري Apple

داخل تلك الحاوية، يأخذ التدرّج العرض المقاس كاملًا. أما المستطيل داخل القناع وحده فيأخذ العرض المضروب في نسبة الإنجاز. وعندما يتغير التقدم، يظل إطار التدرّج ثابتًا عند حجم نافذة معين.

محاذاة القناع إلى .leading أساسية: فهي تضع القناع الأصغر عند الحافة الأمامية للتدرّج. وتحدد معامل المحاذاة الخاص بإطار العرض موضع المحتوى داخل ذلك الإطار؛ ولا يحل محل المحاذاة بين القناع والعرض الذي يُطبّق عليه القناع. توثّق Apple هاتين المسؤوليتين بوصفهما منفصلتين. mask(alignment:_:) — توثيق مطوري Apple، frame(width:height:alignment:) — توثيق مطوري Apple

يكشف المستطيل المعتم التدرّج حيثما وُجد. ولا يحدد لونه الدرجات اللونية التي تظهر، لأن القناع يستخدم ألفا. أما استبداله بمستطيل شفاف فسيخفي التدرّج. أبقِ المسار الرمادي خارج القناع ليظل ظاهرًا عند انعدام التقدم.

يقص الغلاف النهائي على شكل كبسولة المسار المركب والتدرّج المكشوف وفق المحيط الخارجي نفسه. وتوثّق Apple أن القص يُبقي الأجزاء التي يغطيها شكل القص. يُبقي هذا المثال عمدًا الحافة المتقدمة مستقيمة حتى تصل إلى الطرف المستدير للكبسولة؛ أما قناع الكشف على شكل كبسولة فسينتج حافة متقدمة مستديرة. clipShape(_:style:) — توثيق مطوري Apple

يغيّر تغيير حجم النافذة عرض المسار الكامل، ولذلك يُعاد حساب التدرّج على امتداد هذا العرض الجديد. والمقصود بعبارة «ألوان ثابتة» هنا ثبات مواضع الألوان بالنسبة إلى المسار أثناء تحديثات التقدم، لا ثباتها عند إحداثيات الشاشة عبر تغيير حجم النافذة.

حافظ على قيم التقدم وأنواعه وسلوك دورة الحياة

تتلقى الواجهة fractionCompleted كقيمة اختيارية من النوع Double. وتحدد Apple نطاقها العددي من 0 إلى 1، وتستخدم nil للتقدم غير المحدد أو التقدم نسبةً إلى فاصل زمني. يجعل تحويل تلك النسبة إلى CGFloat عند حساب العرض الحد الفاصل بين النوعين العدديين صريحًا. fractionCompleted — توثيق مطوري Apple

تقيّد العينة النسب المحدودة احترازيًا، وتحوّل النسبة المفقودة أو غير المحدودة إلى مؤشر قياسي غير محدد. هذا التعامل مع القيم غير المحدودة سياسة صريحة في هذه العينة. وهو يمنع تفسير النسبة غير المتاحة على أنها إنجاز معدوم. ويختار المؤشر الداخلي النمط المضمّن صراحةً باستخدام .circular.

في موضع الاستدعاء، قدّم مقدارًا منجزًا محدودًا وإجماليًا محدودًا موجبًا. يقبل المُهيّئ المعتمد على القيمة nil للتقدم غير المحدد، ويوثّق تثبيت القيم الخارجة عن النطاق عند حدوده. تستخدم العينة إجماليًا قدره 1، وتقيّد شريط التمرير بالفاصل نفسه. init(value:total:label:currentValueLabel:) — توثيق مطوري Apple

تملك الواجهة الأم حالة التقدم، بينما تستمد الواجهة تنسيقها من الإعدادات. تحدّث SwiftUI العروض التابعة عند تغير الحالة. لذلك، لا تحتاج هذه العينة إلى مؤقت أو إجراء استدعاء عند الظهور أو مهمة في الخلفية للحفاظ على حالتها المرئية. State — توثيق مطوري Apple

يُطبّق التحريك على مستطيل الكشف. ويطبّق المُعدِّل المحدد بقيمة حركةً عند تغير النسبة المراقبة؛ أما مدة ربع الثانية المختارة فهي مجرد خيار توضيحي للعرض. ولا تحسب تقدم المهمة. أزل المُعدِّل إذا كانت التحديثات الفورية أنسب لواجهتك. animation(_:value:) — توثيق مطوري Apple

هناك قيد موثّق للتقدم المدفوع بالمؤقت: تقول Apple إن عروض التقدم المرتبطة بالتاريخ لا تدعم الأنماط المخصصة. لاستخدام هذا المظهر المتدرّج، استخدم مُهيّئ التقدم العددي، واجعل مالك النشاط يقدّم نسبة الإنجاز. وإذا كنت تحتاج إلى التحديثات التلقائية المرتبطة بالتاريخ من Apple، فاستخدم ذلك المُهيّئ مع العرض الذي يدعمه. init(timerInterval:countsDown:label:currentValueLabel:) — توثيق مطوري Apple

تحقق من الكشف باستخدام تسلسل تشخيصي قصير

نفّذ هذه الفحوص بعد إنشاء هدف macOS. فهي تميّز مشكلات توزيع الألوان من مشكلات المحاذاة والحجم:

  1. اختر Reset. ينبغي أن يظل المسار الرمادي ظاهرًا من دون ظهور أي تدرّج.
  2. اختر 25%. ينبغي ألا تظهر إلا الألوان الأولى من التدرّج. يشير ظهور الطرف البرتقالي إلى أن التدرّج نفسه يجري تقصير عرضه.
  3. اختر 50%. ينبغي أن تصل حدود الكشف إلى نقطة المنتصف البنفسجية مع نقاط التوقف المتوفرة.
  4. اختر Complete. ينبغي أن يملأ التدرّج المسار حتى اللون البرتقالي.
  5. غيّر حجم النافذة عند 50%. ينبغي أن يظل الجزء الظاهر نصف المسار الجديد الحجم.
  6. فعّل Indeterminate. ينبغي أن يحل المؤشر المضمّن محل التعبئة العددية، مع بقاء تسميتي المهمة والقيمة الحالية ظاهرتين.

إذا نما الكشف من الوسط إلى الخارج، فتحقق من .mask(alignment: .leading). وإذا شغل المؤشر مساحة رأسية مفرطة، فتحقق من الإطار خارج GeometryReader. وإذا أدى تغيير التقدم إلى تحريك كل نقاط الألوان، فتحقق من أن القناع وحده يستخدم معامل الإنجاز.

القاعدة القابلة لإعادة الاستخدام بسيطة: اضبط حجم التدرّج ليتناسب مع المسار الكامل، ثم استخدم قناعًا مستقلًا مضبوط الحجم لإظهار التقدم. إن فصل هاتين المسؤوليتين يسهل فهم الإنجاز الجزئي وتغيير الحجم والتحريك، مع الحفاظ على الإعدادات الموثّقة لعرض التقدم.

المصادر