غالبًا ما يتعين علينا التعامل مع الجداول الثابتة ، ويمكن أن تكون إعدادات التطبيق لدينا ، وشاشات التفويض ، وشاشات "عنا" ، والعديد غيرها. لكن في كثير من الأحيان ، لا يطبق مطورو المبتدئون أي أنماط تطوير لهذه الجداول ، ويكتبون في صف واحد نظامًا غير قابل للتطوير وغير مرن.
حول كيف يمكنني حل هذه المشكلة - تحت الخفض.
عن ماذا تتحدث
قبل أن تحل مشكلة الجداول الثابتة ، يجب أن تفهم ما هي عليه. الجداول الثابتة هي الجداول التي تعرف فيها بالفعل عدد الصفوف والمحتوى الموجود فيها. أمثلة على الجداول المماثلة أدناه.

المشكلة
بادئ ذي بدء ، من المفيد تحديد المشكلة: لماذا لا يمكننا إنشاء ViewController ليكون UITableViewDelegate و UITableViewDatasource ونصف فقط جميع الخلايا التي تحتاجها؟ على الأقل - هناك 5 مشاكل في طاولتنا:
- من الصعب الحجم
- مؤشر يعتمد
- غير مرن
- عدم إعادة الاستخدام
- يتطلب الكثير من التعليمات البرمجية للتهيئة
الحل
تعتمد طريقة حل المشكلة على الأساس التالي:
- إزالة مسؤولية تكوين الجدول في فصل منفصل ( مُنشئ )
- مجمع مخصص على UITableViewDelegate و UITableViewDataSource
- توصيل الخلايا بالبروتوكولات المخصصة لإعادة استخدامها
- إنشاء نماذج البيانات الخاصة بك لكل جدول
أولاً أريد أن أوضح كيف يتم استخدام هذا في الممارسة العملية - ثم سأوضح كيف يتم تنفيذ كل شيء تحت غطاء محرك السيارة.
التنفيذ
تتمثل المهمة في إنشاء جدول يحتوي على خليتين نصيتين وخلية واحدة بينهما.
بادئ ذي
بدء ، قمت بإنشاء
TextTableViewCell العادية مع
UILabel .
بعد ذلك ، يحتاج كل UIViewController مع جدول ثابت إلى مُنشئه الخاص ، فلننشئه:
class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = <#type#> }
عندما ورثناه من
StaticConstructorContainer ، أولاً وقبل كل شيء ، فإن البروتوكول العام يتطلب منا كتابة النموذج (
ModelType ) - وهذا هو نوع نموذج الخلية الذي نحتاج أيضًا إلى إنشائه ، دعنا نفعل ذلك.
أنا استخدم التعداد لهذا ، لأنه أكثر ملاءمة لمهامنا وهنا تبدأ المتعة. سوف نملأ جدولنا بالمحتوى باستخدام بروتوكولات مثل:
Titled، Subtitled، Colored، Fonted ، وهكذا. كما يمكنك تخمين أن هذه البروتوكولات مسؤولة عن عرض النص. قل يتطلب بروتوكول بعنوان
العنوان: سلسلة؟ وإذا كانت خليتنا تدعم عرض
العنوان ، فستملؤه. لنرى كيف يبدو:
protocol Fonted { var font: UIFont? { get } } protocol FontedConfigurable { func configure(by model: Fonted) } protocol Titled { var title: String? { get } } protocol TitledConfigurable { func configure(by model: Titled) } protocol Subtitled { var subtitle: String? { get } } protocol SubtitledConfigurable { func configure(by model: Subtitled) } protocol Imaged { var image: UIImage? { get } } protocol ImagedConfigurable { func configure(by model: Imaged) }
وفقًا لذلك ، يتم تقديم جزء صغير فقط من هذه البروتوكولات هنا ، يمكنك إنشائها بنفسك ، كما ترى - إنها بسيطة جدًا. أذكرك أننا نقوم بإنشائها مرة واحدة لغرض واحد ثم ننساهم ونستخدمها بهدوء.
تدعم خليتنا (
مع النص ) بشكل أساسي الأشياء التالية: خط النص ، والنص نفسه ، ولون النص ، ولون الخلفية للخلية ، وأي الأشياء التي تتبادر إلى الذهن عمومًا.
نحن بحاجة فقط
اللقب حتى الآن. لذلك ، نحن نرث نموذجنا من العنوان. داخل النموذج في الحالة ، نشير إلى أنواع الخلايا التي سيكون لدينا.
enum CellModel: Titled { case firstText case emptyMiddle case secondText var title: String? { switch self { case .firstText: return " - " case .secondText: return " - " default: return nil } } }
نظرًا لعدم وجود تسمية في الوسط (الخلية الفارغة) ، يمكنك إرجاع لا شيء.
لقد انتهينا من الخلية C ويمكنك إدخالها في المنشئ الخاص بنا.
class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = CellModel var models: [CellModel]
وفي الواقع ، هذا هو كل رمز لدينا. يمكننا القول أن طاولتنا جاهزة. دعونا ملء البيانات ومعرفة ما يحدث.
أوه نعم ، لقد نسيت تقريبا. نحتاج إلى وراثة خليتنا من بروتوكول TitledConfigurable حتى يتمكن من إدراج عنوان في نفسه. تدعم الخلايا ارتفاعات ديناميكية أيضًا.
extension TextTableViewCell: TitledConfigurable { func configure(by model: Titled) { label.text = model.title } }
كيف يبدو المنشئ المملوء:
class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = CellModel var models: [CellModel] = [.firstText, .emptyMiddle, .secondText] func cellType(for model: CellModel) -> StaticTableViewCellClass.Type { switch model { case .emptyMiddle: return EmptyTableViewCell.self case .firstText, .secondText: return TextTableViewCell.self } } func configure(cell: UITableViewCell, by model: CellModel) { cell.selectionStyle = .none } func itemSelected(item: CellModel) { switch item { case .emptyMiddle: print(" ") default: print(" ...") } } }
يبدو المدمجة جدا ، أليس كذلك؟
في الواقع ، فإن آخر شيء بقي لنا القيام به هو توصيل كل شيء إلى ViewController'e:
class ViewController: UIViewController { private let tableView: UITableView = { let tableView = UITableView() return tableView }() private let constructor = ViewControllerConstructor() private lazy var delegateDataSource = constructor.delegateDataSource() override func viewDidLoad() { super.viewDidLoad() constructor.setup(at: tableView, dataSource: delegateDataSource) } }
كل شيء جاهز ، علينا أن نجعل
delegateDataSource كخاصية منفصلة في فئتنا حتى لا ينقطع الرابط الضعيف داخل أي وظيفة.
يمكننا الركض والاختبار:

كما ترون ، كل شيء يعمل.
الآن دعونا نلخص ونفهم ما حققناه:
- إذا أنشأنا خلية جديدة وأردنا استبدال الخلية الحالية بها ، فإننا نقوم بذلك عن طريق تغيير متغير واحد. لدينا نظام جدول مرن للغاية
- نعيد استخدام كل الخلايا. كلما زاد عدد الخلايا التي تربطها بهذا الجدول ، أصبح العمل به أسهل وأكثر سهولة. عظيم للمشاريع الكبيرة.
- لقد قمنا بتقليل مقدار الكود لإنشاء الجدول. وسيتعين علينا كتابتها بشكل أقل عندما يكون لدينا العديد من البروتوكولات والخلايا الساكنة في المشروع.
- جلبنا بناء الجداول الثابتة من UIViewController إلى المنشئ
- لقد توقفنا عن الاعتماد على المؤشرات ، يمكننا تبديل الخلايا في الصفيف بأمان ولن ينهار المنطق.
رمز لمشروع اختبار في نهاية المقال.
كيف يعمل من الداخل الى الخارج؟
كيف تعمل البروتوكولات التي ناقشناها بالفعل. نحن الآن بحاجة إلى فهم كيفية عمل المنشئ بأكمله والفئات المرتبطة به.
لنبدأ مع المنشئ نفسه:
protocol StaticConstructorContainer { associatedtype ModelType var models: [ModelType] { get } func cellType(for model: ModelType) -> StaticTableViewCellClass.Type func configure(cell: UITableViewCell, by model: ModelType) func itemSelected(item: ModelType) }
هذا بروتوكول مشترك يتطلب وظائف مألوفة لنا بالفعل.
الأكثر إثارة للاهتمام هو
امتداده :
extension StaticConstructorContainer { typealias StaticTableViewCellClass = StaticCell & NibLoadable func delegateDataSource() -> StaticDataSourceDelegate<Self> { return StaticDataSourceDelegate<Self>.init(container: self) } func setup<T: StaticConstructorContainer>(at tableView: UITableView, dataSource: StaticDataSourceDelegate<T>) { models.forEach { (model) in let type = cellType(for: model) tableView.register(type.nib, forCellReuseIdentifier: type.name) } tableView.delegate = dataSource tableView.dataSource = dataSource dataSource.tableView = tableView } }
وظيفة
الإعداد التي أطلقناها في ViewController لدينا تسجل جميع الخلايا بالنسبة لنا وتفوض
dataSource والمفوض .
و
delegateDataSource () يخلق لنا مجمّع
UITableViewDataSource و
UITableViewDelegate . دعونا ننظر في الأمر:
class StaticDataSourceDelegate<Container: StaticConstructorContainer>: NSObject, UITableViewDelegate, UITableViewDataSource { private let container: Container weak var tableView: UITableView? init(container: Container) { self.container = container } func reload() { tableView?.reloadData() } func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { let type = container.cellType(for: container.models[indexPath.row]) return type.estimatedHeight ?? type.height } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let type = container.cellType(for: container.models[indexPath.row]) return type.height } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return container.models.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let model = container.models[indexPath.row] let type = container.cellType(for: model) let cell = tableView.dequeueReusableCell(withIdentifier: type.name, for: indexPath) if let typedCell = cell as? TitledConfigurable, let titled = model as? Titled { typedCell.configure(by: titled) } if let typedCell = cell as? SubtitledConfigurable, let subtitle = model as? Subtitled { typedCell.configure(by: subtitle) } if let typedCell = cell as? ImagedConfigurable, let imaged = model as? Imaged { typedCell.configure(by: imaged) } container.configure(cell: cell, by: model) return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let model = container.models[indexPath.row] container.itemSelected(item: model) } }
أعتقد أنه لا توجد أسئلة حول وظائف
heightForRowAt و
numberOfRowsInSection و
didSelectRowAt ، فهم يقومون فقط بتنفيذ وظائف واضحة. الطريقة الأكثر إثارة للاهتمام هنا هي
cellForRowAt .
في ذلك ، نحن لا ننفذ المنطق الأكثر جمالا. نحن مضطرون إلى كتابة كل بروتوكول جديد للخلايا هنا ، لكننا نفعل ذلك مرة واحدة - لذلك ليس مخيفًا للغاية. إذا كان النموذج يتوافق مع البروتوكول ، تمامًا مثل خليتنا ، فسنقوم بتكوينه. إذا كان لدى أي شخص أفكار حول كيفية أتمتة هذا ، فسوف يسعدني الاستماع في التعليقات.
هذا ينتهي المنطق. لم أتطرق إلى دروس النفعية التابعة لجهة خارجية في هذا النظام ،
يمكنك قراءة الكود الكامل هنا .
شكرا لاهتمامكم!