iOS: Jak vytvořit tabulkový pohled se skládacími sekcemi

Část 2. Pokračujte v přijímání protokolů a MVVM se zobrazením tabulek

Toto je druhá část mé výukové série v zobrazení tabulky s více typy buněk.

Po přečtení několika odpovědí a rad pro první část jsem se rozhodl přidat některé významné aktualizace.
UITableViewController je změněn na UIViewController s TableView jako subview.
Nyní ViewModel vyhovuje protokolu TableViewDataSource. NumberOfRowsInSection, cellForRowAt a numberOfSections jsou součástí ViewModel. Díky tomu jsou ViewController a ViewModel odděleny.
Zde najdete konečný aktualizovaný projekt.
Děkujeme všem za příspěvek!

V první části jsme vytvořili následující tabulkový pohled:

V tomto článku provedeme některé změny, aby bylo možné sekci sklopit:

Tabulka se skládacími sekcemi

Chcete-li přidat sklopné chování, potřebujeme vědět o této věci dvě věci:

  • je sekce sklopná nebo ne
  • aktuální stav sekce: sbaleno / rozbaleno

Do existujícího protokolu ProfileViewModelItem můžeme přidat obě vlastnosti:

protokol ProfileViewModelItem {
   var typ: ProfileViewModelItemType {get}
   var sectionTitle: String {get}
   var rowCount: Int {get}
   var isCollapsible: Bool {get}
   var isCollapsed: Bool {get set}
}

Tato vlastnost isCollapsible má pouze getter, protože ji nebudeme muset upravovat.

Dále přidáme k rozšíření protokolu výchozí hodnotu isCollapsible. Výchozí hodnotu nastavíme na true:

rozšíření ProfileViewModelItem {
   var rowCount: Int {
      návrat 1
   }
   
   var isCollapsible: Bool {
      návrat pravda
   }
}

Jakmile upravíte protokol, uvidíte v každém z ProfileViewModelItems více kompilačních chyb. Opravte to přidáním této vlastnosti do každého ViewModelItem:

třída ProfileViewModelNamePictureItem: ProfileViewModelItem {
   var isCollapsed = true
}

To jsou všechny změny, které musíme provést v aplikaci ViewModel. Zbývající část je upravit pohled tak, aby zvládal akce sbalení / rozbalení.

Neexistuje žádný out-of-the-box způsob, jak přidat skládací chování na tableView, takže budeme napodobovat velmi jednoduchým způsobem: když je sekce sbalena, nastavíme její počet řádků na nulu. Když je rozbalen, použijeme pro tuto sekci výchozí rowCount. Pro TableView můžeme tyto informace poskytnout v metodě numberOfRowsInSection:

přepsat func tableView (_ tableView: UITableView, numberOfRowsInSection: Int) -> Int {
   let item = viewModel.items [sekce]
   if item.isCollapsible && item.isCollapsed {
      návrat 0
   }
   vrátit item.rowCount
}

Nyní musíme vytvořit vlastní pohled záhlaví, který bude mít nadpis a štítek se šipkou. Vytvořte podtřídu UITableViewHeaderFooterView a nastavte rozložení ve formátu xib nebo code:

třída HeaderView: UITableViewHeaderFooterView {
   @IBOutlet slabý var titleLabel: UILabel?
   @IBOutlet slabá var arrowLabel: UILabel?
   var sekce: Int = 0
}

Proměnnou sekce použijeme k uložení aktuálního indexu sekce, který budeme potřebovat později.

Když uživatel klepne na sekci, měl by se pohled se šipkou otočit dolů. Toho můžeme dosáhnout pomocí rozšíření UIView:

rozšíření UIView {
   func rotate (_ toValue: CGFloat, duration: CFTimeInterval = 0.2) {
      let animation = CABasicAnimation (keyPath: “transform.rotation”)
      animation.toValue = toValue
      animation.duration = trvání
      animation.isRemovedOnCompletion = false
      animation.fillMode = kCAFillModeForwards
      self.layer.add (animace, forKey: nil)
   }
}
Toto je pouze jeden z možných způsobů animace rotace pohledu

Pomocí této metody rozšíření přidejte do třídy HeaderView následující kód:

func setCollapsed (sbaleno: Bool) {
   arrowLabel? .rotate (sbalený? 0,0: .pi)
}

Když voláme tuto metodu pro sbalený stav, otočí šipku do původní polohy, pro rozšířený stav otočí šipku na pí radian.

Dále musíme nastavit aktuální název sekce. Jak jsme to udělali pro buňky v předchozím tutoriálu, vytvořte proměnnou položky a pomocí pozorovatele didSet nastavte nadpis a počáteční polohu štítku se šipkou:

var položka: ProfileViewModelItem? {
   didSet {
      guard let item = item else {
         vrátit se
      }
     titleLabel? .text = item.sectionTitle
     setCollapsed (sbaleno: item.isCollapsed)
   }
}

Poslední otázky jsou: jak detekovat klepnutí uživatele na záhlaví a jak upozornit TableView?

Abychom zjistili interakci uživatele, můžeme do naší hlavičky nastavit TapGestureRecognizer:

přepsat func awakeFromNib () {
   super.awakeFromNib ()
   addGestureRecognizer (UITapGestureRecognizer (target: self, action: #selector (didTapHeader))))
}
@objc private func didTapHeader () {
}

K upozornění TableView můžeme použít kterýkoli ze způsobů, které jsem zde popsal. V tomto případě budu používat delegaci. Vytvořte protokol HeaderViewDelegate jednou metodou:

protokol HeaderViewDelegate: class {
   func toggleSection (záhlaví: HeaderView, sekce: Int)
}

Přidejte vlastnost delegáta do HeaderView:

slabý var delegát: HeaderViewDelegate?

Nakonec volejte tuto metodu delegáta z selektoru tapHeader:

@objc private func tapHeader (gestureRecognizer: UITapGestureRecognizer) {
   delegate? .toggleSection (header: self, section: section)
}

HeaderView je nyní připraven k použití. Připojme ji k našemu ViewControlleru.

Otevřete ViewModel a přizpůsobte ho TableViewDelegate:

rozšíření ProfileViewModel: UITableViewDelegate {
}

Poté odeberte metodu titleForHeaderInSection. Protože používáme vlastní hlavičku, nastavíme název jiným způsobem:

func tableView (_ tableView: UITableView, viewForHeaderInSection: Int) -> UIView? {
pokud necháme headerView = tableView.dequeueReusableHeaderFooterView (withIdentifier: HeaderView.identifier) ​​jako? HeaderView {
      headerView.item = viewModel.items [sekce]
      headerView.section = section
      headerView.delegate = self // tento řádek nezapomeňte !!!
      vrátit headerView
   }
   návrat UIView ()
}
Aby dequeueReusableHeaderFooterView fungovalo, nezapomeňte zaregistrovat headerViewView pro tabulkuView

Jakmile nastavíte headerView.delegate na sebe, všimnete si chyby kompilátoru, protože náš ViewModel dosud protokol nesplňuje. Opravte to přidáním další přípony:

rozšíření ProfileViewModel: HeaderViewDelegate {
   func toggleSection (záhlaví: HeaderView, sekce: Int) {
      var item = items [section]
      if item.isCollapsible {
         // Přepnout kolaps
         nechal sbalený =! item.isCollapsed
         item.isCollapsed = sbaleno
         header.setCollapsed (sbaleno: sbaleno)
         // Upravte počet řádků uvnitř sekce
      }
   }
}

Musíme nastavit způsob, jak znovu načíst sekci TableView, aby se aktualizovalo uživatelské rozhraní. Ve složitějších ViewModels, které vyžadují aktualizaci, přidání nebo odebrání tableViewRows, bude mít smysl použít delegáta s více metodami. V našem projektu potřebujeme pouze jednu metodu (ReloadSection), takže nám můžeme zavolat:

třída ProfileViewModel: NSObject {
   var items = [ProfileViewModelItem] ()
   // zpětné volání pro načtení tableViewSections
   var reloadSections: ((_ section: Int) -> Void)?
   .....
}

Volání tohoto zpětného volání v toggleSection:

rozšíření ProfileViewModel: HeaderViewDelegate {
   func toggleSection (záhlaví: HeaderView, sekce: Int) {
      var item = items [section]
      if item.isCollapsible {
         // Přepnout kolaps
         nechal sbalený =! item.isCollapsed
         item.isCollapsed = sbaleno
         header.setCollapsed (sbaleno: sbaleno)
         // Upravte počet řádků uvnitř sekce
         reloadSections? (section)
      }
   }
}

V ViewController použijeme toto zpětné volání k načtení sekcí tableView:

přepsat func viewDidLoad () {
   super.viewDidLoad ()
   viewModel.reloadSections = {[slabé já] (sekce: Int) v
      self? .tableView? .beginUpdates ()
      self? .tableView? .reloadSections ([section], with: .fade)
      self? .tableView? .endUpdates ()
   }
 
   ...
}

Pokud projekt sestavíte a spustíte, uvidíte toto pěkné animované kolapsu.

Zde si můžete prohlédnout závěrečný projekt.

Pro tuto funkci existují některé potenciální aktualizace:

  1. Pokuste se vymyslet způsob, jak povolit rozšíření pouze jedné sekce. Když tedy uživatel klepne na jinou sekci, rozbalí nejprve rozbalenou a poté rozbalí novou.
  2. Když se sekce rozšiřuje, posouvejte tableView a zobrazte poslední řádek v této sekci.

Prosím, podělte se o své myšlenky v komentářích níže, abychom o nich mohli diskutovat.

Děkuji za přečtení!