مرة واحدة ، واجهت أنا (حسنًا ، ولا أنا حتى) مهمة إضافة خلية واحدة من نوع مختلف تمامًا إلى
UICollectionView
مع نوع معين من الخلايا ، وللقيام بذلك فقط في حالة خاصة ، تتم معالجتها "أعلاه" ولا تعتمد بشكل مباشر على
UICollectionView
. أدت هذه المهمة ، إذا كانت ذاكرتي
UICollectionViewDataSource
، لبضعة قبيحة
if
- else
كتل داخل
UICollectionViewDelegate
و
UICollectionViewDelegate
، التي استقرت بأمان في رمز "الإنتاج" ، وربما لن تذهب إلى أي مكان من هناك.
في إطار المهمة المذكورة أعلاه ، لم يكن هناك أي معنى في التفكير في أي حل أكثر أناقة ، أو إضاعة طاقة "مدروسة" في هذا الصدد. ومع ذلك ، تذكرت هذه القصة: فكرت في محاولة تنفيذ كائن "مصدر بيانات" معين ، والذي يمكن أن يتكون من أي عدد من كائنات "مصدر البيانات" الأخرى في كلي واحد. من الواضح أن الحل يجب تعميمه ومناسب لأي عدد من المكونات (بما في ذلك صفر وواحد) وألا يعتمد على أنواع محددة. اتضح أن هذا ليس حقيقيًا فحسب ، ولكنه أيضًا ليس صعبًا للغاية (على الرغم من أن جعل الكود "جميلًا" أكثر صعوبة أيضًا).
UITableView
ما فعلت مع مثال
UITableView
. إذا كنت ترغب في ذلك ، فإن كتابة رمز مشابه لـ
UICollectionView
يجب ألا يكون صعباً.
"الفكرة دائماً أهم من تجسيدها"
ينتمي هذا القول المأثور إلى مؤلف الكتاب الهزلي الكبير آلان مور ( "حراس المرمى" ، و "يرمز V إلى الثأر " ، و "رابطة السادة البارزين" ) ، لكن الأمر لا يتعلق بالبرمجة ، أليس كذلك؟الفكرة الرئيسية في مقالي هي تخزين مجموعة من كائنات
UITableViewDataSource
وإرجاع العدد الإجمالي للأقسام وتكون قادرة على تحديد عند الوصول إلى القسم أي من الكائنات "مصدر البيانات" الأصلية ستعيد توجيه هذه المكالمة.
يحتوي بروتوكول
UITableViewDataSource
بالفعل على الأساليب اللازمة للحصول على عدد الأقسام والصفوف ، وما إلى ذلك ، ولكن ، لسوء الحظ ، في هذه الحالة ، وجدت أنه غير مريح للغاية في الاستخدام بسبب الحاجة إلى تمرير مرجع إلى مثيل محدد من
UITableView
كأحد الوسيطات. لذلك ، قررت تمديد بروتوكول
UITableViewDataSource
القياسي مع بضعة أعضاء بسيطين إضافيين:
protocol ComposableTableViewDataSource: UITableViewDataSource { var numberOfSections: Int { get } func numberOfRows(for section: Int) -> Int }
وقد تبين أن "مصدر البيانات" المركب عبارة عن فئة بسيطة تنفذ متطلبات
UITableViewDataSource
وتتم تهيئتها باستخدام وسيطة واحدة فقط - مجموعة من الحالات المحددة لـ
ComposableTableViewDataSource
:
final class ComposedTableViewDataSource: NSObject, UITableViewDataSource { private let dataSources: [ComposableTableViewDataSource] init(dataSources: ComposableTableViewDataSource...) { self.dataSources = dataSources super.init() } private override init() { fatalError("\(#file) \(#line): Initializer with parameters must be used.") } }
الآن يبقى فقط كتابة تطبيقات جميع أساليب بروتوكول
UITableViewDataSource
بحيث تشير إلى أساليب المكونات المقابلة.
لقد كان القرار الصحيح. قراري
تنتمي هذه الكلمات إلى بوريس نيكولايفيتش يلتسين ، أول رئيس للاتحاد الروسي ، ولا تشير حقًا إلى النص أدناه ، لقد أعجبتني فقط.بدا لي القرار الصحيح لاستخدام
وظيفة لغة
سويفت ، واتضح أنها مريحة حقًا.
أولاً ، نطبق طريقة تُرجع عدد الأقسام - هذا ليس بالأمر الصعب. كما ذكر أعلاه ، نحتاج فقط إلى العدد الإجمالي لجميع أقسام المكونات:
func numberOfSections(in tableView: UITableView) -> Int {
(لن أشرح بناء الجملة والمعنى للوظائف القياسية. إذا لزم الأمر ، فإن الإنترنت مليء
بالمقالات التمهيدية الجيدة حول الموضوع . ويمكنني أيضًا أن أوصي
بكتاب جيد .)
إلقاء نظرة سريعة على جميع أساليب
UITableViewDataSource
، ستلاحظ أنها
IndexPath
تقبل فقط ارتباطًا بالجدول وقيمة رقم القسم أو صف
IndexPath
المطابق. سوف نكتب بعض المساعدين التي ستكون مفيدة لنا في تنفيذ جميع أساليب البروتوكول الأخرى.
أولاً ، يمكن تقليل كافة المهام إلى وظيفة
"عامة" ، والتي تأخذ
ComposableTableViewDataSource
كمرجع إلى
ComposableTableViewDataSource
معيّنة وقيمة رقم القسم أو
IndexPath
. للراحة والإيجاز ، نقوم بتعيين
أسماء مستعارة لأنواع هذه الوظائف. بالإضافة إلى ذلك ، من أجل سهولة القراءة الإضافية ، أقترح إعلان اسم مستعار لرقم المقطع:
private typealias SectionNumber = Int private typealias AdducedSectionTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ sectionNumber: SectionNumber) -> T private typealias AdducedIndexPathTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ indexPath: IndexPath) -> T
(سأشرح الأسماء المحددة أدناه).
ثانياً ، نطبق دالة بسيطة تحدد
ComposableTableViewDataSource
و رقم القسم المقابل بواسطة رقم قسم
ComposedTableViewDataSource
:
private func decompose(section: SectionNumber) -> (dataSource: ComposableTableViewDataSource, decomposedSection: SectionNumber) { var section = section var dataSourceIndex = 0 for (index, dataSource) in dataSources.enumerated() { let diff = section - dataSource.numberOfSections dataSourceIndex = index if diff < 0 { break } else { section = diff } } return (dataSources[dataSourceIndex], section) }
ربما ، إذا كنت تعتقد أنك أطول بقليل من عملي ، فإن التطبيق سوف يكون أكثر أناقة وأقل وضوحًا. على سبيل المثال ، اقترح زملائي على الفور أن أقوم بتنفيذ
بحث ثنائي في هذه الوظيفة (سابقًا ، على سبيل المثال ، أثناء التهيئة ، عن طريق تكوين فهرس لعدد الأقسام - مجموعة بسيطة من
الأعداد الصحيحة ). أو حتى قضاء بعض الوقت في تجميع وتخزين جدول المراسلات بأرقام الأقسام ، ولكن بدلاً من استخدام الطريقة مع
تعقيد الوقت O (n) أو O (log n) ، يمكنك الحصول على النتيجة بتكلفة O (1). لكنني قررت أخذ نصيحة
دونالد نوث العظيم بعدم المشاركة في تحسين سابق لأوانه دون الحاجة الواضحة والقياسات المناسبة. نعم ، وليس عن هذا المقال.
وأخيرًا ، الدالات التي تقبل
AdducedSectionTask
و
AdducedIndexPathTask
المشار إليها أعلاه و "إعادة توجيهها" إلى مثيلات
ComposedTableViewDataSource
محددة:
private func adduce<T>(_ section: SectionNumber, _ task: AdducedSectionTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: section) return task(dataSource, decomposedSection) } private func adduce<T>(_ indexPath: IndexPath, _ task: AdducedIndexPathTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: indexPath.section) return task(dataSource, IndexPath(row: indexPath.row, section: decomposedSection)) }
والآن يمكنك شرح الأسماء التي اخترتها لهذه الوظائف كافة. إنها بسيطة: فهي تعكس أسلوب تسمية وظيفي. أي يعني قليلا حرفيا ، ولكن الصوت مثير للإعجاب.
تبدو الدالتان الأخيرتان مثل التوائم تقريبًا ، لكن بعد قليل من التفكير ، تخليت عن محاولة التخلص من الازدواجية في الكود ، لأنه جلب إزعاجًا أكثر من المزايا: اضطررت إلى إخراج أو تحويل وظائف التحويل إلى رقم القسم والعودة إلى النوع الأصلي. بالإضافة إلى ذلك ، فإن احتمال إعادة استخدام هذا النهج المعمم يميل إلى الصفر.
جميع هذه الاستعدادات والمساعدين تعطي ميزة لا تصدق في تنفيذ ، في الواقع ، من أساليب البروتوكول. طرق تكوين الجدول:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForHeaderInSection: $1) } } func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForFooterInSection: $1) } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return adduce(section) { $0.tableView(tableView, numberOfRowsInSection: $1) } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return adduce(indexPath) { $0.tableView(tableView, cellForRowAt: $1) } }
إدراج وحذف الصفوف:
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { return adduce(indexPath) { $0.tableView?(tableView, commit: editingStyle, forRowAt: $1) } } func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
بطريقة مماثلة ، يمكن تنفيذ دعم رؤوس فهرس الأقسام. في هذه الحالة ، بدلاً من رقم القسم ، يجب عليك العمل باستخدام فهرس الرأس. أيضًا ، على الأرجح ، سيكون من المفيد لهذا إضافة حقل إضافي إلى بروتوكول
ComposableTableViewDataSource
. تركت هذه القطعة خارج المادة.
"المستحيل اليوم سيكون ممكنا غدا"
هذه هي كلمات العالم الروسي كونستانتين إدواردوفيتش تسيولكوفسكي ، مؤسس علم الفضاء النظري.أولاً ، لا يدعم الحل المقدم سحب الصفوف وإفلاتها. تضمنت الخطة الأصلية دعمًا للسحب والإفلات داخل أحد كائنات "مصدر البيانات" التأسيسية ، ولكن لسوء الحظ ، لا يمكن تحقيق ذلك باستخدام
UITableViewDataSource
فقط. تحدد طرق هذا البروتوكول ما إذا كان من الممكن "سحب وإفلات" خط معين وتلقي "رد اتصال" في نهاية السحب. ويتم معالجة الحدث نفسه ضمن أساليب
UITableViewDelegate
.
ثانياً ، والأهم من ذلك ، من الضروري التفكير في آليات تحديث البيانات على الشاشة. أعتقد أنه يمكن تنفيذ ذلك من خلال الإعلان عن بروتوكول تفويض
ComposableTableViewDataSource
، والذي سيتم تطبيق أساليبه بواسطة
ComposedTableViewDataSource
وتلقي إشارة إلى أن "مصدر البيانات" الأصلي قد تلقى تحديثًا. يظل سؤالان مفتوحان: كيفية تحديد ما
ComposableTableViewDataSource
قد تم تغيير
ComposableTableViewDataSource
داخل
ComposedTableViewDataSource
وكيف تكون مهمة منفصلة وليس أكثرها تافهاً ، ولكن بعدد من الحلول (على سبيل المثال ،). وبالطبع ، ستحتاج إلى
ComposedTableViewDataSource
تفويض
ComposedTableViewDataSource
، وسيتم استدعاء الأساليب الخاصة به عند تحديث "مصدر البيانات" المركب ويتم تنفيذها بواسطة نوع العميل (على سبيل المثال ،
وحدة تحكم أو
نموذج عرض ).
آمل أن أستكشف هذه القضايا بشكل أفضل مع مرور الوقت وأن أغطيها في الجزء الثاني من المقال. في هذه الأثناء ، أنا مستمتع ، كنت فضولياً لقراءة هذه التجارب!
PS
في اليوم السابق ، كان عليّ الدخول في الكود المذكور في المقدمة لتعديله: كنت بحاجة إلى تبديل خلايا هذين النوعين. باختصار ، كان عليّ أن أعذّب نفسي وأن "أعيش"
Index out of bounds
تظهر باستمرار في أماكن مختلفة. عند استخدام الطريقة الموضحة ، سيكون من الضروري فقط تبديل كائنين "مصدر البيانات" في الصفيف الذي تم تمريره كوسيطة مُهيئ.
المراجع-
Playgroud مع رمز الكامل والمثال-
تويتر الخاص بي