لاستقبال إكمالات مختلفة الأنواع في شاشة SwiftUI واحدة على macOS، امنح كل مكوّن محوّلًا عامًا أو إغلاقًا محدد النوع. لا تتيح Swift للشاشة المطابقة مع البروتوكول نفسه مرتين بنوعين مختلفين مرتبطين.
يبني هذا البرنامج التعليمي معاينة صغيرة لسطح المكتب تطبّق أزرارها عنوانًا اصطناعيًا وعددًا من الأعمدة. تتمثل المهمة في إبقاء عقد الإكمال لكل مكوّن خاضعًا لفحص المترجم، مع السماح لعرض أب واحد باستقبال القيمتين.
لماذا لا يمكن لنوع واحد اعتماد عقدَي إكمال؟
ينتمي النوع المرتبط في البروتوكول إلى المطابقة معه. إذا أعلن بروتوكول الإكمال عن نوع مرتبط اسمه Value، فعلى النوع المطابق أن يحدد اختيارًا ثابتًا لذلك النوع. إضافة طريقتين مثقلتين بالإكمال لا تنشئ مطابقتين مستقلتين. يشرح دليل لغة Swift كيفية اكتساب الأنواع المرتبطة لأنواعها الملموسة عند اعتماد بروتوكول. Generics — Associated Types.
على سبيل المثال، لا يمكن لشاشة اعتماد بروتوكول إكمال مرةً يكون فيها Value مساويًا لـ TitleChoice، ومرةً أخرى يكون فيها مساويًا لـ LayoutChoice. ولا يؤدي نقل التصريحات إلى امتدادات منفصلة إلى تغيير هذا القيد. تحافظ SE-0143 صراحةً على حظر تكرار المطابقة، بما في ذلك المطابقات الشرطية ذات الشروط المتباينة. Conditional conformances.
لا تلغي الأنواع المرتبطة الأساسية هذا الحد. فالصياغة CompletionHandling<TitleChoice> تعبّر عن قيد على نوع مرتبط؛ ولا تنشئ هوية بروتوكول منفصلة لكل وسيط. قدّمت SE-0346 صياغة هذا القيد في Swift 5.7. Lightweight same-type requirements for primary associated types.
ينقل التصميم المدعوم أدناه المطابقة إلى محوّل عام. يوجد تصريح مطابقة واحد، وينشئ التطبيق قيمًا من المحوّل بتخصيصات مختلفة للمكوّنين. ويمتلك الأب الحالة الناتجة دون اعتماد بروتوكول الإكمال.
ما تصميم الإكمال المناسب لمكوّنك؟
استخدم دليل القرار التالي قبل تغيير الواجهة. هذه توصيات تصميم مستمدة من قواعد الأنواع الموثقة، وليست متطلبات إضافية في لغة Swift.
| متطلب المكوّن | التصميم المقترح | ما يظل صريحًا |
|---|---|---|
| استدعاء واحد، مع واجهة تتحكم بها | إغلاق محدد النوع | نوع مدخل الاستدعاء |
| واجهة برمجة قائمة على بروتوكول | محوّل عام مطابق | البروتوكول ونوعه المرتبط |
| قناة أحداث واحدة تحمل بدائل متعددة مسماة | تعداد بقيم مرتبطة | كل حالة حدث يتعامل معها المستقبِل |
| عدة عمليات غير مترابطة ذات معانٍ مختلفة | بروتوكولات مسماة منفصلة | المتطلبات المميزة لكل عملية |
لنفترض في هذا المثال أن المكوّن القابل لإعادة الاستخدام يتوقع بالفعل معالجًا قائمًا على بروتوكول. يحافظ المحوّل العام على هذا الحد، ويُبقي شيفرة المعالجة في الشاشة بجوار تحديثات حالتها. إذا كان المكوّن يحتاج إلى استدعاء فقط ولا يتطلب بروتوكولًا، فالإغلاق هو التصميم الأبسط.
تعبر طريقة إكمال عامة عن وعد مختلف. يجب أن تعمل طريقة مثل complete<T>(_ value: T) مع كل نوع تسمح به قيودها. ولا تعبّر عن المتطلب الأضيق الذي يقضي بأن يسلّم كل مكوّن نوع نتيجة محددًا بعينه. تشرح وثائق الأدوية العامة في Swift كيفية عمل الدوال العامة مع الأنواع المحددة في مواقع استدعائها. Generics — Generic Functions.
أنشئ محوّل الإكمال المحدد النوع
استخدم هدف تطبيق macOS SwiftUI مع مترجم Swift 5.7 أو أحدث وهدف نشر macOS 12 أو أحدث لهذا المثال. يأتي متطلب المترجم من تصريح النوع المرتبط الأساسي. أما هدف النشر فهو حد أدنى محافظ لهذا البرنامج التعليمي، وليس ادعاءً بأن كل واجهة برمجة معروضة ظهرت لأول مرة في macOS 12.
تشكل الملفات الثلاثة أدناه المثال الكامل. أضفها إلى هدف التطبيق نفسه، واستبدل نقطة دخول التطبيق وعرض المحتوى المولّدين بحيث لا يحتوي الهدف إلا على تصريح واحد لـ @main. لا حاجة إلى حزم أو أصول أو ملفات نموذجية أو مدخلات خارجية.
أنشئ CompletionContracts.swift بهذه التصريحات:
import SwiftUI
struct TitleChoice {
let text: String
}
struct LayoutChoice {
let columns: Int
}
@MainActor
protocol CompletionHandling<Value> {
associatedtype Value
func complete(_ value: Value)
}
@MainActor
struct CompletionAdapter<Output>: CompletionHandling {
typealias Value = Output
private let receive: @MainActor (Output) -> Void
init(_ receive: @escaping @MainActor (Output) -> Void) {
self.receive = receive
}
func complete(_ value: Output) {
receive(value)
}
}
@MainActor
struct CompletionButton<Value, Handler: CompletionHandling>: View
where Handler.Value == Value {
let title: String
let value: Value
let handler: Handler
var body: some View {
Button(action: {
handler.complete(value)
}) {
Text(title)
}
}
}
يربط القيد Handler.Value == Value مخرجات المكوّن بالمدخلات التي يقبلها المعالج. لذلك يتطلب الزر الذي يحمل TitleChoice معالجًا يكون نوعه المرتبط TitleChoice. ويجعل الاسم المستعار الصريح للنوع نوع المحوّل المرتبط سهل المعاينة.
يخزّن المحوّل إغلاقه لاستدعائه لاحقًا، لذا يضع المُهيّئ السمة @escaping على ذلك الوسيط. وتتطلب وثائق الإغلاقات في Swift هذا التصريح عندما يمكن أن يتجاوز عمر الإغلاق مدة استدعاء الدالة التي تلقته. Closures — Escaping Closures.
يستخدم البروتوكول والمحوّل والعرض @MainActor، ويحافظ الاستدعاء المخزّن على هذا العزل في نوع الدالة. وبذلك يصبح سياق التنفيذ المقصود لواجهة المستخدم جزءًا من العقد. توثّق SE-0316 كلاً من عزل الممثل العام وأنواع الدوال المؤهلة بممثل عام. Global actors.
توثّق Apple أن Button يأخذ إجراءً وتسمية. يستدعي الإجراء هنا المعالج بالقيمة المحددة النوع مسبقًا للمكوّن؛ وتوفّر Text(title) التسمية المرئية. Button.
أوصل نوعَي النتيجة إلى شاشة SwiftUI واحدة
أنشئ ContentView.swift. لنتيجتي العنوان والتخطيط وجهتا حالة منفصلتان، ويحدّث كل إغلاق في المحوّل وجهته المطابقة فقط.
import SwiftUI
@MainActor
struct ContentView: View {
@State private var previewTitle = "Sample Board"
@State private var columnCount = 1
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Component Preview")
.font(.headline)
Text(previewTitle)
Text("Columns: \(columnCount)")
HStack {
CompletionButton(
title: "Apply title",
value: TitleChoice(text: "Pebble Board"),
handler: CompletionAdapter<TitleChoice> { choice in
previewTitle = choice.text
}
)
CompletionButton(
title: "Apply layout",
value: LayoutChoice(columns: 3),
handler: CompletionAdapter<LayoutChoice> { choice in
columnCount = choice.columns
}
)
}
}
.padding(24)
.frame(minWidth: 420, minHeight: 180)
}
}
أنشئ TypedCompletionsApp.swift ليكون نقطة الدخول الوحيدة للتطبيق:
import SwiftUI
@main
@MainActor
struct TypedCompletionsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
يحتفظ الأب بقيمه الأولية في خصائص @State خاصة. توثّق Apple أن SwiftUI يدير هذا التخزين ويحدّث العروض التابعة عند تغيّر القيم. وتشير وثائقها الحالية أيضًا إلى أن الإصدارات المبنية باستخدام Xcode 27 أو أحدث تستخدم ماكرو State بدلًا من ذلك؛ ويتبع أسلوب التصريح المعروض هنا الأساس الأقدم القائم على غلاف الخاصية. State.
يستضيف WindowGroup التسلسل الهرمي لعروض الشاشة. توثّق Apple تخزينًا مستقلًا للحالة لكل نافذة ينشئها التجميع، لذا تخص حالة هذا المثال كل نافذة معاينة. WindowGroup.
هناك نوعان متخصصان من المحوّل: CompletionAdapter<TitleChoice> وCompletionAdapter<LayoutChoice>. وكلاهما ناتج عن تصريح المحوّل العام الوحيد. لا يطابق ContentView إلا View؛ ويتلقى النتيجتين عبر الإغلاقين اللذين يوفّرهما.
تحقّق من السلوك وشخّص حالات عدم التطابق
لا يوجد مترجم Swift مثبت في بيئة البحث، لذا جرى فحص هذه الملفات دون ترجمتها أو تشغيلها. ابنِ هدف التطبيق على جهاز Mac، ثم استخدم الفحوص التالية للتحقق من السلوك المقصود للمثال.
- شغّل التطبيق. ينبغي أن تعرض المعاينة
Sample BoardوColumns: 1. - انقر
Apply title. ينبغي أن يصبح العنوانPebble Board، مع بقاء عدد الأعمدة دون تغيير. - انقر
Apply layout. ينبغي أن يصبح العددColumns: 3، مع بقاء العنوان المختار دون تغيير. - انقر أيًا من الزرين مجددًا. ينبغي أن يطبّق القيمة الاصطناعية نفسها مرة أخرى.
لمعاينة حد النوع، غيّر مؤقتًا تخصيص معالج الزر الأول إلى CompletionAdapter<LayoutChoice> واترك جسم إغلاقه فارغًا. أبقِ قيمة ذلك الزر من نوع TitleChoice. عندئذٍ يتطلب المكوّن تساوي نوعين غير متوافقين عبر Handler.Value == Value، لذا ينبغي أن تفشل الترجمة. أعد المعالج الأصلي بعد ذلك.
إذا أبلغ المترجم عن تكرار المطابقة، فابحث عن تصريح آخر يجعل النوع الاسمي نفسه يعتمد CompletionHandling. احتفظ بمطابقة المحوّل العام الوحيدة وأنشئ قيمة محوّل أخرى للمخرج الإضافي. ويظل تصريحي امتدادين مقيدين للمطابقة نفسها خاضعين لقيد Swift على تكرار المطابقة. Conditional conformances — Multiple conformances.
إذا رفض المترجم صياغة النوع المرتبط الأساسي للبروتوكول، فتحقق من إصدار المترجم. في المترجمات الأقدم، يمكن الإبقاء على تصميم المحوّل مع تصريح نوع مرتبط عادي. هذا مقطع بديل لتصريح البروتوكول وحده؛ اترك التصريحات الأخرى كما هي:
@MainActor
protocol CompletionHandling {
associatedtype Value
func complete(_ value: Value)
}
يزيل هذا التغيير اعتماد المثال على صياغة Swift 5.7. لكنه لا يلغي متطلب المترجم لـ @MainActor، التي أُدخلت في Swift 5.5، ولا يغيّر هدف النشر للبرنامج التعليمي. يستخدم المكوّن بالفعل عبارة where العامة، لذا لا يحتاج إلى صياغة النوع المرتبط الأساسي في موضع استخدامه. Lightweight same-type requirements، Global actors.
ما الذي يضمنه عقد الإكمال هذا؟
يضمن المثال تطابق نوع الحمولة عند حد كل مكوّن. يستدعي التنفيذ المعالج مرة واحدة لكل تفعيل للزر، وبصورة متزامنة. وهو لا ينفذ عملية غير متزامنة أو إلغاءً أو حفظًا أو دورة حياة لإكمال يُنفذ مرة واحدة فقط. تتطلب هذه السلوكيات منطقًا إضافيًا وعقدًا صريحًا.
يستخدم المثال أيضًا قيمًا محلية معروفة. إذا قبل مكوّن مستقبلي أعدادًا اعتباطية للأعمدة، فتحقق من صحة ذلك الإدخال قبل تحديث المعاينة وحدد كيفية الإبلاغ عن رفضه. تطابق نوع Swift وحده لا يضمن أن يكون العدد صحيحًا ومفيدًا للأعمدة.
لهذه المهمة المكتبية، استخدم محوّلًا واحدًا لكل نقطة نهاية محددة النوع، ودع الأب يمتلك الحالة الناتجة. يمنح ذلك المكوّنين عقدَي إكمال دقيقين من خلال تصريح مطابقة واحد قابل لإعادة الاستخدام، مع وضوح وجهة كل نتيجة عند موضع الاستدعاء.
المصادر
- Conditional conformances — Swift Evolution
- Lightweight same-type requirements for primary associated types — Swift Evolution
- Generics — The Swift Programming Language
- Closures — The Swift Programming Language
- Global actors — Swift Evolution
- Button — Apple Developer Documentation
- State — Apple Developer Documentation
- WindowGroup — Apple Developer Documentation