لتخزين أشكال SwiftUI مختلفة في مجموعة واحدة مع فرض واجهة مشتركة، استخدم غلافًا ملموسًا يطابق Hashable مع مُهيِّئ مقيّد ببروتوكول. يقدّم هذا الدليل لوحة أشكال صغيرة لنظام Mac، مع قواعد صريحة للتكرار وعمليات قابلة للاستدعاء على الأشكال.
المهمة محددة: اجمع مكوّنات قرصية وبلاطية اصطناعية، وارفض القيم غير المرتبطة وقت الترجمة، وأزل التكرارات، واعرض المكوّنات المتبقية في نافذة SwiftUI. تقبل المجموعة أنواعًا متعددة من المكوّنات عبر بروتوكول مشترك يوسّع بروتوكول Shape في SwiftUI.
لماذا لا تفي مجموعة من قيم البروتوكول بهذا العقد؟
تشترط Set في Swift أن يطابق نوع عناصرها Hashable. إن التصريح بأن كل قيمة داخل كيان وجودي تطابق Hashable لا يمنح ذلك النوع الوجودي مطابقة خاصة به. لذلك، لا تصبح Set<any PaletteShape> صالحة لمجرد أن PaletteShape يرث Hashable. توثّق Apple قيد عناصر المجموعة، بينما يشرح Swift Evolution الفرق بين حاوية كيان وجودي والقيمة المطابقة للبروتوكول التي تحتويها. Set، Implicitly Opened Existentials.
يمكن لكيان وجودي مثل any PaletteShape أن يحتوي أنواعًا مختلفة مطابقة للبروتوكول. تكمن الصعوبة في المساواة: إذ يقارن الشرط Equatable الموروث قيمتين من نوع Self نفسه، بينما قد تحتوي حاويتان كيانيتان وجوديتان نوعين ملموسين مختلفين. ولا يوفّر الدعم الموسّع في Swift للقيم الوجودية عملية مساواة عامة لهذه الحاويات. Unlock existentials for all protocols.
تعالج هذه البدائل جوانب مختلفة من المشكلة:
| التمثيل | أنواع ملموسة مختلطة؟ | هل تُفرض واجهة مشتركة؟ | مناسب كعنصر مجموعة؟ |
|---|---|---|---|
[any PaletteShape] |
نعم | نعم | لا تضمن المصفوفة تفرد العناصر كما تفعل المجموعة |
Set<S> مع S: PaletteShape |
نوع S محدد |
نعم | نعم، لهذا النوع الملموس الواحد |
Set<AnyHashable> |
نعم | لا يوجد قيد على الأشكال | نعم |
Set<AnyPaletteShape> أدناه |
نعم، داخل الأغلفة | نعم، عند الإنشاء | نعم |
يوفّر AnyHashable مساواة وتجزئة مع محو النوع، لكن مُهيِّئه يقبل عمومًا قيمًا قابلة للتجزئة. لذلك يمكن لمجموعة تحتويه وحده أن تقبل سلسلة نصية أو عددًا صحيحًا. يقيّد الغلاف المخصص أدناه الإدخال ببروتوكول الأشكال في التطبيق، ويحتفظ بواجهة الشكل إلى جانب القيمة القابلة للتجزئة ذات النوع الممحو. AnyHashable.
ما الذي ينبغي اعتباره شكلًا مكررًا؟
حدّد المساواة قبل تنفيذ الغلاف. في هذه اللوحة، يُعد المكوّنان مكررين إذا كانا من النوع الملموس نفسه وكانت القيم المخزنة فيهما متساوية. يظل القرص الموسوم Sample والبلاط الموسوم Sample مدخلين منفصلين. ويندمج قرصان يحملان التسمية نفسها في مدخل واحد.
التسمية هي الخاصية المخزنة الوحيدة في كل مكوّن نموذجي، لذا تشملها المساواة والتجزئة المُنشأتان تلقائيًا. هذا اختيار توضيحي في تصميم النموذج: التشابه البصري وحده لا يحدد المساواة. توثّق Apple الإنشاء التلقائي لمطابقة Hashable للبنى التي تكون خصائصها المخزنة قابلة للتجزئة، وتشترط أن تمرّر القيم المتساوية المكوّنات نفسها إلى أدوات التجزئة. Hashable.
يضمّن الغلاف أيضًا معرّف النوع في كل من المساواة والتجزئة. وهذا يجعل قاعدة «النوع نفسه» صريحة بدلًا من الاعتماد على افتراضات بشأن المقارنات بعد محو النوع. يستطيع AnyHashable مقارنة بعض القيم ذات الأنواع الأساسية المختلفة باستخدام تمثيلات معيارية؛ لكن Apple لا تعرّفه بوصفه فاصلًا مطلقًا بحسب النوع الملموس. يتيح ObjectIdentifier التعامل مع الأنواع الفوقية، ما يمكّن هذا المثال من تحديد نوع الإدخال العام. AnyHashable، ObjectIdentifier.
أنشئ مثال لوحة Mac كاملًا
استخدم مشروع تطبيق SwiftUI لنظام macOS في Xcode مع مترجم Swift 5.7 أو أحدث. ولخط أساس قابل لإعادة الإنتاج، اختر وضع لغة Swift 5 وهدف نشر macOS 13.0 أو أحدث. اختيار هدف النشر تحفظي لهذا الدليل، ولا يعني أن كل واجهة برمجية أدناه تتطلب macOS 13.
الحد الأدنى لإصدار المترجم مهم لأن المثال يخزّن any PaletteShape، الذي تشمل بروتوكولاته الموروثة متطلبات الأنواع المرتبطة وSelf. يسجل SE-0309 إتاحة دعم الكيانات الوجودية الموسّع هذا في Swift 5.7. ولا يتطلب المثال ميزة أحدث لمطابقة الكيانات الوجودية المعممة. Unlock existentials for all protocols.
استبدل تعريف التطبيق وعرض المحتوى اللذين أنشأهما المشروع تلقائيًا بملف المصدر الكامل التالي. أبقِ على تصريح واحد فقط لـ@main في هدف التطبيق. يتضمن المثال تعريفات المكوّنات والغلاف والقيم النموذجية والفحوص والعرض ونقطة دخول التطبيق. Swift غير متاح في بيئة البحث، لذا جرت مراجعة هذا الرمز، لكنه لم يُترجم أو يُنفذ هنا.
import SwiftUI
protocol PaletteShape: Shape, Hashable {
var label: String { get }
}
struct Disk: PaletteShape {
let label: String
func path(in rect: CGRect) -> Path {
Path(ellipseIn: rect)
}
}
struct Tile: PaletteShape {
let label: String
func path(in rect: CGRect) -> Path {
var result = Path()
result.addRect(rect)
return result
}
}
struct AnyPaletteShape: Hashable {
private let base: any PaletteShape
private let typeID: ObjectIdentifier
private let key: AnyHashable
init<S: PaletteShape>(_ value: S) {
base = value
typeID = ObjectIdentifier(S.self)
key = AnyHashable(value)
}
var label: String { base.label }
func path(in rect: CGRect) -> Path {
base.path(in: rect)
}
static func == (
lhs: AnyPaletteShape,
rhs: AnyPaletteShape
) -> Bool {
lhs.typeID == rhs.typeID && lhs.key == rhs.key
}
func hash(into hasher: inout Hasher) {
hasher.combine(typeID)
hasher.combine(key)
}
}
func makePalette() -> Set<AnyPaletteShape> {
var result: Set<AnyPaletteShape> = [
AnyPaletteShape(Disk(label: "Disk")),
AnyPaletteShape(Tile(label: "Tile"))
]
let duplicate = result.insert(
AnyPaletteShape(Disk(label: "Disk"))
)
precondition(!duplicate.inserted)
precondition(result.count == 2)
precondition(result.contains(
AnyPaletteShape(Tile(label: "Tile"))
))
let sameLabel: Set<AnyPaletteShape> = [
AnyPaletteShape(Disk(label: "Sample")),
AnyPaletteShape(Tile(label: "Sample"))
]
precondition(sameLabel.count == 2)
return result
}
struct ContentView: View {
private let shapes = makePalette()
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Shape Palette")
.font(.title)
ForEach(
shapes.sorted { $0.label < $1.label },
id: \.self
) { item in
HStack(spacing: 16) {
GeometryReader { geometry in
item.path(in: CGRect(
origin: .zero,
size: geometry.size
))
.fill(Color.blue)
}
.frame(width: 56, height: 56)
Text(item.label)
}
}
}
.padding(24)
.frame(minWidth: 300, minHeight: 220)
}
}
@main
struct ShapePaletteApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
يرث PaletteShape بروتوكول Shape الفعلي في SwiftUI. ويوفّر القرص والبلاط path(in:)، وهو متطلب الرسم الذي توثّقه Apple. تستخدم تطبيقاتهما عمليات Path موثّقة لإنشاء قطع ناقص ومستطيل. يجعل إطار العرض المربع القطع الناقص يبدو دائريًا في المثال. Shape، Path.
المُهيِّئ المقيّد هو قاعدة الإدخال: يقبل init<S: PaletteShape> الأنواع المطابقة للبروتوكول المشترك فقط. والخصائص المخزنة خاصة، لذا لا يستطيع المستدعون استبدال الشكل المحتفَظ به أو مفتاح المساواة الخاص به كلًّا على حدة. ينبغي أن ينتج AnyPaletteShape("Disk") خطأ مطابقة وقت الترجمة لأن String لا يطابق PaletteShape؛ لذا أبقِ هذا الرفض المتعمد خارج الملف القابل للتشغيل.
مع أن للمجموعة نوع عنصر ملموسًا واحدًا، يمكن لكل غلاف أن يحتفظ بشكل ملموس مختلف. وتُمرّر العضوان label وpath(in:) العمليات المطلوبة لهذه اللوحة. يصرّح الغلاف نفسه بمطابقة Hashable، لكنه لا يصرّح بمطابقة Shape.
كيف تتحقق من الإدراج والعضوية وترتيب العرض؟
شغّل التطبيق ببنية تصحيح الأخطاء. تعبّر الفحوص المضمّنة عن ثلاث نتائج متوقعة: يُرجع إدراج قرص ثانٍ مساوٍ القيمة inserted على أنها false، ويعثر فحص العضوية على بلاط مساوٍ، وتبقى أنواع المكوّنات المختلفة حتى عندما تتطابق تسمياتها. توثّق Apple أن insert(_:) تُرجع علامة للإدراج والعضو الناتج معًا. Set.
تحتوي النافذة المتوقعة على صفّين، Disk ثم Tile، برسوم زرقاء. والفرز عملية عرض منفصلة لأن ترتيب عناصر المجموعة غير محدد. وتفي المصفوفة الناتجة أيضًا بمتطلب ForEach لمجموعة تدعم الوصول العشوائي؛ إذ يستخدم id: \.self القيمة القابلة للتجزئة للغلاف معرّفًا للصف. Set، ForEach.
يكفي هذا الترتيب للقيم المقدّمة لأن تسمياتها مختلفة. إذا اشتركت مكوّنات مستقبلية في تسميات، فأضف حقلًا ثانويًا يحدد ترتيبًا حتميًا. وإذا أصبحت الصفوف قابلة للتحرير وكان ينبغي أن تحتفظ بهويتها عند تغيير التسميات، فأضف معرّفًا ثابتًا وحدد بشكل منفصل ما إذا كان هذا المعرّف يشارك في مساواة المجموعة.
لا تتوقع أن يؤدي إدراج عنصر في المجموعة إلى استبدال حمولة مكوّن موجود مساوٍ له. عندما يكون الاستبدال هو العملية المطلوبة، توفّر Apple update(with:). ويهم هذا التمييز إذا استبعد نموذج لاحق خصائص العرض من المساواة؛ فقد يؤدي إدراج قيمة مساوية إلى الاحتفاظ بالعضو الموجود بدلًا من تحديث مظهره. Set.
ما حدود الغلاف؟
التخزين القابل للتجزئة لا يحتفظ بكل إمكانات SwiftUI
يمرّر الغلاف إنشاء المسار للوحة ثابتة. لكنه لا يمرّر بيانات التحريك للشكل الأساسي أو عمليات إدراجه أو كل أعضاء View الموروثين. لذا فإن رسم المسار المُعاد لا ينشئ محوّلًا للشكل يحافظ على الحركة. يرث Shape في SwiftUI أيضًا Animatable وView؛ ولا تعيد إعادة تنفيذ طريقة رسم واحدة تمريرها إنشاء ذلك العقد كاملًا. Shape.
إذا احتاجت واجهة برمجية أخرى إلى شكل ملموس محوّل النوع، توفّر SwiftUI AnyShape، المتاح على macOS 13 والإصدارات الأحدث. ويتمثل دوره الموثّق في محو النوع الملموس للشكل، ولا تشمل مطابقاته المدرجة Hashable. ويمكنه معالجة حدّ العرض، لكنه لا يوفّر سياسة المساواة أو عنصر المجموعة المقيّد بالبروتوكول اللذين بُنيا هنا. AnyShape.
دعم المترجم الأحدث لا يلغي قيد عنصر المجموعة
يسمح SE-0352، المنفذ في Swift 5.7، بفتح النوع الأساسي لكيان وجودي لاستدعاءات عامة مناسبة. ويسجل نصه الحالي أيضًا سلوك وضع لغة Swift 6. فتح قيمة لاستدعاء لا يجعل any PaletteShape نفسه نوعًا يطابق Hashable ويصلح لمعامل عنصر المجموعة. أبقِ الغلاف الملموس عند استخدام هذا التصميم عبر أجيال المترجم. Implicitly Opened Existentials.
يتطلب توسيع النموذج قرارًا بشأن المساواة
يستخدم المثال بنى غير قابلة للتغيير. عند إضافة أبعاد أو إعدادات مخزنة أخرى، قرر ما إذا كان تغييرها ينشئ عضوًا مميزًا في المجموعة. ستشمل التجزئة المُنشأة تلقائيًا الخصائص المخزنة الإضافية القابلة للتجزئة. وفي المساواة المخصصة، جزّئ المكوّنات الأساسية نفسها تمامًا وبالترتيب نفسه؛ فتطابق قيم التجزئة وحده لا يثبت المساواة أبدًا. Hashable.
إذا كان للتطبيق مجموعة ثابتة دائمًا من متغيرات المكوّنات، فإن التعداد ذي القيم المرتبطة القابلة للتجزئة تصميم معقول آخر؛ فهو يجمع الحالات المدعومة في مكان واحد. ويكون الغلاف مفيدًا عندما يُفترض أن تدخل أنواع مكوّنات مطابقة إضافية عبر المُهيِّئ نفسه دون توسيع قائمة حالات مركزية. ويتوقف هذا الاختيار على احتياجات التطبيق من التوسعة.
في لوحة Mac هذه، يضع التنفيذ مكانًا ملموسًا لكل متطلب: يحدد البروتوكول الأشكال المقبولة، ويفرض المُهيِّئ قواعد الإدخال، وتعرّف المساواة التكرارات، ويوفّر تمرير المسار الرسم. أضف مكوّنات جديدة غير قابلة للتغيير ومطابقة للبروتوكول بلفّها عند الإدراج، ثم وسّع الفحوص كلما تغيّر معنى التكرار.
المصادر
- Set — Apple
- AnyHashable — Apple
- Hashable — Apple
- ObjectIdentifier — Apple
- Shape — Apple
- Path — Apple
- ForEach — Apple
- AnyShape — Apple
- Unlock existentials for all protocols — Swift Evolution
- Implicitly Opened Existentials — Swift Evolution