iOS: Jak vytvořit tabulkové zobrazení s více typy buněk

Část 1. Jak se neztratit ve špagetovém kódu

Existují zobrazení tabulky se statickými buňkami, kde počet buněk a pořadí buněk je konstantní. Implementace tohoto zobrazení tabulky je velmi jednoduchá a příliš se neliší od běžného zobrazení UIView.

Existují zobrazení tabulky s dynamickými buňkami jednoho typu: počet a pořadí buněk se dynamicky mění, ale všechny buňky mají stejný typ obsahu. Zde přicházejí znovu použitelné buňky. Toto je také nejoblíbenější typ v případě zobrazení tabulky.

Jsou to také zobrazení tabulky s dynamickými buňkami, které mají různé typy obsahu: počet, pořadí a typy buněk jsou dynamické. Tyto pohledy na tabulky jsou nejzajímavější a nejnáročnější na implementaci.

Představte si aplikaci, kde musíte vytvořit tuto obrazovku:

Všechna data pocházejí z backendu a my nemáme žádnou kontrolu nad tím, jaká data budou přijata s další žádostí: možná nebude žádná „informace“, nebo bude galerie prázdná. V tomto případě nemusíme tyto buňky vůbec zobrazovat. Nakonec musíme vědět, na jaký typ buňky uživatel klepne a podle toho reagovat.

Nejprve určíme problém.

Toto je přístup, který často vidím v různých projektech: konfigurace buňky na základě jejího indexu v UITableView.

přepsat func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   if indexPath.row == 0 {
        // nakonfigurovat typ buňky 1
   } jinak, pokud indexPath.row == 1 {
        // nakonfigurovat typ buňky 2
   }
   ....
}

Téměř stejný kód se používá pro delegovanou metodu didSelectRowAt:

přepsat func tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
        // nakonfigurovat akci při klepnutí na buňku 1
   } jinak, pokud indexPath.row == 1 {
        // nakonfigurovat akci při klepnutí na buňku 1
   }
   ....
}

To bude fungovat podle očekávání až do okamžiku, kdy chcete změnit pořadí buněk nebo odebrat / přidat nové buňky do tabulky TableView. Pokud změníte jeden index, celá struktura zobrazení tabulky bude přerušena a budete muset ručně aktualizovat všechny indexy v metodách cellForRowAt a didSelectRowAt.

Jinými slovy, není znovu použitelný, není jasně čitelný a nesleduje žádné programovací vzorce, protože mísí pohled a model.

Jaký je lepší způsob?

V tomto projektu použijeme vzor MVVM. MVVM je zkratka „Model-View-ViewModel“ a tento vzor je velmi užitečný, pokud potřebujete další vrstvu mezi vaším modelem a pohledem. Zde si můžete přečíst více o všech hlavních designových vzorech pro iOS.

V první části této série tutoriálů vytvoříme dynamické zobrazení tabulky pomocí JSON jako zdroje dat. Budeme se zabývat následujícími tématy a koncepty: protokoly, rozšíření protokolu, vypočtené vlastnosti, příkazy přepínače a další.

V dalším tutoriálu to posuneme o jednu úroveň výše: uděláme sekci sklopitelnou s několika řádky kódu.

Část 1: Model

Nejprve vytvořte nový projekt, přidejte TableView do výchozího ViewControlleru, připojte TableView k ViewControlleru a vložte ho do ControlControlleru ViewController a ujistěte se, že se projekt zkompiluje a spustí podle očekávání. Toto je základní krok, který zde nebude zahrnut. Pokud máte s touto částí potíže, je pravděpodobně příliš brzy na to, abyste se v tomto tématu dostali hlouběji.

Vaše třída ViewController bude vypadat takto:

třída ViewController: UIViewController {
   @IBOutlet slabý var tableView: UITableView?
 
   přepsat func viewDidLoad () {
      super.viewDidLoad ()
   }
}

Vytvořil jsem jednoduchá data JSON, která napodobují odpověď serveru. Můžete si ji stáhnout z mého Dropboxu zde. Uložte tento soubor do složky projektu a ujistěte se, že soubor má název projektu jako cíl v inspektoru souboru:

Budete také potřebovat nějaké obrázky, které najdete zde. Stáhněte si archiv, rozbalte jej a přidejte obrázky do složky aktiv. Nepřejmenovávejte žádné obrázky.

Musíme vytvořit model, který bude uchovávat všechna data, která čteme z JSON.

profil třídy {
   var fullName: String?
   var pictureUrl: String?
   var email: String?
   var about: String?
   var friends = [Friend] ()
   var profileAttributes = [Attribute] ()
}
třída přítel {
   název var: String?
   var pictureUrl: String?
}
Atribut třídy {
   var klíč: String?
   hodnota var: String?
}

Přidáme inicializátor pomocí objektu JSON, takže můžete JSON snadno namapovat na Model. Nejprve potřebujeme způsob, jak extrahovat obsah ze souboru .json a reprezentovat jej jako Data:

public func dataFromFile (_ filename: String) -> Data? {
   @objc třída TestClass: NSObject {}
   let bundle = Bundle (pro: TestClass.self)
   if let path = bundle.path (forResource: filename, ofType: "json") {
      návrat (zkuste? Data (contentOf: URL (fileURLWithPath: cesta))))
   }
   návrat nula
}

Pomocí dat můžeme inicializovat profil. Existuje mnoho různých způsobů, jak rychle analyzovat JSON pomocí nativních serializátorů nebo serializátorů třetích stran, takže můžete použít ten, který se vám líbí. Budu se držet standardní Swift JSONSerializace, aby byl projekt jednoduchý a nepřetěžovaný žádnými externími knihovnami:

profil třídy {
   var fullName: String?
   var pictureUrl: String?
   var email: String?
   var about: String?
   var friends = [Friend] ()
   var profileAttributes = [Attribute] ()
   init? (data: Data) {
      dělat {
         if js js = try JSONSerialization.jsonObject (with: data) as? [Řetězec: Nějaký], nechť tělo = json [„data“] jako? [Řetězec: Libovolný] {
            self.fullName = body [„fullName“] jako? Řetězec
            self.pictureUrl = body [“pictureUrl”] jako? Řetězec
            self.about = body [“about”] as? Řetězec
            self.email = body [„email“] jako? Řetězec
            pokud nechť přátelé = tělo [„přátelé“] jako? [[Řetězec: Libovolný]] {
               self.friends = friends.map {Friend (json: $ 0)}
            }
            if nechat profileAttributes = body [„profileAttributes“] jako? [[Řetězec: Libovolný]] {
               self.profileAttributes = profileAttributes.map {Atribut (json: $ 0)}
            }
         }
      } úlovek {
         print („Chyba deserializace JSON: \ (chyba)“)
         návrat nula
      }
   }
}
třída přítel {
   název var: String?
   var pictureUrl: String?
   init (json: [String: Any]) {
      self.name = json [“name”] as? Řetězec
      self.pictureUrl = json [“pictureUrl”] jako? Řetězec
   }
}
Atribut třídy {
   var klíč: String?
   hodnota var: String?
  
   init (json: [String: Any]) {
      self.key = json [„key“] jako? Řetězec
      self.value = json [„value“] as? Řetězec
   }
}

Část 2: Zobrazit model

Náš model je připraven, takže musíme vytvořit ViewModel. Bude zodpovědný za poskytování dat našemu TableView.

Vytvoříme 5 různých sekcí tabulky:

  • Celé jméno a profilový obrázek
  • O
  • E-mailem
  • Atributy
  • Přátelé

První tři sekce mají vždy jednu buňku, poslední dvě mohou mít více buněk v závislosti na obsahu našeho souboru JSON.

Protože naše data jsou dynamická, počet buněk není konstantní a pro každý typ dat používáme různé tableViewCells, musíme přijít se správnou strukturou ViewModel.

Nejprve musíme rozlišit typy dat, abychom mohli použít příslušnou buňku. Nejlepší způsob práce s více položkami, když potřebujete rychle mezi nimi přepínat, je výčet. Začněme tedy stavět ViewModel pomocí ViewModelItemType:

enum ProfileViewModelItemType {
   název případuAndPicture
   případ
   e-mail s případem
   případ přítel
   atribut case
}

Každý případ výčtu představuje typ dat, který vyžaduje jiný TableViewCell. Ale protože chceme používat naše data ve stejném tableView, takže je třeba mít jediný DataModelItem, který určí všechny vlastnosti. Toho můžeme dosáhnout pomocí protokolu, který poskytne vypočítané vlastnosti našim položkám:

protokol ProfileViewModelItem {

}

První věc, kterou potřebujeme vědět o naší položce, je její typ. Takže vytvoříme vlastnost type pro protokol. Při vytváření vlastnosti protokolu musíte zadat její název, typ a určit, zda je vlastnost gettable nebo settable a gettable. Více informací a příkladů o vlastnostech protokolu naleznete zde. V našem případě bude typem ProfileViewModelItemType a pro tuto vlastnost potřebujeme pouze getter:

protokol ProfileViewModelItem {
   var typ: ProfileViewModelItemType {get}
}

Další vlastnost, kterou potřebujeme, je rowCount. Řekne nám, kolik řádků bude mít každá sekce. Zadejte pro tuto vlastnost typ a getter:

protokol ProfileViewModelItem {
   var typ: ProfileViewModelItemType {get}
   var rowCount: Int {get}
}

Poslední věcí, kterou je dobré mít v tomto protokolu, je název sekce. Název sekce je v podstatě také data pro tabulkuViewView. Jak si pamatujete, pomocí struktury MVVM nechceme vytvářet data ani nic jiného, ​​ale v viewModel:

protokol ProfileViewModelItem {
   var typ: ProfileViewModelItemType {get}
   var rowCount: Int {get}
   var sectionTitle: String {get}
}

Nyní jsme připraveni vytvořit ViewModelItem pro každý z našich datových typů. Každá položka bude odpovídat protokolu. Než to však uděláme, udělejme další krok k čistému a organizovanému projektu: poskytněte některé výchozí hodnoty pro náš protokol. V aplikaci Swift můžeme protokolům poskytnout výchozí hodnoty pomocí rozšíření protokolu:

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

Teď nemusíme poskytovat počet řádků pro naše položky, pokud je počet řádků jeden, takže vám ušetří pár řádků nadbytečného kódu.

Rozšíření protokolu vám také umožní provést volitelné metody protokolu bez použití protokolů @objc. Jednoduše vytvořte rozšíření protokolu a do této rozšíření vložte výchozí implementaci metody.

Vytvořte první ViewModeItem pro buňku Name and Picture.

třída ProfileViewModelNameItem: ProfileViewModelItem {
   var typ: ProfileViewModelItemType {
      návrat .nameAndPicture
   }
   var sectionTitle: String {
      návrat „Hlavní informace“
   }
}

Jak jsem již řekl, nemusíme poskytovat počet řádků, protože v tomto případě potřebujeme výchozí hodnotu 1.

Nyní přidáváme další vlastnosti, které budou pro tuto položku jedinečné: pictureUrl a userName. Obě budou uloženými vlastnostmi bez počáteční hodnoty, takže musíme také zadat init pro tuto třídu:

třída ProfileViewModelNameAndPictureItem: ProfileViewModelItem {
   var typ: ProfileViewModelItemType {
      návrat .nameAndPicture
   }
   var sectionTitle: String {
      návrat „Hlavní informace“
   }
   var pictureUrl: String
   var userName: String
   init (pictureUrl: String, userName: String) {
      self.pictureUrl = pictureUrl
      self.userName = userName
   }
}

Nyní můžeme vytvořit zbývající 4 položky modelu:

třída ProfileViewModelAboutItem: ProfileViewModelItem {
   var typ: ProfileViewModelItemType {
      návrat
   }
   var sectionTitle: String {
      návrat „About“
   }
   var about: String
  
   init (about: String) {
      self.about = about
   }
}
třída ProfileViewModelEmailItem: ProfileViewModelItem {
   var typ: ProfileViewModelItemType {
      návrat. email
   }
   var sectionTitle: String {
      vrátit „Email“
   }
   var email: String
   init (email: String) {
      self.email = email
   }
}
třída ProfileViewModelAttributeItem: ProfileViewModelItem {
   var typ: ProfileViewModelItemType {
      návrat .attribute
   }
   var sectionTitle: String {
      návrat „Atributy“
   }
 
   var rowCount: Int {
      návrat atributy.count
   }
   atributy var:: [Atribut]
   init (atributy: [Atribut]) {
      self.attributes = atributy
   }
}
třída ProfileViewModeFriendsItem: ProfileViewModelItem {
   var typ: ProfileViewModelItemType {
      vrátit se
   }
   var sectionTitle: String {
      návrat „Přátelé“
   }
   var rowCount: Int {
      návrat friends.count
   }
   var friends: [Friend]
   init (přátelé: [Friend]) {
      self.friends = přátelé
   }
}

Pro ProfileViewModeAttributeItem a ProfileViewModeFriendsItem můžeme mít více buněk, takže RowCount bude počet atributů a počet přátel odpovídajícím způsobem.

To je vše, co pro datové položky potřebujeme. Posledním krokem bude třída ViewModel. Tuto třídu může používat jakýkoli ViewController, a to je jeden z klíčových nápadů struktury MVVM: váš ViewModel neví nic o pohledu, ale poskytuje všechna data, která pohled může potřebovat.

Jedinou vlastností, kterou bude ViewModel mít, je pole položek, které bude představovat pole sekcí pro UITableView:

třída ProfileViewModel: NSObject {
   var items = [ProfileViewModelItem] ()
}

K inicializaci ViewModel použijeme model Profile. Nejprve se pokusíme analyzovat soubor .json na Data:

třída ProfileViewModel: NSObject {
   var items = [ProfileViewModelItem] ()
   
   přepsat init (profil: Profil) {
      super.init ()
      guard let data = dataFromFile ("ServerData"), let profile = Profile (data: data) else {
         vrátit se
      }
      // půjde sem inicializační kód
   }
}

Zde je nejzajímavější část: na základě modelu nakonfigurujeme položky ViewModel, které chceme zobrazit.

třída ProfileViewModel: NSObject {
   var items = [ProfileViewModelItem] ()
   přepsat init () {
      super.init ()
      guard let data = dataFromFile ("ServerData"), let profile = Profile (data: data) else {
         vrátit se
      }
 
      pokud necháme jméno = profile.fullName, necháme pictureUrl = profile.pictureUrl {
         let nameAndPictureItem = ProfileViewModelNamePictureItem (jméno: name, pictureUrl: pictureUrl)
         items.append (nameAndPictureItem)
      }
      pokud nechat about = profile.about {
         let aboutItem = ProfileViewModelAboutItem (about: about)
         items.append (aboutItem)
      }
      pokud nechť e-mail = profile.email {
         let dobItem = ProfileViewModelEmailItem (email: email)
         items.append (dobItem)
      }
      let atributy = profile.profileAttributes
      // Položky atributů potřebujeme, pouze pokud atributy nejsou prázdné
      if! attrib.isEmpty {
         let atributyItem = ProfileViewModeAttributeItem (atributy: atributy)
         items.append (atributyItem)
      }
      nechte přátele = profile.friends
      // Potřebujeme položku přátel, pokud přátelé nejsou prázdný
      if! profile.friends.isEmpty {
         let friendsItem = ProfileViewModeFriendsItem (přátelé: přátelé)
         items.append (friendsItem)
      }
   }
}

Nyní, pokud chcete změnit pořadí, přidat nebo odebrat položky, stačí upravit toto pole položek ViewModel. Docela jasné, že?

Dále přidáme UITableViewDataSource do našeho ModelView:

extension ViewModel: UITableViewDataSource {
   func numberOfSections (v tableView: UITableView) -> Int {
      vrátit items.count
   }
   func tableView (_ tableView: UITableView, numberOfRowsInSection: Int) -> Int {
      vrátit položky [sekce] .rowCount
   }
   func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   // zde nakonfigurujeme buňky
   }
}

Část 3: Pohled

Vraťte se do ViewControlleru a připravte TableView.

Nejprve vytvoříme uloženou vlastnost ProfileViewModel a inicializujeme ji. Ve skutečném projektu byste museli nejprve požádat o data, poslat je do ViewModel a znovu načíst TableView při aktualizaci dat (podívejte se na způsoby předávání dat v aplikaci pro iOS zde).

Dále nakonfigurujeme tabulku TableViewDataSource:

přepsat func viewDidLoad () {
   super.viewDidLoad ()
   
   tableView? .dataSource = viewModel
}

Nyní jsme připraveni vytvořit uživatelské rozhraní. Musíme vytvořit pět různých typů buněk, jeden pro každou z ViewModelItems. Vytváření buněk není v tomto tutoriálu něco, čeho se budu zabývat, takže si můžete vytvořit vlastní třídy buněk, design a rozložení buněk. Jako příklad vám ukážu jednoduchý příklad toho, co musíte udělat:

Příklad NameAndPictureCell a FriendCellPříklad EmailCell a AboutCellPříklad atributové buňky

Pokud potřebujete pomoc s vytvářením buňky, nebo chcete najít nějaké tipy, podívejte se na jeden z mých předchozích tutoriálů o tabulce TableCells.

Každá buňka by měla mít vlastnost typu typu ProfileViewModelItem, kterou použijeme k nastavení uživatelského rozhraní buňky:

// předpokládá se, že již máte všechna dílčí zobrazení buněk: štítky, obrázkyViews atd
class NameAndPictureCell: UITableViewCell {
    var položka: ProfileViewModelItem? {
      didSet {
         // přetypuje ProfileViewModelItem na příslušný typ položky
         guard let item = item as? ProfileViewModelNamePictureItem else {
            vrátit se
         }
         nameLabel? .text = item.name
         pictureImageView? .image = UIImage (pojmenovaný: item.pictureUrl)
      }
   }
}
třída AboutCell: UITableViewCell {
   var položka: ProfileViewModelItem? {
      didSet {
         guard let item = item as? ProfileViewModelAboutItem else {
            vrátit se
         }
         aboutLabel? .text = item.about
      }
   }
}
třída EmailCell: UITableViewCell {
    var položka: ProfileViewModelItem? {
      didSet {
         guard let item = item as? ProfileViewModelEmailItem else {
            vrátit se
         }
         emailLabel? .text = item.email
      }
   }
}
class FriendCell: UITableViewCell {
    Var item: Friend? {
      didSet {
         guard let item = item else {
            vrátit se
         }
         pokud necháme pictureUrl = item.pictureUrl {
            pictureImageView? .image = UIImage (pojmenovaný: pictureUrl)
         }
         nameLabel? .text = item.name
      }
   }
}
var položka: Atribut? {
   didSet {
      titleLabel? .text = item? .key
      valueLabel? .text = item? .value
   }
}

Někteří z vás mohou položit rozumnou otázku: Proč nepoužíváme stejnou buňku pro ProfileViewModelAboutItem a ProfileViewModelEmailItem, protože oba mají jeden textový štítek? Odpověď zní ano, můžeme použít stejnou buňku. Účelem tohoto tutoriálu je však ukázat způsob použití různých typů buněk.

Nezapomeňte zaregistrovat buňky, pokud je chcete použít jako opakovaně použitelné buňky: UITableView má způsoby registrace jak buněčných tříd, tak souborů nibů, v závislosti na způsobu, jakým jste buňku vytvořili.

Nyní je čas použít buňky v našem TableView. ViewModel to opět zvládne velmi jednoduchým způsobem:

přepsat func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let item = items [indexPath.section]
   switch item.type {
   case .nameAndPicture:
      if let cell = tableView.dequeueReusableCell (withIdentifier: NamePictureCell.identifier, for: indexPath) jako? NamePictureCell {
         cell.item = položka
         návratová buňka
      }
   případ.
      if nechť cell = tableView.dequeueReusableCell (withIdentifier: AboutCell.identifier, for: indexPath) jako? AboutCell {
         cell.item = položka
         návratová buňka
      }
   případ. email:
      if nechť cell = tableView.dequeueReusableCell (withIdentifier: EmailCell.identifier, for: indexPath) jako? EmailCell {
         cell.item = položka
         návratová buňka
      }
   případ .friend:
      if let cell = tableView.dequeueReusableCell (withIdentifier: FriendCell.identifier, for: indexPath) jako? FriendCell {
         cell.item = přátelé [indexPath.row]
         návratová buňka
      }
   case .attribute:
      if nechat cell = tableView.dequeueReusableCell (withIdentifier: AttributeCell.identifier, pro: indexPath) jako? AttributeCell {
         cell.item = atributy [indexPath.row]
         návratová buňka
      }
   }
   // vrátí výchozí buňku, pokud žádná z výše uvedených možností není úspěšná
   návrat UITableViewCell ()
}
Stejnou strukturu můžete použít k nastavení metody delegování didSelectRowAt:
přepsat func tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      přepnout položky [indexPath.section] .type {
          // proveďte příslušnou akci pro každý typ
      }
}

Nakonec nakonfigurujte headerView:

přepsat func tableView (_ tableView: UITableView, titleForHeaderInSection: Int) -> String? {
   vrátit položky [sekce] .sectionTitle
}

Vytvořte a spusťte svůj projekt a užijte si dynamické zobrazení tabulky!

Výsledný obrázek

Chcete-li otestovat flexibilitu, můžete upravit soubor JSON: přidejte nebo odeberte některé přátele nebo úplně odeberte některá data (prostě nenarušujte strukturu JSON, jinak neuvidíte vůbec žádná data). Když znovu sestavíte svůj projekt, bude tabulkaViewView vypadat a pracovat tak, jak by měla, bez jakýchkoli úprav kódu. ViewModel a ViewController budete muset upravit pouze v případě, že změníte samotný model: přidáte novou vlastnost nebo dramaticky změníte celou její strukturu. Ale to je úplně jiný příběh.

Celý projekt si můžete prohlédnout zde:

Děkuji za přečtení! Máte-li jakékoli dotazy nebo návrhy, zeptejte se!

V dalším článku upgradujeme stávající projekt tak, aby pro sekce přidal pěkný efekt sbalení / rozbalení.

Aktualizace: zde se dozvíte, jak dynamicky aktualizovat tento tableView bez použití metody ReloadData.

Píšu také pro blog American Express Engineering Blog. Podívejte se na moje další díla a díla mých talentovaných spolupracovníků na AmericanExpress.io.