لإنشاء مكوّن SwiftUI عام، اشترط المُهيئ ضمن بروتوكول أو مرّر إغلاقًا يعيد المكوّن. يستخدم هذا الدليل النهجين لعرض العنصر الاصطناعي نفسه في نافذة macOS، مع تحديد حدود المترجم وحزمة SDK بوضوح.
ما الذي يجب أن يضمنه المكوّن العام؟
لنفترض أن تطبيق macOS يعرض بطاقة عنصر صغيرة. تريد لوحة قابلة لإعادة الاستخدام تنشئ البطاقة من عنصر، مع السماح لمستدعيها باختيار طريقة العرض. السؤال المحوري هو: من المسؤول عن الإنشاء، الواجهة المعلنة للمكوّن أم المستدعي الذي يوفّر المكوّن؟
يتيح القيد العام للتنفيذ استخدام الإمكانات التي يحددها ذلك القيد. إن كتابة Component: View تثبت أن المكوّن عرض في SwiftUI؛ لكنها لا تثبت أن للمكوّن مُهيئًا باسم init(item:). يشرح فصل الأنواع العامة في Swift قيود البروتوكولات، بينما توثّق Apple View وتشرح خاصية body المطلوبة للعرض.
لذلك، هذا المقتطف غير صالح عمدًا:
// Invalid fragment: View does not require init(item:).
@MainActor
func construct<Component: View>(item: Item) -> Component {
Component(item: item)
}
إضافة هذا المُهيئ إلى مكوّن ملموس واحد لا تجعله متاحًا عبر كل معامل عام لا يفرض عليه سوى القيد View. تحتاج الواجهة القابلة لإعادة الاستخدام إلى عقد إنشاء صريح. استخدم جدول القرار هذا قبل تغيير المكوّن:
| متطلب الإنشاء | الواجهة المناسبة | السبب |
|---|---|---|
| كل مكوّن مشارك يقبل العنصر نفسه | بروتوكول يشترط init(item:) |
الإنشاء جزء من واجهة عائلة المكوّنات. |
| يختار المستدعون إعدادات إضافية أو معدّلات | إغلاق مصنع يعيد المكوّن العام | يظل الإنشاء في موضع الاستدعاء. |
| لدى المستدعي مكوّن جاهز بالفعل | تخزين قيمة المكوّن العام | تم الإنشاء بالفعل. |
| يتبدل العرض بين مجموعة محدودة من التخطيطات | تركيب البدائل داخل عرض SwiftUI | يمكن للواجهة المحيطة الاحتفاظ ببنية عرض ملموسة. |
هذا الجدول أداة تصميم مستمدة من قواعد اللغة الموثقة. نطبّق الخيارين الأولين معًا أدناه لمقارنة سلوكهما مباشرة.
أنشئ مثالًا كاملًا لنظام macOS
استخدم هدف تطبيق macOS في Xcode مع SwiftUI وهدف نشر macOS 11 أو أحدث. كُتب المثال لمترجم Swift 6 مع حزمة SDK المطابقة من Apple. تتطلب تعليقات @MainActor الصريحة Swift 5.5 أو أحدث، لذا فالقائمة البرمجية ليست مخصصة للمترجمات الأقدم. توثّق Apple أن WindowGroup متاح بدءًا من macOS 11، وأنه حاوية للتسلسل الهرمي لعروض التطبيق. راجع WindowGroup.
استبدل ملف نقطة دخول التطبيق المُنشأ بالبرنامج الكامل التالي. تأكد من أن الهدف يحتوي على تعريف واحد فقط للتطبيق باستخدام @main. يمكن ترك ملف ContentView المُنشأ من دون استخدام. لا حاجة إلى حزم أو أصول خارجية أو بيانات نموذجية مُنزّلة.
import SwiftUI
struct Item {
let title: String
let detail: String
}
@MainActor
protocol ItemComponent: View {
init(item: Item)
}
@MainActor
struct ItemCard: ItemComponent {
let item: Item
init(item: Item) {
self.item = item
}
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(item.title)
.font(.headline)
Text(item.detail)
}
.padding()
.background(Color.gray.opacity(0.12))
.cornerRadius(8)
}
}
@MainActor
struct ContractPanel<Component: ItemComponent>: View {
let item: Item
init(item: Item, component: Component.Type) {
self.item = item
}
var body: some View {
Component(item: item)
}
}
@MainActor
struct FactoryPanel<Component: View>: View {
private let makeComponent: @MainActor () -> Component
init(make: @escaping @MainActor () -> Component) {
self.makeComponent = make
}
var body: some View {
makeComponent()
}
}
@MainActor
struct DemoView: View {
private let item = Item(
title: "Sample tile",
detail: "Blue circle on a neutral background"
)
var body: some View {
VStack(alignment: .leading, spacing: 18) {
Text("Initializer contract")
ContractPanel(item: item, component: ItemCard.self)
Text("Injected factory")
FactoryPanel {
ItemCard(item: item)
.padding(.leading, 12)
}
Text("Alternative factory output")
FactoryPanel {
Text(item.title + ": " + item.detail)
}
}
.padding(24)
.frame(width: 420)
}
}
@main
@MainActor
struct ComponentDemoApp: App {
var body: some Scene {
WindowGroup {
DemoView()
}
}
}
شغّل هدف تطبيق macOS. النتيجة المتوقعة نافذة واحدة تحتوي على طريقتين لعرض البطاقة وطريقة عرض نصية بسيطة. للبطاقة الثانية مسافة بادئة إضافية من جهة البداية يوفّرها المستدعي. هذه أمثلة اصطناعية على سلوك الواجهة، وليست نتائج مقاسة. لا يوجد مترجم Swift مثبت في بيئة البحث، لذا جرت مراجعة هذه القائمة البرمجية، لكن لم تُترجم أو تُنفّذ هنا.
تجعل تعليقات الممثل الرئيسي حدود إنشاء الواجهة صريحة. تعريف View الحالي لدى Apple موسوم بـ @MainActor و@preconcurrency؛ كما تشرح وثائقه كيفية توريث العزل عندما يظهر الامتثال في التصريح الأصلي للنوع. راجع View.
كيف يعمل عقد المُهيئ؟
يوسّع ItemComponent البروتوكول View ويضيف متطلب إنشاء واحدًا فقط: init(item:). تدعم Swift متطلبات المُهيئات مباشرةً في البروتوكولات. يوفّر النوع الممتثل التنفيذ، بينما يحدد البروتوكول التوقيع من دون متن للمُهيئ. راجع البروتوكولات: متطلبات المُهيئات.
يجعل هذا المتطلب Component(item: item) صالحًا داخل ContractPanel. يوفّر الوسيط component: Component.Type نوعًا ملموسًا للاستدلال العام: تمرير ItemCard.self يحدد أن نوع المكوّن لهذه اللوحة هو ItemCard. لا تحتاج اللوحة إلى تخزين قيمة النوع، لأن body ينشئ عبر معامل النوع العام.
يفيد هذا العقد عندما ينبغي أن يكون من الممكن إنشاء كل مكوّن انطلاقًا من المدخل نفسه. فمثلًا، قد تشترك عائلة من عروض العناصر المدمجة والموسعة التي يملكها التطبيق في هذا المتطلب. عندها يمكن للمستدعي تغيير نوع المكوّن المختار من دون إعادة كتابة تعبير الإنشاء في اللوحة.
يظهر الجانب الآخر عندما يحتاج التنفيذ إلى إعدادات إضافية. فاشتراط init(item:) يعني أن على كل مكوّن ممتثل توفير مسار الإنشاء هذا، حتى لو كان مُهيئه المفضّل يقبل أيضًا تسمية توضيحية أو إعداد تخطيط. إذا كانت هذه المعلومات الإضافية من مسؤولية المستدعي، فعادةً ما يعبّر نهج المصنع أدناه عن المتطلب بصورة أوضح.
في التطبيقات المعتمدة على الأصناف، يجب أن يكون المُهيئ الذي يفي بمتطلب بروتوكول موسومًا بـ required ما لم يكن الصنف final. وقد يحتاج الصنف الفرعي الذي يتجاوز أيضًا مُهيئًا معيّنًا مطابقًا في الصنف الفائق إلى كل من required وoverride. لا تستخدم البُنى مثل ItemCard الرمز required. توثّق البروتوكولات هذه الفروق.
متى ينبغي نقل الإنشاء إلى مصنع؟
يقبل FactoryPanel دالة من النوع @MainActor () -> Component. ووعده ببساطة هو أن استدعاء الدالة ينتج نوع المكوّن المختار. يمكن للإغلاق استدعاء مُهيئ، أو التقاط إعدادات، أو إرجاع عرض مع تطبيق معدّلات عليه.
يوضح البرنامج الفرق العملي: يمكن للمصنع إرجاع ItemCard مع مسافة بادئة، أو إرجاع Text، من دون اشتراط امتثال أي من النوعين الناتجين للبروتوكول ItemComponent. تشرح Apple أن معدّلات العرض قد تغلّف مدخلاتها داخل عرض آخر؛ ولهذا يفيد هنا ترك المترجم يستنتج النوع الناتج للمصنع. راجع View.
يعلّم المُهيئ الإغلاق بـ @escaping لأن اللوحة تخزّنه لاستدعائه لاحقًا. يوثّق فصل الإغلاقات في Swift الإغلاقات الهاربة وقدرتها على التقاط القيم المحيطة. في هذا المثال، يلتقط الإغلاق العنصر الاصطناعي الذي يستخدمه العرض المحيط.
اختر مدخلات المصنع بعناية. يعني المصنع الذي لا يأخذ وسائط أن المستدعي يلتقط كل ما يلزم للإنشاء. إذا كانت اللوحة نفسها تختار العنصر، فقد يعبّر مصنع ذو وسيط عن هذه المسؤولية بوضوح أكبر. التعريف التالي بديل لتعريف المكوّن، ويُستخدم إلى جانب Item الحالي؛ وهو ليس تطبيقًا كاملًا ثانيًا:
@MainActor
struct ItemFactoryPanel<Component: View>: View {
let item: Item
private let makeComponent: @MainActor (Item) -> Component
init(
item: Item,
make: @escaping @MainActor (Item) -> Component
) {
self.item = item
self.makeComponent = make
}
var body: some View {
makeComponent(item)
}
}
مع هذا التعريف، توفّر اللوحة العنصر، ويوفّر المستدعي طريقة العرض. احصر عمل المصنع في إنتاج قيمة العرض: فعقد الإنشاء في المثال لا يضمن تنفيذًا لمرة واحدة أو تخزينًا دائمًا أو تخزينًا مؤقتًا.
هل تحتاج المصانع إلى منشئ نتائج؟
المصانع في البرنامج الكامل إغلاقات عادية. يعيد كل منها تعبيرًا واحدًا، لذا لا حاجة إلى وسم مخصّص بمنشئ. وإذا كانت هناك حاجة إلى عدة عروض فرعية، يكفي إرجاع VStack واحدة لهذه الواجهة؛ إذ تضم المكدسة العروض الفرعية.
يفيد منشئ النتائج عندما ينبغي لمعامل المصنع نفسه قبول تسلسل تصريحي من التعبيرات أو محتوى شرطي. يسجل Swift Evolution تنفيذ ميزة منشئات النتائج المعيارية في Swift 5.4. راجع SE-0289: منشئات النتائج.
حافظ على حدود حزمة SDK عند اعتماد صياغة المنشئ. تصف وثائق Apple الحالية @ViewBuilder للوسائط التي تنتج عروضًا في Xcode 26 والإصدارات الأقدم. أما في Xcode 27 والإصدارات الأحدث، فتصف إنشاءً غير مقيد بالنوع وتوصي بالاسم المستعار ContentBuilder للدلالة على هذا السلوك. راجع ViewBuilder وContentBuilder. يتجنب الإغلاق العادي المستخدم هنا الحاجة إلى الاسم المستعار الأحدث.
لماذا لا تحل الأنواع المعتمة أو الوجودية محل العقد؟
تخفي some View نوع نتيجة أساسيًا محددًا مع الحفاظ على هويته. وهي مناسبة لخصائص body في هذا البرنامج. لكنها لا تضيف init(item:) إلى واجهة View. أما القيمة المغلّفة المكتوبة بصيغة any View فتخفي نوعها الأساسي بطريقة مختلفة، ولا تحافظ على الهوية النوعية الثابتة نفسها. راجع أنواع البروتوكولات المعتمة والمغلّفة.
لا تحوّل ذلك إلى ادعاء عام بأن القيم الوجودية لا يمكن تمريرها أبدًا إلى الدوال العامة. فقد أدخل SE-0352 فتح القيم الوجودية ضمنيًا في Swift 5.7، ما يسمح للاستدعاءات المؤهلة بربط معامل عام بالنوع الأساسي لقيمة وجودية. كما يوثّق قيودًا، منها النتائج التي تدمج النوع المفتوح ضمن غلاف عام حين يتعذر تمثيل النتيجة بعد محو النوع. راجع القيم الوجودية المفتوحة ضمنيًا.
في مهمة هذه اللوحة، يكفي الاحتفاظ بنوع المكوّن الملموس عبر معامل عام. لا حاجة إلى إدخال تخزين وجودي لمجرد السماح للمستدعين باختيار طريقة إنشاء عرض.
كيف تشخّص خطأً في الإنشاء؟
اتبع التعبير الذي يفشل وافحص العقد المتاح في موضعه:
- إذا فشل
Component(item:)، فتحقق من أن القيد العام يتضمن بروتوكولًا يشترط توقيع المُهيئ نفسه تمامًا. - إذا تعذر على المترجم استنتاج
Component، فمرّر قيمة نوع ملموسة مثلItemCard.selfأو مصنعًا له تعبير إرجاع ملموس. - إذا كان المكوّن يحتاج إلى إعدادات خاصة بالمستدعي، فانقل الإنشاء إلى المصنع بدلًا من إضافة متطلبات لا صلة لها إلى البروتوكول.
- إذا أدى تخزين إغلاق إلى تشخيص يتعلق بإغلاق هارب، فتحقق من وجود وسم
@escapingعلى المُهيئ. - إذا أدى الإنشاء إلى تشخيص متعلق بعزل الممثل، فتحقق من أن موضع الاستدعاء ونوع دالة المصنع يحددان حدود الممثل الرئيسي المقصودة.
- إذا أدت قيمة وجودية إلى خطأ في النوع العام الناتج، فافحص قيود نوع الإرجاع في SE-0352 قبل افتراض أن ترقية المترجم ستزيل القيد.
لعائلة مكوّنات مشتركة على macOS، اشترط المُهيئ عندما يكون الإنشاء الموحد جزءًا من الواجهة. واستخدم مصنعًا مُمررًا عندما ينبغي للمستدعي تركيب العرض وإعداده. يوضّح كلا المسارين وعد الإنشاء الناقص، مع الاحتفاظ بنوع المكوّن الملموس الذي تحتاج إليه اللوحة.
المصادر
- البروتوكولات — لغة البرمجة Swift، Swift.org.
- الأنواع العامة — لغة البرمجة Swift، Swift.org.
- الإغلاقات — لغة البرمجة Swift، Swift.org.
- أنواع البروتوكولات المعتمة والمغلّفة — لغة البرمجة Swift، Swift.org.
- View — وثائق مطوري Apple.
- WindowGroup — وثائق مطوري Apple.
- ViewBuilder — وثائق مطوري Apple.
- ContentBuilder — وثائق مطوري Apple.
- منشئات النتائج — Swift Evolution.
- القيم الوجودية المفتوحة ضمنيًا — Swift Evolution.