امنح مكوّن SwiftUI عامًا مظهرًا افتراضيًا بإضافة مُهيّئ مقيّد بنوع محتوى محدد. احتفظ بمُهيّئ منفصل يستخدم بانيًا للتخصيص. يشرح هذا الدليل خطوة بخطوة كيفية إنشاء لوحة قابلة لإعادة الاستخدام لنافذة macOS، ومتى يمكن لاستدلال الوسائط الافتراضية في Swift 5.7 تبسيط الواجهة.
حدّد الاستدعاءين اللذين ينبغي أن يدعمهما المكوّن
مهمة القارئ واضحة: إنشاء لوحة لسطح المكتب تبدو مكتملة عند استدعائها بعنوان فقط، مع السماح لشاشة أخرى بتوفير محتواها الخاص. ينبغي أن يظل العنوان المحيط والتباعد والحد متسقة في الحالتين.
الواجهة المقصودة هي DesktopPanel(title: "Workspace") للعرض الافتراضي وDesktopPanel(title: "Workspace") { Text("Three sample items") } للمحتوى المخصّص. ينشئ هذان الاستدعاءان تخصيصين مختلفين للمكوّن العام نفسه.
تمثّل المعاملات العامة في Swift أنواعًا محددة. يتطلب قيد مثل Content: View المطابقة للبروتوكول؛ أما شرط تساوي النوع مثل Content == DefaultPanelContent فيختار نوعًا بعينه. يوثّق مرجع اللغة كلا الشرطين وحلّ التحميل الزائد للمُهيّئات عبر القيود. المعاملات والوسائط العامة
يمنح ذلك المكوّن مسؤوليتين واضحتين: يتولى body المظهر المشترك للوحة، ويحدّد المُهيّئ المحتوى الذي يوضع داخلها. يحصل المحتوى الافتراضي على نوع عرض مسمّى كي يتمكن المُهيّئ الملائم من الإشارة إلى تخصيص ثابت وواضح.
أنشئ مثالًا كاملًا لـ macOS
استخدم هدف تطبيق SwiftUI لـ macOS، مع هدف نشر macOS 11 أو أحدث ومصرّف Swift 5.7 أو أحدث لهذا الدليل. يتيح الحد الأدنى لإصدار macOS استخدام بنية App وWindowGroup الواردة أدناه؛ وتوثّق Apple WindowGroup بوصفها حاوية المشهد للتسلسل الهرمي لعروض النافذة. WindowGroup
استبدل نقطة دخول التطبيق المُنشأة تلقائيًا بهذا الملف المصدري الكامل، واحذف ContentView.swift المُنشأ تلقائيًا إذا كان يعرّف ContentView آخر. أبقِ إعلانًا واحدًا فقط للتطبيق باستخدام @main في الهدف. جميع القيم المعروضة أدناه أمثلة محلية اصطناعية.
import SwiftUI
struct DefaultPanelContent: View {
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Sample workspace")
.font(.headline)
Text("Choose a layout to begin.")
.foregroundColor(.secondary)
}
}
}
struct DesktopPanel<Content: View>: View {
private let title: String
private let content: Content
init(
title: String,
@ViewBuilder content: () -> Content
) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.title2)
Divider()
content
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.08))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1)
)
}
}
extension DesktopPanel where Content == DefaultPanelContent {
init(title: String) {
self.init(title: title) {
DefaultPanelContent()
}
}
}
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 20) {
DesktopPanel(title: "Default panel")
DesktopPanel(title: "Custom panel") {
HStack {
Text("Sample items")
Spacer()
Text("3")
.font(.headline)
}
Text("Layout: Compact")
.foregroundColor(.secondary)
}
}
.padding(24)
.frame(minWidth: 420, minHeight: 300)
}
}
@main
struct PanelExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
أنشئ هدف macOS وشغّله. النتيجة المتوقعة لوحتان لهما التنسيق الخارجي نفسه: تحتوي الأولى على نص مساحة العمل الافتراضي، وتحتوي الثانية على صف للعدد يتبعه وصف للتخطيط. لا تتوفر في هذه البيئة أداة ترجمة Swift أو حزمة Apple SDK، لذا أمكن فحص المثال لكن تعذّر ترجمته هنا.
تدعم إرشادات Apple للعروض المخصّصة هذا التركيب: عرّف نوعًا يطابق View، ونفّذ body، واجمع العروض المضمّنة والمخصّصة في تسلسل هرمي. وتوضح أيضًا أن المعدّلات تعيد عروضًا معدّلة، ولهذا ينبغي أن يحيط التنسيق المشترك بمحتوى اللوحة. تعريف عرض مخصّص
لماذا يحلّ المُهيّئ المقيّد النوع العام
يستقبل المُهيّئ المخصّص إغلاقًا يعيد Content. يؤدي استدعاء content() إلى إنشاء القيمة المخزّنة في اللوحة. يُستهلك الإغلاق أثناء التهيئة، لذا لا يخزّنه هذا التنفيذ ولا يتطلب @escaping.
في الاستدعاء بلا باني، يتيح الامتداد init(title:) تحديدًا عندما يكون Content هو DefaultPanelContent. يوفّر هذا الشرط معلومات النوع الناقصة. ثم يفوّض التنفيذ إلى مُهيّئ الباني، بحيث تبقى تهيئة الخصائص في موضع واحد.
تكون قيمة المحتوى الافتراضي الناتجة من النوع DesktopPanel<DefaultPanelContent>. ويُستدل على نوع محتوى اللوحة المخصّصة من إغلاقها. ولا يحتاج المستدعون إلى كتابة هذا النوع الذي قد يكون معقدًا.
من النتائج المفيدة أن النسخة الافتراضية مخصّصة لنوعها بالفعل. لا يمكنك إسناد لوحة ذات نوع محتوى مختلف إلى المتغير نفسه لمجرد أن القيمتين تعرضان لوحات. أضف فرعًا يستخدم بانيًا إلى العرض المحيط به إذا كانت الواجهة تحتاج إلى عروض بديلة.
تتيح سمة الباني الإغلاق المخصّص متعدد العبارات في المثال. فهي تسمح بمشاركة HStack وText التالية في المحتوى المُنشأ؛ لكنها لا تحل محل شرط Content: View الخاص بالمكوّن. توثّق Apple كيفية تعامل الباني مع التعبيرات المتعددة والمحتوى الشرطي. ViewBuilder
متى يمكن لمُهيّئ باني واحد ذي وسيطة افتراضية أن يحل محل التحميل الزائد؟
نفّذت Swift 5.7 الاقتراح SE-0347، الذي يسمح للتعبير الافتراضي المحدد لنوع الوسيطة المحذوفة باستدلال نوع عام. ويوفر ذلك بديلًا أقصر لهذا المكوّن. SE-0347: استدلال النوع من التعبيرات الافتراضية
في مصرّف Swift 5.7 أو أحدث، استبدل مُهيّئ الباني داخل DesktopPanel بالجزء التالي، واحذف الامتداد المقيّد بالكامل. أبقِ الاستيراد والخصائص المخزّنة وDefaultPanelContent وbody ورمز التطبيق من المثال الكامل.
// Replacement initializer inside DesktopPanel.
init(
title: String,
@ViewBuilder content: () -> Content = {
DefaultPanelContent()
}
) {
self.title = title
self.content = content()
}
يُستدل الآن على Content من DefaultPanelContent() عند حذف الإغلاق. وعند تقديم إغلاق، يُستدل عليه من تلك الوسيطة. هذا استدلال من تعبير افتراضي، وليس وسيطة نوع افتراضية في تعريف المكوّن.
يقيّد الاقتراح هذا الاستدلال: يجب أن يظهر المعامل العام المعني في موضع واحد فقط في قائمة الوسائط، كما يجب أن تستوفي شروط تساوي الأنواع المرتبطة به قواعده. يظهر Content هنا في وسيطة الباني وحدها. وقد تؤدي إعادة استخدامه في وسيطة مُهيّئ أخرى إلى إبطال هذا الأسلوب. التصميم التفصيلي لـ SE-0347
اختر التحميل الزائد عندما يحتاج المحتوى الافتراضي إلى مدخلات المُهيّئ. فعلى سبيل المثال، يمكن إنشاء عنوان فرعي افتراضي مشتق من title داخل init(title:) وتمريره إلى مُهيّئ الباني. لا يستطيع تعبير الوسيطة الافتراضية التقاط title المقدّمة كما لو كان إغلاقًا أُنشئ داخل جسم ذلك المُهيّئ.
| متطلبات المكوّن | الواجهة المناسبة |
|---|---|
| محتوى افتراضي ثابت ومصرّف Swift 5.7 أو أحدث | وسيطة باني افتراضية موجزة. |
| محتوى افتراضي يُنشأ من مدخلات مُهيّئ أخرى | استخدم مُهيّئًا مقيّدًا وأنشئ القيمة داخله. |
| عدة عروض افتراضية مسمّاة | استخدم تحميلات زائدة صريحة للمُهيّئ مع تسميات وقيود واضحة. |
| نوع مخزّن صريح للتخصيص الافتراضي | سمِّ DesktopPanel<DefaultPanelContent> أو عرّف اسمًا مستعارًا للنوع. |
لا تحاول التعبير عن هذه الواجهة بصيغة struct DesktopPanel<Content: View = DefaultPanelContent>. لا توفر قواعد صياغة المعاملات العامة في Swift الحالية صياغة النوع الافتراضي هذه. المساران المدعومان هنا هما التهيئة المقيّدة والاستدلال من التعبيرات الافتراضية. المعاملات والوسائط العامة
حافظ على الواجهة عبر إصدارات الباني
تميّز وثائق Apple الحالية بين Xcode 26 والإصدارات الأقدم وXcode 27 والإصدارات الأحدث. مع سلسلة الأدوات الأحدث، ينشئ ViewBuilder محتوى غير مرتبط بنوع محدد؛ وتقدّم Apple ContentBuilder، وهو اسم مستعار للنوع ViewBuilder، بوصفه الصياغة لهذا السلوك الموحّد. يعتمد هذا الاختلاف على سلسلة أدوات البناء، لا على هدف نشر التطبيق وحده. ViewBuilder، ContentBuilder
يحتفظ المثال بـ@ViewBuilder للتوافق مع سلاسل الأدوات الأقدم. عند اعتماد الصياغة الأحدث في Xcode 27 أو إصدار أحدث، غيّر سمة وسيطة المُهيّئ إلى @ContentBuilder. أبقِ Content: View: فما زالت اللوحة تتطلب محتوى يمكنها وضعه في تسلسل هرمي للعروض.
تجنّب كتابة التخصيص الافتراضي بصيغة نوع متداخل لمكدس ووسيلة تجميع للصفوف. توثّق ملاحظة الترحيل لدى Apple تغييرًا من TupleView إلى TupleContent في البواني متعددة التعبيرات، وقد يؤدي ذلك إلى كسر القيود التي تسمي هذه البنى المُنشأة. يقيّد المثال الامتداد بدلًا من ذلك بـDefaultPanelContent؛ ويبقى تخطيطه الداخلي ضمن body: some View. TN3211: حل حالات عدم التوافق في مصدر SwiftUI لـ State وContentBuilder
كما يسهّل هذا الغلاف المسمّى التعديلات المعتادة. فإضافة سطر ثالث إلى المحتوى الافتراضي تغيّر تنفيذه دون الحاجة إلى إعادة كتابة نوع محدد طويل في المُهيّئ الملائم.
شخّص العقد قبل تغيير التخطيط
إذا تعذّر ترجمة المكوّن، فحدّد أولًا الجزء الذي تنقصه معلومات النوع في الواجهة. لن يؤدي تغيير الحشو أو إحاطة كل شيء بمكدس آخر إلى حل غياب تخصيص عام.
- يتعذّر استدلال
Contentفي الاستدعاء بلا باني. تأكد من وجودinit(title:)المقيّد، أو من ترجمة المُهيّئ ذي القيمة الافتراضية باستخدام Swift 5.7 أو أحدث. - لا يمكن إسناد محتوى افتراضي محدد إلى
Content. تحقّق من أن الإسناد يحدث داخل الامتداد المقيّد. فالمطابقة لـViewوحدها لا تجعلDefaultPanelContentقابلة للاستبدال بأيContentممكن. - تُرفض تعبيرات مخصّصة متعددة. تأكد من أن وسيطة الإغلاق تحمل سمة الباني.
- يرفض نوع صريح المحتوى المخصّص. يثبّت التعليق التوضيحي
DesktopPanel<DefaultPanelContent>نوع المحتوى عمدًا. أزل هذا التعليق عندما ينبغي للاستدعاء استدلال تخصيص مختلف. - يكسر تحديث سلسلة الأدوات قيدًا قائمًا على الصفوف. استبدل الاعتماد على بنية الباني المُنشأة بعرض افتراضي مسمّى، أو اتبع توثيق Apple للترحيل الخاص بهذا القيد.
أخيرًا، اجعل عمل الباني خفيفًا. يُنفّذ في هذا التطبيق كلما هُيّئت اللوحة؛ وليس هناك ما يضمن تنفيذه مرة واحدة فقط. تشير Apple إلى أن SwiftUI قد يعيد تهيئة العروض، وتنصح بتجنب الأعمال الكبيرة في مُهيّئاتها. تعريف عرض مخصّص
استخدم المُهيّئ المقيّد نقطة بداية صريحة: فهو يُظهر التخصيص الافتراضي ويُبقي استدلال المحتوى المخصّص سهلًا. اعتمد المُهيّئ الافتراضي الواحد عندما تلائم متطلبات مصرّفه وقيود الاستدلال واجهة برمجة التطبيقات لديك. يتيح كلا النهجين للوحة macOS تولّي مظهرها المشترك، بينما يوفّر المستدعون المحتوى الذي يحتاجون إليه فقط.
المصادر
- المعاملات والوسائط العامة — Swift.org
- استدلال النوع من التعبيرات الافتراضية — Swift Evolution
- تعريف عرض مخصّص — وثائق مطوري Apple
- ViewBuilder — وثائق مطوري Apple
- ContentBuilder — وثائق مطوري Apple
- WindowGroup — وثائق مطوري Apple
- TN3211: حل حالات عدم التوافق في مصدر SwiftUI لـ State وContentBuilder — وثائق مطوري Apple