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

يبني هذا الدليل نافذة macOS صغيرة تحتوي على عنصرين تجريبيين. تتمثل المهمة في تخزين موفّر والعرض الذي ينتجه دون استبدال النوع المرتبط بـ any View أو AnyView. ينطبق النمط نفسه عندما يقبل مكوّن سطح مكتب قابل لإعادة الاستخدام تسمية مخصّصة أو عنصرًا إضافيًا أو موفّر محتوى.

ما العلاقة التي يجب أن تحافظ عليها الخصائص المخزّنة؟

يسمّي النوع المرتبط نوعًا تختاره كل مطابقة للبروتوكول. إذا أعلن بروتوكول عن associatedtype Content: View، فيجب على الموفّر المطابق أن يقدّم نوع محتوى محددًا. ويمكن للشيفرة العامة الإشارة إلى هذا النوع باسم Provider.Content، حتى عندما يكون تنفيذه مخفيًا خلف نتيجة مبهمة. توثّق Swift الأنواع المرتبطة والقيود العامة في فصل Generics.

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

تستخدم SwiftUI عقدًا ذا صلة: يعلن View عن associatedtype Body: View، وتستنتج Swift هذا النوع المرتبط من تنفيذ body في العرض المخصّص. إن Content الخاص بموفّرك وBody الخاص بالمكوّن نوعان مرتبطان منفصلان؛ ولا يلزم أن يكونا متساويين. راجع توثيق Body من Apple.

إنشاء مثال كامل لنظام macOS

استخدم في هذا المثال مشروع تطبيق macOS في Xcode مع SwiftUI وهدف نشر macOS 13 أو أحدث. استبدل تصريحات التطبيق وعرض المحتوى التي أنشأها المشروع بالملف الواحد التالي، مع الإبقاء على تصريح @main واحد بالضبط في الهدف. توثّق Apple بنية نقطة الدخول هذه واستخدام WindowGroup في App.

يستخدم المثال عمدًا الصياغة المعروفة للنوع المرتبط والمعامل العام المسمّى. طُبّقت النتائج المبهمة في Swift 5.1؛ أما صياغة النوع المرتبط الأساسي الاختيارية التي نناقشها لاحقًا فتتطلب Swift 5.7. الملف الكامل معدّ لسلسلة أدوات Swift 6 الحالية. جرت مراجعته بمقارنته بالوثائق المسترجعة، لكن لم يُترجم هنا لأن هذه البيئة لا تحتوي على مترجم Swift. هدف نشر macOS المختار متطلب لهذا المثال، وليس ادعاءً بشأن أقدم نظام تشغيل يدعم كل ميزة أساسية. راجع SE-0244 وSE-0346.

import SwiftUI

@MainActor
protocol TileProvider {
    associatedtype Content: View

    var title: String { get }
    func makeContent() -> Content
}

@MainActor
struct TextTile: TileProvider {
    let title: String
    let message: String

    func makeContent() -> Text {
        Text(message)
    }
}

@MainActor
struct CircleTile: TileProvider {
    let title: String
    let caption: String

    func makeContent() -> some View {
        HStack(spacing: 8) {
            Circle()
                .fill(Color.blue)
                .frame(width: 16, height: 16)
            Text(caption)
        }
    }
}

@MainActor
struct TileCard<Provider: TileProvider>: View {
    private let provider: Provider
    private let content: Provider.Content

    init(provider: Provider) {
        self.provider = provider
        self.content = provider.makeContent()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(provider.title)
                .font(.headline)
            content
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.12))
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        VStack(spacing: 12) {
            TileCard(provider: TextTile(
                title: "Text tile",
                message: "Blue square"
            ))
            TileCard(provider: CircleTile(
                title: "Shape tile",
                caption: "Blue circle"
            ))
        }
        .padding()
        .frame(width: 360)
    }
}

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

أنشئ هدف macOS وشغّله. النتيجة المتوقعة نافذة تحتوي على عنصر نصي وعنصر فيه دائرة زرقاء بجوار تعليقها. يستخدم كلاهما المكوّن العام نفسه، مع استنتاج نوع الموفّر بصورة مستقلة عند كل موضع استدعاء.

تُبقي التعليقات التوضيحية الصريحة @MainActor هذه التصريحات التي تنتج واجهة المستخدم في سياق الممثل نفسه. يعرّف توثيق View الحالي من Apple البروتوكول بعزل الممثل الرئيسي. كما توضّح التعليقات التوضيحية هنا السياق المقصود لبروتوكول الموفّر المخصّص.

لماذا يحافظ المكوّن العام على النوع؟

في البطاقة الأولى، يكون Provider هو TextTile، ولذلك يكون Provider.Content هو Text. وفي البطاقة الثانية، يكون Provider هو CircleTile، ونوع المحتوى المرتبط به هو النتيجة المبهمة لـ CircleTile.makeContent(). يمكنك تسمية هذا النوع عبر CircleTile.Content من دون كتابة نوع تخطيط SwiftUI المتداخل.

الإسناد الأساسي هو self.content = provider.makeContent(). يضمن البروتوكول أن هذه الطريقة تُرجع Provider.Content تحديدًا، وهو النوع نفسه المعلن للخاصية المخزّنة. هذه علاقة وقت ترجمة مستمدة من القيد العام، وليست مقارنة أنواع وقت التشغيل. يشرح فصل Generics في Swift كيفية الوصول إلى الأنواع المرتبطة عبر المعاملات العامة المقيّدة.

استخدام some View في تنفيذ المطابقة لا يمحو هذه العلاقة. تخفي النتيجة المبهمة التنفيذ الملموس مع الحفاظ على هوية نوعه. وهذا يجعلها مناسبة لتكون شاهدًا على النوع المرتبط. تشرح Swift هذا الفرق في أنواع البروتوكولات المبهمة والمغلّفة.

لهاتين البطاقتين تخصيصان مختلفان: TileCard<TextTile> وTileCard<CircleTile>. إعادة استخدام التصريح المصدر لا تعني أن على كل بطاقة امتلاك نوع محتوى مخزّن موحّد.

لماذا لا تحل الخصائص المبهمة أو الوجودية المستقلة المشكلة؟

المقاطع التالية لأغراض التشخيص، وليست بدائل للبرنامج الكامل:

// Diagnostic fragment: no initializer establishes the opaque type.
let content: some View

// Diagnostic fragment: hides which conforming provider is stored.
let provider: any TileProvider

// Diagnostic fragment: stores an existential view value.
let content: any View

تحتاج الخاصية المخزّنة المبهمة إلى مهيّئ تصريح يحدّد نوعها الأساسي. كتابة some View لا تنشئ معاملًا عامًا يختاره المستدعي ويمكن لمهيّئ مكوّن عشوائي ملؤه لاحقًا. مثلًا، يحدّد let content: some View = Text("Sample") نوعًا لهذا التصريح؛ لكنه لا يربط الخاصية بالنوع المرتبط بخاصية أخرى. توثّق SE-0244: أنواع النتائج المبهمة قاعدة الخصائص المخزّنة هذه.

تسمح صيغة وجودية مثل any TileProvider بتخزين أنواع مختلفة مطابقة للبروتوكول. لكنها تخفي النوع المطابق المحدد، لذلك لم يعد التصريح يكشف العلاقة بصيغة Provider.Content. وبالمثل، فإن any View حاوية وجودية؛ وليس العقد نفسه الذي يوفّره نوع عام مسمّى يطابق View. راجع شرح Swift لـأنواع البروتوكولات المغلّفة.

يمكن لـ Swift فتح وسيطات وجودية ضمنيًا في بعض الاستدعاءات العامة. لكن ذلك لا يجعل كل وجودية صالحة للاستخدام في كل مهيّئ عام. على وجه الخصوص، تقيّد SE-0352 الفتح عندما يُفلت النوع الأساسي المجهول داخل نتيجة عامة مثل X<T>. وقد يواجه تخصيص مكوّن عام هذا الحد تحديدًا. أبقِ الموفّر عامًا قبل إنشاء البطاقة بدل افتراض إمكان تحويل وجودية دائمًا إلى تخصيص مستمر. راجع SE-0352: فتح الوجوديات ضمنيًا.

اختر التخزين وفق توقيت إنتاج المحتوى

يستدعي المثال makeContent() أثناء تهيئة كل TileCard ويخزّن قيمة العرض المُعادة. ولا ينشئ ذلك ذاكرة تخزين مؤقت مستمرة عبر عمليات تهيئة المكوّن اللاحقة. يوضّح الاحتفاظ بالخاصيتين علاقة النوع المطلوبة، لكن يمكن لمكوّن لا يحتاج إلا إلى العرض الناتج حذف الموفّر المخزّن.

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

@MainActor
struct TileCard<Provider: TileProvider>: View {
    private let provider: Provider

    init(provider: Provider) {
        self.provider = provider
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(provider.title)
                .font(.headline)
            provider.makeContent()
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.12))
    }
}

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

استخدم دليل القرار هذا عند تعديل المثال:

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

هل تُغني الأنواع المرتبطة الأساسية عن الأنواع العامة؟

توفر الأنواع المرتبطة الأساسية طريقة أقصر للتعبير عن قيود التساوي المختارة بين الأنواع. إذا كنت تملك البروتوكول، فإن هذا الترويس البديل يجعل Content نوعًا أساسيًا؛ وتبقى متطلبات البروتوكول الأخرى كما هي:

// Alternative protocol header, requiring Swift 5.7 or later.
@MainActor
protocol TileProvider<Content> {
    associatedtype Content: View

    var title: String { get }
    func makeContent() -> Content
}

مع هذا التصريح، يصف any TileProvider<Text> وجودية يكون نوع محتواها المرتبط معروفًا وهو Text. ولا يمكن لتطبيقات موفّر مختلفة مشاركة نوع التخزين هذا إلا إذا كشفت مطابقاتها عن تساوي المحتوى المحدد نفسه. تقيّد هذه الصياغة نوعًا مرتبطًا؛ ولا تحوّل البروتوكول إلى بنية عامة. راجع SE-0346 وSE-0353.

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

طُبّقت الوجوديات المقيّدة في Swift 5.7، وتحدّد SE-0353 صراحةً قيود وقت التشغيل والنشر بالإصدارات السابقة. لذلك لا يكفي دعم المترجم وحده لإثبات توافق النشر لهذا التصميم الاختياري. تحقّق من تشخيصات التوفّر الخاصة بسلسلة الأدوات المستهدفة قبل اعتماده. المثال الأساسي لا يعتمد على تخزين الوجوديات المقيّدة. راجع مناقشة ABI في SE-0353.

ما الذي ينبغي التحقق منه عند فشل الترجمة؟

تتبّع النوع من متطلب البروتوكول إلى الخاصية المخزّنة قبل تغيير التخطيط:

  1. تأكّد من أن البروتوكول يعلن عن نوع مرتبط مقيّد بـ View.
  2. تأكّد من أن للمكوّن معامل موفّر عامًّا مسمّى ومقيّدًا بذلك البروتوكول.
  3. عرّف المحتوى المخزّن على أنه Provider.Content، بما يطابق نوع القيمة المُعادة من الطريقة.
  4. تحقّق مما إذا كان عنصر أب قد حوّل الموفّر بالفعل إلى any TileProvider.
  5. إذا أضفت معدّلًا قبل التخزين، فتحقّق من نوع نتيجته: فقد لا يعود العرض المعدّل من النوع Provider.Content.
  6. تحقّق من تشخيصات عزل الممثل على نحو منفصل عن تشخيصات النوع المرتبط.

توثّق Apple أن معدّلات العرض تغلّف العرض في عرض آخر. وبناءً على ذلك، فإن إسناد provider.makeContent().padding() إلى خاصية معلن عنها بالنوع Provider.Content يغيّر العقد الذي يجب أن يفي به الإسناد. خزّن المحتوى الأصلي وطبّق معدّلات العرض في body، أو أعد تصميم النوع المخزّن ليصف النتيجة المعدّلة. راجع View.

للبنّاءات أيضًا حدود مرتبطة بالإصدار. تصف وثائق Apple الحالية سلوك ViewBuilder في Xcode 26 والإصدارات الأقدم، ثم سلوكًا مختلفًا لبناء المحتوى بدءًا من Xcode 27. يتجنب المثال الحاجة إلى بنّاء مخصّص: إذ يُرجع كل موفّر تعبيرًا صريحًا واحدًا. إذا وسّعته بواجهات API لموفّرات تعتمد على البنّاءات، فاتبع وثائق إصدار Xcode الذي تستخدمه فعليًا للبناء. راجع ViewBuilder.

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

في هذا المكوّن، أبقِ العلاقة ظاهرة عند حدّ التخزين: Provider عام، ونتيجة من النوع Provider.Content، وخاصية مخزّنة من ذلك النوع نفسه. عندئذ يمكنك تغيير التخطيط الداخلي للموفّر مع ترك المترجم ينقل نوع العرض المرتبط عبر المكوّن.

المصادر