هذا هو الجزء الأول من سلسلة من المقالات في مكتبة
ReactiveDataDisplayManager (RDDM) . في هذه المقالة ، سوف أصف المشكلات الشائعة التي يجب علي معالجتها عند العمل مع الجداول "العادية" ، بالإضافة إلى وصف RDDM.

المشكلة 1. UITableViewDataSource
بالنسبة للمبتدئين ، ننسى تخصيص المسؤوليات وإعادة الاستخدام والكلمات الرائعة الأخرى. دعونا نلقي نظرة على العمل المعتاد مع الجداول:
class ViewController: UIViewController { ... } extension ViewController: UITableViewDelegate { ... } extension ViewController: UITableViewDataSource { ... }
سنقوم بتحليل الخيار الأكثر شيوعا. ما الذي نحتاج إلى تنفيذه؟ بشكل صحيح ، عادةً ما يتم تطبيق 3 أساليب
UITableViewDataSource
:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int func numberOfSections(in tableView: UITableView) -> Int func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
في الوقت الحالي ، لن ننتبه إلى الطرق المساعدة (
numberOfSection
، وما إلى ذلك)
numberOfSection
func tableView(tableView: UITableView, indexPath: IndexPath)
الأكثر إثارة للاهتمام
func tableView(tableView: UITableView, indexPath: IndexPath)
لنفترض أننا نرغب في ملء جدول بالخلايا مع وصف للمنتجات ، فستبدو طريقتنا كما يلي:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { let anyCell = tableView.dequeueReusableCell(withIdentifier: ProductCell.self, for: indexPath) guard let cell = anyCell as? ProductCell else { return UITableViewCell() } cell.configure(for: self.products[indexPath.row]) return cell }
ممتاز ، هذا ليس بالأمر الصعب. الآن ، لنفترض أن لدينا عدة أنواع من الخلايا ، على سبيل المثال ، ثلاثة:
- المنتجات؛
- قائمة الأسهم
- الإعلان.
getCell
المثال ، نحصل على الخلية إلى طريقة
getCell
:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { switch indexPath.row { case 0: guard let cell: PromoCell = self.getCell() else { return UITableViewCell() } cell.configure(self.promo) return cell case 1: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.ad) return cell default: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.products[indexPath.row - 2]) return cell } }
بطريقة ما الكثير من التعليمات البرمجية. تخيل أننا نريد تعويض شاشة الإعدادات. ماذا سيكون هناك؟
- غطاء الخلية مع الصورة الرمزية ؛
- مجموعة من الخلايا ذات التحولات "في العمق" ؛
- خلايا ذات مفاتيح (على سبيل المثال ، تمكين / تعطيل الإدخال بواسطة رمز التعريف الشخصي) ؛
- خلايا تحتوي على معلومات (على سبيل المثال ، خلية سيكون بها هاتف أو بريد إلكتروني أو أي شيء) ؛
- العروض الشخصية.
علاوة على ذلك ، تم تعيين النظام. طريقة رائعة سوف تتحول ...
والآن حالة أخرى - هناك نموذج الإدخال. في نموذج الإدخال ، مجموعة من الخلايا المتطابقة ، كل منها مسؤولة عن حقل معين في نموذج البيانات. على سبيل المثال ، تكون الخلية التي تدخل الهاتف مسؤولة عن الهاتف وما إلى ذلك.
كل شيء بسيط ، ولكن هناك واحد "لكن". في هذه الحالة ، لا يزال يتعين عليك رسم حالات مختلفة ، لأنك تحتاج إلى تحديث الحقول الضرورية.
يمكنك الاستمرار في تخيل وتخيل تصميم Backend-Driven ، حيث نتلقى 6 أنواع مختلفة من حقول الإدخال ، واعتمادًا على حالة الحقول (الرؤية ، ونوع الإدخال ، والتحقق من الصحة ، والقيمة الافتراضية ، وما إلى ذلك) تتغير الخلايا كثيرًا بحيث تتغير خلاياها لا يمكن أن يؤدي إلى واجهة واحدة. في هذه الحالة ، ستبدو هذه الطريقة غير سارة للغاية. حتى لو قمت بتحليل التكوين إلى طرق مختلفة.
بالمناسبة ، بعد ذلك ، تخيل كيف سيبدو الكود الخاص بك إذا كنت تريد إضافة / إزالة الخلايا أثناء عملك. لن تبدو لطيفة جدًا نظرًا لحقيقة أننا
ViewController
إلى مراقبة تناسق البيانات المخزنة في
ViewController
وعدد الخلايا بشكل مستقل.
المشاكل:
- إذا كانت هناك خلايا من أنواع مختلفة ، فإن الرمز يصبح مثل المعكرونة ؛
- هناك العديد من المشكلات في التعامل مع الأحداث من الخلايا ؛
- رمز القبيح في حال كنت بحاجة إلى تغيير حالة الجدول.
مشكلة 2. MindSet
لم يحن الوقت بعد للكلمات الرائعة.
دعونا ننظر في كيفية عمل التطبيق ، أو بالأحرى ، كيف تظهر البيانات على الشاشة. نقدم دائمًا هذه العملية بالتتابع. حسنا ، أكثر أو أقل:
- الحصول على البيانات من الشبكة ؛
- لمعالجة
- عرض هذه البيانات على الشاشة.
ولكن هل هو حقا كذلك؟ لا! في الواقع ، نحن نفعل هذا:
- الحصول على البيانات من الشبكة ؛
- لمعالجة
- حفظ داخل نموذج ViewController ؛
- شيء يسبب تحديث الشاشة ؛
- يتم حفظ النموذج المحفوظ إلى خلايا.
- يتم عرض البيانات على الشاشة.
بالإضافة إلى الكمية ، لا تزال هناك اختلافات. أولاً ، لم نعد نخرج البيانات ؛ بل هو الإخراج. ثانياً ، هناك فجوة منطقية في عملية معالجة البيانات ، يتم حفظ النموذج وتنتهي العملية هناك. ثم يحدث شيء وتبدأ عملية أخرى. وبالتالي ، من الواضح أننا لا نضيف عناصر إلى الشاشة ، ولكننا نوفرها فقط (والتي ، بالمناسبة ، محفوفة بالمخاطر) عند الطلب.
وتذكر
UITableViewDelegate
، كما يتضمن طرقًا لتحديد ارتفاع الخلايا. عادة ما يكون
automaticDimension
كافياً ، لكن هذا في بعض الأحيان لا يكفي وتحتاج إلى ضبط الارتفاع بنفسك (على سبيل المثال ، في حالة الرسوم المتحركة أو الرؤوس)
بعد ذلك نشترك بشكل عام في إعدادات الخلية ، الجزء في تكوين الارتفاع هو في طريقة أخرى.
المشاكل:
- يتم فقد الاتصال الصريح بين معالجة البيانات وعرضه على واجهة المستخدم ؛
- تكوين الخلية ينقسم إلى أجزاء مختلفة.
فكرة
المشاكل المدرجة على الشاشات المعقدة تسبب صداعًا ورغبة حادة في تناول الشاي.
أولاً ، لا أريد تنفيذ أساليب التفويض باستمرار. الحل الواضح هو إنشاء كائن يقوم بتنفيذه. بعد ذلك سنفعل شيئًا مثل:
let displayManager = DisplayManager(self.tableView)
ممتاز. أنت الآن بحاجة إلى أن يكون الكائن قادرًا على العمل مع أي خلايا ، بينما يجب نقل تكوين هذه الخلايا في مكان آخر.
إذا وضعنا التكوين في كائن منفصل ، فسنقوم بتغليف (حان الوقت للكلمات الذكية) التكوين في مكان واحد. في هذا المكان نفسه ، يمكننا إخراج منطق تنسيق البيانات (على سبيل المثال ، تغيير تنسيق التاريخ ، تسلسل السلسلة ، وما إلى ذلك). من خلال نفس الكائن ، يمكننا الاشتراك في الأحداث في الخلية.
في هذه الحالة ، سيكون لدينا كائن له واجهتان مختلفتان:
- واجهة إنشاء مثيل
UITableView
مخصصة لـ DisplayManager. - التهيئة والاشتراك والتكوين واجهة - لمقدم أو ViewController.
نحن نسمي هذا الكائن مولد. بعد ذلك ، يعد المولد الخاص بنا للجدول عبارة عن خلية ولكل شيء آخر - طريقة لتقديم البيانات على واجهة المستخدم ومعالجة الأحداث.
ونظرًا لأن المولد يتم تغليفه الآن ، وأن المولد نفسه عبارة عن خلية ، يمكننا حل الكثير من المشكلات. بما في ذلك تلك المذكورة أعلاه.
تطبيق
public protocol TableCellGenerator: class { var identifier: UITableViewCell.Type { get } var cellHeight: CGFloat { get } var estimatedCellHeight: CGFloat? { get } func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell func registerCell(in tableView: UITableView) } public protocol ViewBuilder { associatedtype ViewType: UIView func build(view: ViewType) }
مع هذه التطبيقات ، يمكننا أن نجعل التنفيذ الافتراضي:
public extension TableCellGenerator where Self: ViewBuilder { func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: self.identifier.nameOfClass, for: indexPath) as? Self.ViewType else { return UITableViewCell() } self.build(view: cell) return cell as? UITableViewCell ?? UITableViewCell() } func registerCell(in tableView: UITableView) { tableView.registerNib(self.identifier) } }<source lang="swift">
سأقدم مثالا على مولد صغير:
final class FamilyCellGenerator { private var cell: FamilyCell? private var family: Family? var didTapPerson: ((Person) -> Void)? func show(family: Family) { self.family = family cell?.fill(with: family) } func showLoading() { self.family = nil cell?.showLoading() } } extension FamilyCellGenerator: TableCellGenerator { var identifier: UITableViewCell.Type { return FamilyCell.self } } extension FamilyCellGenerator: ViewBuilder { func build(view: FamilyCell) { self.cell = view view.selectionStyle = .none view.didTapPerson = { [weak self] person in self?.didTapPerson?(person) } if let family = self.family { view.fill(with: family) } else { view.showLoading() } } }
نحن هنا اختبأ كل من التكوين والاشتراكات. لاحظ أن لدينا الآن مكانًا يمكننا فيه تغليف الحالة (لأنه من المستحيل تغليف الحالة في الخلية لأنه يعاد استخدامها من قبل الجدول). وحصلوا أيضًا على فرصة لتغيير البيانات في الخلية "أثناء الطيران".
إيلاء الاهتمام ل
self.cell = view
. لقد تذكرنا خلية والآن يمكننا تحديث البيانات دون إعادة تحميل هذه الخلية. هذه هي ميزة مفيدة.
ولكن كنت مشتتا. نظرًا لأن لدينا أي خلية ممثلة بمولد ، يمكننا أن نجعل واجهة DisplayManager لدينا أكثر جمالًا بقليل.
public protocol DataDisplayManager: class { associatedtype CollectionType associatedtype CellGeneratorType associatedtype HeaderGeneratorType init(collection: CollectionType) func forceRefill() func addSectionHeaderGenerator(_ generator: HeaderGeneratorType) func addCellGenerator(_ generator: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType], after: CellGeneratorType) func addCellGenerator(_ generator: CellGeneratorType, after: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType]) func update(generators: [CellGeneratorType]) func clearHeaderGenerators() func clearCellGenerators() }
هذا في الواقع ليس كل شيء. يمكننا إدراج المولدات في الأماكن الصحيحة أو حذفها.
بالمناسبة ، إدخال خلية بعد خلية معينة يمكن أن يكون مفيدًا لعنة. خاصةً إذا قمنا بتحميل البيانات تدريجيًا (على سبيل المثال ، أدخل المستخدم رقم التعريف الشخصي ، فقد حمّلنا معلومات رقم التعريف الشخصي وعرضناه بإضافة عدة خلايا جديدة بعد حقل رقم التعريف الشخصي).
يؤدي
كيف سيبدو عمل الخلية الآن:
class ViewController: UIViewController { func update(data: [Products]) { let gens = data.map { ProductCellGenerator($0) } self.ddm.addGenerators(gens) } }
أو هنا:
class ViewController: UIViewController { func update(fields: [Field]) { let gens = fields.map { field switch field.type { case .phone: let gen = PhoneCellGenerator(item) gen.didUpdate = { self.updatePhone($0) } return gen case .date: let gen = DateInputCellGenerator(item) gen.didTap = { self.showPicker() } return gen case .dropdown: let gen = DropdownCellGenerator(item) gen.didTap = { self.showDropdown(item) } return gen } } let splitter = SplitterGenerator() self.ddm.addGenerator(splitter) self.ddm.addGenerators(gens) self.ddm.addGenerator(splitter) } }
يمكننا التحكم في ترتيب إضافة عناصر ، وفي الوقت نفسه ، لا يتم فقد الاتصال بين معالجة البيانات وإضافتها إلى واجهة المستخدم. وبالتالي ، في الحالات البسيطة ، لدينا رمز بسيط. في الحالات الصعبة ، لا يتحول الرمز إلى المعكرونة وفي الوقت نفسه يبدو مقبولًا. لقد ظهرت واجهة تعريفية للعمل مع الجداول ، والآن نقوم بتغليف تكوين الخلايا ، والذي يسمح لنا في حد ذاته بإعادة استخدام الخلايا مع التكوينات بين الشاشات المختلفة.
إيجابيات استخدام RDDM:
- تغليف تكوين الخلية.
- تقليل ازدواجية الكود بتغليف العمل من المجموعات إلى المهايئ ؛
- حدد كائن محول بتغليف المنطق المحدد للعمل مع المجموعات ؛
- رمز يصبح أكثر وضوحا وأسهل في القراءة.
- يتم تقليل مقدار التعليمات البرمجية التي يلزم كتابتها لإضافة جدول ؛
- تم تبسيط عملية معالجة الأحداث من الخلايا.
المصادر
هنا .
شكرا لاهتمامكم!