iOS: كيفية إنشاء طريقة عرض جدول مع أقسام قابلة للطي

الجزء 2. مواصلة اعتماد البروتوكولات و MVVM مع وجهات النظر الجدول

هذا هو الجزء الثاني من سلسلة دروسي على طريقة عرض الجدول مع أنواع خلايا متعددة.

بعد قراءة الردود والنصائح المتعددة للجزء الأول ، قررت إضافة بعض التحديثات الرئيسية.
يتم تغيير UITableViewController إلى UIViewController مع TableView بمثابة عرض فرعي.
الآن ، يتوافق ViewModel مع بروتوكول TableViewDataSource. تعد NumberOfRowsInSection و cellForRowAt و numberOfSections جزءًا من ViewModel. هذا يبقي ViewController و ViewModel مفصولة.
يرجى الاطلاع على المشروع المحدث النهائي هنا.
شكرا للجميع على المساهمة!

في الجزء الأول ، أنشأنا طريقة عرض الجدول التالية:

في هذه المقالة ، سنجري بعض التغييرات لجعل المقطع قابل للطي:

عرض الجدول مع أقسام قابلة للطي

لإضافة السلوك القابل للطي ، نحتاج إلى معرفة شيئين حول القسم:

  • هل القسم قابل للطي أم لا
  • حالة القسم الحالي: انهار / وسع

يمكننا إضافة كلا الخصائص إلى بروتوكول ProfileViewModelItem الموجود:

ProfileViewModelItem بروتوكول {
   var type: ProfileViewModelItemType {get}
   var sectionTitle: String {get}
   var rowCount: Int {get}
   var isCollapsible: Bool {get}
   var isCollapsed: Bool {get set}
}

لاحظ أن هذه الخاصية القابلة للكسر تحتوي فقط على getter ، لأننا لن نحتاج إلى تعديلها.

بعد ذلك ، نضيف القيمة الافتراضية القابلة للامتداد إلى ملحق البروتوكول. قمنا بتعيين القيمة الافتراضية إلى true:

ملحق ProfileViewModelItem {
   var rowCount: Int {
      عودة 1
   }
   
   var isCapsapsible: Bool {
      عودة صحيح
   }
}

بمجرد تعديل البروتوكول ، سترى أخطاء ترجمة متعددة في كل من ProfileViewModelItems. إصلاحه عن طريق إضافة هذه الخاصية إلى كل ViewModelItem:

الفئة ProfileViewModelNamePictureItem: ProfileViewModelItem {
   فار isCollapsed = صحيح
}

هذه هي كل التغييرات التي نحتاجها لإجراء في ViewModel. الجزء المتبقي هو تعديل العرض ، بحيث يمكنه التعامل مع إجراءات طي / توسيع.

لا توجد طريقة خارج الإطار لإضافة السلوك القابل للطي إلى طريقة عرض الجدول ، لذلك سنحاكي بطريقة بسيطة للغاية: عندما يتم طي القسم ، سنقوم بتعيين عدد الصفوف على صفر. عندما يتم توسيعه ، سوف نستخدم RowCount الافتراضي لهذا القسم. بالنسبة لـ TableView ، يمكننا توفير هذه المعلومات في طريقة numberOfRowsInSection:

overcide func tableView (_ tableView: UITableView، numberOfRowsInSection section: Int) -> Int {
   دع العنصر = viewModel.items [قسم]
   إذا item.isCollapsible && item.isCollapsed {
      العودة 0
   }
   العودة item.rowCount
}

نحتاج الآن إلى إنشاء طريقة عرض مخصصة للرأس ، والتي سيكون لها العنوان وتسمية السهم. قم بإنشاء فئة فرعية من UITableViewHeaderFooterView وقم بتعيين التنسيق في xib أو التعليمات البرمجية:

عرض HeaderView: UITableViewHeaderFooterView {
   IBOutlet ضعيف var titleLabel: UILabel؟
   IBOutlet ضعيف arrow arrowLabel: UILabel؟
   فار القسم: كثافة العمليات = 0
}

سنستخدم متغير المقطع لتخزين فهرس القسم الحالي ، والذي سنحتاج إليه لاحقًا.

عندما ينقر المستخدم على القسم ، يجب تدوير عرض السهم لأسفل. يمكننا تحقيق ذلك باستخدام ملحق UIView:

التمديد UIView {
   func rotate (_ toValue: CGFloat ، المدة: CFTimeInterval = 0.2) {
      دع الرسوم المتحركة = CABasicAnimation (keyPath: "transform.rotation")
      animation.toValue = toValue
      الرسوم المتحركة. المدة = المدة
      animation.isRemovedOnCompletion = خطأ
      animation.fillMode = kCAFillModeForwards
      self.layer.add (الرسوم المتحركة ، forKey: لا شيء)
   }
}
هذه مجرد واحدة من الطرق الممكنة لتحريك دوران العرض

باستخدام طريقة التمديد هذه ، أضف التعليمات البرمجية التالية داخل فئة HeaderView:

func setCollapsed (مطوي: Bool) {
   arrowLabel؟ .rotate (انهار؟ 0.0: .pi)
}

عندما نسمي هذه الطريقة للحالة المنهارة ، فسوف تقوم بتدوير السهم إلى الموضع الأصلي ، أما بالنسبة للحالة الموسعة فسوف تقوم بتدوير السهم إلى راديان.

بعد ذلك ، نحتاج إلى إعداد عنوان القسم الحالي. كما فعلنا للخلايا في البرنامج التعليمي السابق ، قم بإنشاء متغير العنصر واستخدم مراقب didSet لتعيين العنوان والموضع الأولي لتسمية السهم:

فار البند: ProfileViewModelItem؟ {
   didSet {
      اسمح للحماية بترك العنصر = عنصر آخر {
         إرجاع
      }
     titleLabel؟ .text = item.sectionTitle
     setCollapsed (مطوي: item.isCollapsed)
   }
}

الأسئلة الأخيرة هي: كيفية اكتشاف نقرة المستخدم على الرأس ، وكيفية إعلام TableView؟

لاكتشاف تفاعل المستخدم ، يمكننا تعيين TapGestureRecognizer في رأسنا:

تجاوز func awakeFromNib () {
   super.awakeFromNib ()
   addGestureRecognizer (UITapGestureRecognizer (الهدف: الذات ، العمل: #selector (didTapHeader)))
}
objc private func didTapHeader () {
}

لإخطار TableView ، يمكننا استخدام أي من الطرق التي وصفتها هنا. في هذه الحالة ، سأستخدم الوفد. إنشاء بروتوكول HeaderViewDelegate مع أسلوب واحد:

بروتوكول HeaderViewDelegate: class {
   func toggleSection (الرأس: HeaderView ، القسم: Int)
}

إضافة خاصية مندوب داخل HeaderView:

مندوب var ضعيفة: HeaderViewDelegate؟

أخيرًا ، قم باستدعاء طريقة المفوض هذه من محدد tapHeader:

objc private func tapHeader (gestureRecognizer: UITapGestureRecognizer) {
   المفوض؟. ToggleSection (الرأس: الذات ، القسم: القسم)
}

HeaderView جاهز الآن للاستخدام. دعونا توصيله إلى ViewController لدينا.

افتح ViewModel واجعله متوافقًا مع TableViewDelegate:

ملحق ProfileViewModel: UITableViewDelegate {
}

بعد ذلك ، قم بإزالة طريقة titleForHeaderInSection. بما أننا نستخدم رأسًا مخصصًا ، فسنقوم بتعيين عنوان بطريقة أخرى:

func tableView (_ tableView: UITableView، viewForHeaderInSection section: Int) -> UIView؟ {
إذا سمحت لـ headerView = tableView.dequeueReusableHeaderFooterView (withIdentifier: HeaderView.identifier) ​​as؟ HeaderView {
      headerView.item = viewModel.items [قسم]
      headerView.section = القسم
      headerView.delegate = الذات // لا تنسى هذا السطر !!!
      عودة headerView
   }
   إرجاع UIView ()
}
لكي يعمل dequeueReusableHeaderFooterView ، لا تنسَ تسجيل headerView للجدول View

بمجرد تعيين headerView.delegate على الذات ، ستلاحظ خطأ المحول البرمجي ، لأن ViewModel لدينا لا يتوافق مع البروتوكول حتى الآن. إصلاحه عن طريق إضافة ملحق آخر:

ملحق ProfileViewModel: HeaderViewDelegate {
   func toggleSection (الرأس: HeaderView ، القسم: Int) {
      فار البند = العناصر [القسم]
      إذا item.isCollapsible {
         // تبديل الانهيار
         اسمحوا انهارت =! item.isCollapsed
         item.isCollapsed = انهار
         header.setCollapsed (مطوي: مطوي)
         // ضبط عدد الصفوف داخل القسم
      }
   }
}

نحتاج إلى تعيين طريقة لإعادة تحميل قسم TableView ، لذلك سيقوم بتحديث واجهة المستخدم. في ViewModels الأكثر تعقيدًا والتي تتطلب تحديث tableViewRows أو إضافته أو إزالته ، سيكون من المنطقي استخدام المفوض بطرق متعددة. في مشروعنا نحتاج إلى طريقة واحدة فقط (إعادة التحميل) ، حتى نتمكن من رد الاتصال:

ملف تعريف الفئة: نموذج NSObject {
   عناصر var = [ProfileViewModelItem] ()
   // رد الاتصال لإعادة تحميل tableViewSections
   var reloadSections: ((_ قسم: Int) -> باطل)؟
   .....
}

استدعاء هذا رد الاتصال في toggleSection:

ملحق ProfileViewModel: HeaderViewDelegate {
   func toggleSection (الرأس: HeaderView ، القسم: Int) {
      فار البند = العناصر [القسم]
      إذا item.isCollapsible {
         // تبديل الانهيار
         اسمحوا انهارت =! item.isCollapsed
         item.isCollapsed = انهار
         header.setCollapsed (مطوي: مطوي)
         // ضبط عدد الصفوف داخل القسم
         reloadSections؟ (قسم)
      }
   }
}

في ViewController نستخدم رد الاتصال هذا لإعادة تحميل أقسام مشاهدة الجدول:

تجاوز func viewDidLoad () {
   super.viewDidLoad ()
   viewModel.reloadSections = {[ضعف الذات] (القسم: كثافة العمليات) في
      النفس؟ .tableView؟ .beginUpdates ()
      self؟ .tableView؟ .reloadSections ([قسم] ، مع: .fade)
      النفس؟ .tableView؟ .endUpdates ()
   }
 
   ...
}

إذا قمت ببناء وتشغيل المشروع ، فسترى هذا السلوك المنهار الرائع.

يمكنك التحقق من المشروع النهائي هنا.

هناك بعض الترقيات المحتملة لهذه الميزة:

  1. حاول التفكير في طريقة السماح بتوسيع قسم واحد فقط. لذلك عندما ينقر المستخدم على قسم آخر ، فإنه سينهار أولاً في القسم الموسع ، ثم يوسع القسم الجديد.
  2. عندما يتم توسيع القسم ، قم بالتمرير في عرض الجدول لإظهار الصف الأخير في هذا القسم.

يرجى مشاركة أفكارك في التعليقات أدناه ، حتى نتمكن من مناقشتها.

شكرا للقراءة!