عرّف المحتوى الذي يوفّره المستدعي على هيئة (Input) -> Content، مع تقييد Content ليكون من النوع View. يتيح ذلك لمكوّن macOS توفير قيمة ذات نوع محدد، بينما يختار المستدعون طريقة عرضها. يشترط العقد قبول هذا الإدخال، لكنه لا يمكنه اشتراط استخدامه.

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

ما الواجهة التي تعبّر عن هذا المتطلب؟

استخدم معاملًا عامًا مسمّى للعرض المُعاد، ونوعًا محددًا لإدخال المكوّن. في المثال أدناه، التوقيع الأساسي هو @ViewBuilder content: @escaping (SwatchInput) -> Content، ضمن تعريف يقيّد Content: View.

للإدخال والإخراج وظيفتان منفصلتان. يحدّد SwatchInput القيم التي يوفّرها المكوّن. ويمثّل Content نوع العرض المحدد الذي ينتجه إغلاق كل مستدعٍ. تتيح المعاملات العامة المسماة للتعريف التعبير عن علاقات الأنواع هذه وإعادة استخدامها، كما يوضح فصل Swift عن المعاملات العامة.

تستخدم Apple نمط الواجهة هذا في GeometryReader: إذ يقبل مُهيّئه إغلاقًا من النوع (GeometryProxy) -> Content. يوفّر الحاوي الوكيل، ويبني المستدعي محتوى انطلاقًا منه. وهذا مثال مفيد لمكوّن مخصص يوفّر إعداداته الخاصة. راجع توثيق Apple لكل من GeometryReader وinit(content:).

اختر العقد وفقًا للمهمة الفعلية:

المتطلب الواجهة المناسبة
يوفّر المستدعي محتوى من دون إدخال من المكوّن () -> Content
يوفّر المكوّن إعدادًا محددًا (SwatchInput) -> Content
تتلقى الحاوية قيمة يختلف نوعها بين الاستخدامات معامل عام مسمّى Input، مع إغلاق مطابق من النوع (Input) -> Content
يجب على المترجم إثبات أن المستدعي يستخدم الإعداد توقيع الإغلاق لا يعبّر عن هذا المتطلب

تنبع هذه الخيارات من أنواع الدوال في Swift: إذ تصف أنواع المعاملات ونوع الإرجاع ما يمكن تمريره وإرجاعه. لكنها لا تصف كيف يجب على التنفيذ استخدام الوسائط. راجع قسم نوع الدالة في مرجع لغة Swift.

إنشاء معاينة متكاملة لعيّنة على macOS

استخدم هدف تطبيق macOS مع SwiftUI. يستخدم المثال الصياغة المعروفة @ViewBuilder، وهو مخصص لـ Swift 5.7 أو أحدث، مع هدف نشر macOS 12 أو أحدث. ويتجنب وحدات الماكرو للمعاينة وأسماء بناة المحتوى الأحدث. لم تكن Swift وحزمة تطوير Apple متاحتين في بيئة البحث، لذا جرت معاينة هذا البرنامج دون تجميعه أو تشغيله هنا.

في مشروع تطبيق SwiftUI في Xcode، استبدل تعريف التطبيق المُنشأ تلقائيًا بالبرنامج الكامل التالي. أزل تعريف ContentView المُنشأ تلقائيًا إذا كان سيكرر هذا التعريف. أبقِ نقطة إدخال واحدة فقط للتطبيق، مع وضع @main عليها. يوثّق دليل مساعدة Xcode بدء العمل بمشروع SwiftUI أو إضافة ملف SwiftUI إلى مشروع موجود.

import SwiftUI

struct SwatchInput {
    let title: String
    let side: CGFloat
    let color: Color
}

@MainActor
struct SwatchPanel<Content: View>: View {
    @State private var isLarge = false
    private let content: (SwatchInput) -> Content

    init(
        @ViewBuilder content: @escaping (SwatchInput) -> Content
    ) {
        self.content = content
    }

    private var input: SwatchInput {
        SwatchInput(
            title: "Sample A",
            side: isLarge ? 120 : 64,
            color: .blue
        )
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Toggle("Large sample", isOn: $isLarge)
            content(input)
        }
        .padding(24)
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        SwatchPanel { input in
            Text(input.title)
                .font(.headline)

            RoundedRectangle(cornerRadius: 12)
                .fill(input.color)
                .frame(width: input.side, height: input.side)

            if input.side == 120 {
                Text("Large preview")
                    .font(.caption)
            }
        }
        .frame(width: 300, height: 280, alignment: .topLeading)
    }
}

@main
@MainActor
struct TypedContentDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

تحتوي الشاشة الأولية المتوقعة على مفتاح Large sample، والعنوان Sample A، ومربع أزرق مستدير طول ضلعه 64 نقطة. يؤدي تشغيل المفتاح إلى تغيير طول الضلع الممرر إلى 120 نقطة وإضافة Large preview. هذه الأحجام والتسميات قيم محلية توضيحية، وليست نتائج مقاسة.

يمتلك المكوّن isLarge. وتُجمّع الخاصية المحسوبة input الاختيار الحالي، ثم تمرر content(input) هذه الحزمة إلى المستدعي. تدير SwiftUI تخزين الحالة وتحدّث أجزاء التسلسل الهرمي للعرض التي تعتمد على قيمة متغيرة، كما هو موثّق تحت State.

يتولى المستدعي العرض: عنوانًا وشكلًا وتعليقًا اختياريًا. ويمكنه استبدال ترتيبها بترتيب آخر مع الإبقاء على الإدخال ذي النوع نفسه. يطبّق كل من اللوحة والمستدعي body لتركيب واجهتيهما، وفق نموذج View الموثّق من Apple.

لماذا نحتاج إلى وسم الباني ووسم الإغلاق الهارب؟

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

لا يوفّر الباني SwatchInput. يعرّف معامل الإغلاق العادي الإدخال، وتوفّر content(input) الوسيط الفعلي. إذا قبل المُهيّئ () -> Content، فلن يحوّله إضافة وسم الباني إلى إغلاق يقبل الإعداد.

نحتاج إلى @escaping لأن المُهيّئ يخزّن الإغلاق في خاصية لاستدعائه لاحقًا من body. يشرح فصل الإغلاقات في Swift أن الإغلاق يكون هاربًا عندما يمكن استدعاؤه بعد عودة الدالة المستقبِلة.

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

الحفاظ على حدود الباني في Xcode

تميّز وثائق Apple الحالية بين Xcode 26 والإصدارات الأقدم، وXcode 27 والإصدارات الأحدث. بالنسبة إلى الأدوات الأقدم، تصف الوثائق ViewBuilder بأنه يبني العروض. أما مع Xcode 27 والإصدارات الأحدث، فتصف الإنشاء بأنه غير مقيّد بنوع محدد، وتوصي بالاسم المستعار ContentBuilder للدلالة على هذا السلوك الأوسع. راجع ViewBuilder.

عند صيانة قاعدة شيفرة تعمل على Xcode 27 أو أحدث، يمكن للمُهيّئ استخدام @ContentBuilder بدلًا من ذلك. توثّقه Apple بوصفه اسمًا مستعارًا للنوع ViewBuilder، مع فرض المطابقة عبر أنواع المحتوى التي ينتجها. أبقِ Content: View في هذا المكوّن، فالناتج ما زال مخصصًا للتسلسل الهرمي للعرض. لا تفترض وجود الصياغة الأحدث في حزم SDK الأقدم لمجرد أن الوثائق الحالية تسرد أهداف نشر أقدم. راجع ContentBuilder.

لماذا لا يحل some View كل جوانب هذا العقد؟

تُعد some View مناسبة لخصائص body في المثال. فهي تخفي نوع النتيجة المحدد، مع الحفاظ على الوعد بأن النتيجة عرض. أما الإغلاق المخزّن فيستخدم المعامل المسمّى Content حتى يتمكن المكوّن من الإشارة إلى نوع النتيجة نفسه في الخاصية والمُهيّئ.

قدّمت Swift 5.7 تعريفات المعاملات المعتمة عبر SE-0341. يقدّم معامل مباشر مثل _ value: some View معاملًا عامًا غير مسمّى يحدّد المستدعي نوعه المحدد. وهذا لا يعني أن تنفيذ الدالة يستطيع اختيار أي عرض وتمريره إلى موضع يُتوقع فيه ذلك النوع غير المسمّى. راجع تعريفات المعاملات المعتمة.

على وجه الخصوص، فإن معامل ردّ اتصال مكتوبًا على هيئة (some View) -> Content يقع خارج مواضع المعاملات المعتمة المدعومة والموثّقة في SE-0341. ويحظر المقترح صراحةً استخدام الأنواع المعتمة في موضع الإدخال لنوع دالة ردّ الاتصال. ويرد النقاش حول احتمال إتاحة هذه الصياغة ضمن التوجهات المستقبلية، لا السلوك المطبّق. ويُبقي مرجع الأنواع الحالي على هذا القيد.

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

ما الذي يضمنه المترجم فعليًا؟

يضمن التوقيع شكلًا قابلًا للاستدعاء: وسيطًا واحدًا من النوع SwatchInput ونتيجة تستوفي Content: View. كما يمنح معامل الإغلاق نوعه المستنتج، حتى يتمكن المستدعي من الوصول إلى input.title وinput.side وinput.color دون تكرار تعريف النوع. توثّق Swift هذا الاستنتاج من سياق الإغلاق في الإغلاقات.

لا يمكنه ضمان أن تؤثر هذه الخصائص في العرض. على سبيل المثال، هذا تعبير بديل صالح داخل ContentView.body، ويتجاهل الإعداد عمدًا بعد قبوله:

SwatchPanel { _ in
    Text("Fixed preview")
}

هذا الحد مهم عند كتابة متطلبات المكوّن. «يقبل محتوى المستدعي SwatchInput» هو عقد أنواع. أما «يعرض محتوى المستدعي الحجم الممرر» فهو متطلب سلوكي. إذا كان عرض ذلك الحجم أساسيًا للمكوّن، فضع هذا العرض داخل المكوّن وأتح مساحة تخصيص أصغر.

استخدم الفحوصات التالية لتشخيص مشكلات الدمج. إنها خطوات فحص مستمدة من الواجهة، وليست نتائج لاختبارات مترجم مُبلّغ عنها:

العَرَض الفحص
وسيط مفقود عند استدعاء content مرّر الإعداد: content(input).
تشخيص بشأن إغلاق هارب في المُهيّئ أبقِ @escaping عند تخزين ردّ الاتصال.
فشل عدة عروض شقيقة في المستدعي أبقِ وسم الباني المناسب على معامل المُهيّئ.
رفض ردّ اتصال ذي نوع محدد صراحةً تحقّق من أن إدخاله هو SwatchInput، لا نوعًا غير مرتبط مثل String.
تجاهل المعاينة لمفتاح الحجم تحقّق مما إذا كان العرض يقرأ input.side.
استمرار المكوّن في عرض الإعداد الأولي تحقّق مما إذا كان المحتوى قد بُني مرة واحدة وخُزّن بدلًا من استدعاء ردّ الاتصال بالإدخال الحالي.

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

المصادر