iOS: Sådan bygger du en tabelvisning med flere celletyper

Del 1. Hvordan man ikke går tabt i spaghettikoden

Der er tabelvisninger med de statiske celler, hvor antallet af celler og celleordren er konstant. Implementering af denne tabelvisning er meget enkel og ikke meget forskellig fra den almindelige UIView.

Der er tabelvisninger med dynamiske celler af en type: antallet og rækkefølgen af ​​cellerne ændres dynamisk, men alle celler har den samme type indhold. Det er her de genanvendelige celler kommer på plads. Dette er også den mest populære type, hvis der er tabelvisninger.

Det er også tabelvisninger med dynamiske celler, der har forskellige indholdstyper: antallet, rækkefølge og celletyperne er dynamiske. Disse tabelvisninger er de mest interessante og mest udfordrende at implementere.

Forestil dig appen, hvor du skal bygge denne skærm:

Alle data kommer fra backend, og vi har ingen kontrol over, hvilke data der vil blive modtaget med den næste anmodning: måske vil der ikke være nogen "om" info, eller galleriet vil være tomt. I dette tilfælde behøver vi slet ikke at vise disse celler. Endelig skal vi vide, hvilken celletype brugeren tapper på og reagere i overensstemmelse hermed.

Lad os først bestemme problemet.

Dette er den tilgang, jeg ofte ser i forskellige projekter: Konfiguration af cellen baseret på dens indeks i UITableView.

tilsidesætte func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   hvis indexPath.row == 0 {
        // konfigurere celletype 1
   } andet hvis indexPath.row == 1 {
        // konfigurere celletype 2
   }
   ....
}

Næsten den samme kode bruges til delegeret metode didSelectRowAt:

tilsidesætte func tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
hvis indexPath.row == 0 {
        // konfigurere handling, når du tapper på celle 1
   } andet hvis indexPath.row == 1 {
        // konfigurer handling, når du tapper celle 1
   }
   ....
}

Dette fungerer som forventet, indtil det øjeblik, hvor du vil omarrangere cellerne eller fjerne / tilføje nye celler til tableView. Hvis du ændrer et indeks, vil hele tabelvisningsstrukturen blive brudt, og du skal manuelt opdatere alle indekserne i cellForRowAt- og didSelectRowAt-metoderne.

Med andre ord er det ikke genanvendeligt, ikke klart læseligt, og det følger ikke nogen programmeringsmønstre, da det blander visningen og modellen sammen.

Hvad er den bedre måde?

I dette projekt vil vi bruge MVVM-mønsteret. MVVM står for “Model-View-ViewModel, og dette mønster er meget nyttigt, når du har brug for et ekstra lag mellem din model og visningen. Du kan læse mere om alle større iOS-designmønstre her.

I den første del af denne tutorial-serie bygger vi den dynamiske tabelvisning ved hjælp af JSON som en datakilde. Vi vil dække følgende emner og koncepter: protokoller, protokoludvidelser, computereegenskaber, switch-udsagn og mere.

I den næste vejledning tager vi det ét niveau op: gør sektionen sammenfoldelig med blot et par kodelinjer.

Del 1: Model

Opret først et nyt projekt, tilføj en TableView til standard ViewController, fastgør TableView til ViewController, og integrer ViewController det i Navigation Controller og sørg for, at projektet kompilerer og kører som forventet. Dette er det grundlæggende trin, og det vil ikke blive dækket her. Hvis du har problemer med denne del, er det sandsynligvis for tidligt for dig at gå dybere ned i dette emne.

Din ViewController-klasse ser sådan ud:

klasse ViewController: UIViewController {
   @IBOutlet svag var tabelView: UITableView?
 
   tilsidesætte func viewDidLoad () {
      super.viewDidLoad ()
   }
}

Jeg oprettede en simpel JSON-data, der efterligner serverens respons. Du kan downloade det fra min Dropbox her. Gem denne fil i projektmappen, og sørg for, at filen har projektnavnet, som det er målet i filinspektøren:

Du har også brug for nogle billeder, som du kan finde her. Download arkivet, pak det ud, og tilføj billederne til aktivmappen. Navngiv ingen billeder.

Vi er nødt til at oprette en model, der indeholder alle de data, vi læser fra JSON.

klasseprofil {
   var fullName: String?
   var pictureUrl: String?
   var e-mail: streng?
   var om: streng?
   var venner = [Ven] ()
   var profileAttribute = [Attribut] ()
}
klasse ven {
   var navn: String?
   var pictureUrl: String?
}
klasse attribut {
   var nøgle: String?
   var-værdi: String?
}

Vi tilføjer en initializer ved hjælp af et JSON-objekt, så du nemt kan kortlægge JSON til modellen. Først har vi brug for måden at udpakke indholdet fra .json-filen og repræsentere det som Data:

public func dataFromFile (_ filnavn: String) -> Data? {
   @objc klasse TestClass: NSObject {}
   lad bundle = bundle (for: TestClass.self)
   hvis lad sti = bundle.path (forResource: filnavn, ofType: "json") {
      return (prøv? data (contentOf: URL (filURLWithPath: sti)))
   }
   returnere nul
}

Ved hjælp af dataene kan vi initialisere profilen. Der er mange forskellige måder at analysere JSON i hurtig ved hjælp af både indbyggede eller tredjeparts serialisatorer, så du kan bruge den, du kan lide. Jeg vil holde mig til standard Swift JSONSerialization for at holde projektet enkelt og ikke overbelaste det med nogen eksterne biblioteker:

klasseprofil {
   var fullName: String?
   var pictureUrl: String?
   var e-mail: streng?
   var om: streng?
   var venner = [Ven] ()
   var profileAttribute = [Attribut] ()
   init? (data: Data) {
      gør {
         hvis lad json = prøv JSONSerialization.jsonObject (med: data) som? [String: Enhver], lad body = json [“data”] som? [String: Enhver] {
            self.fullName = body [“fullName”] som? Snor
            self.pictureUrl = body [“pictureUrl”] som? Snor
            self.about = body [“omkring”] som? Snor
            self.email = body [“email”] som? Snor
            hvis lad venner = krop [“venner”] som? [[String: Enhver]] {
               self.friends = friends.map {Friend (json: $ 0)}
            }
            hvis lad profilAttribut = krop [“profilAttributter”] som? [[String: Enhver]] {
               self.profileAttribut = profileAttribute.map {Attribute (json: $ 0)}
            }
         }
      } fangst {
         print (“Fejl ved at deserialisere JSON: \ (fejl)”)
         returnere nul
      }
   }
}
klasse ven {
   var navn: String?
   var pictureUrl: String?
   init (json: [String: Any]) {
      self.name = json [“name”] som? Snor
      self.pictureUrl = json [“pictureUrl”] som? Snor
   }
}
klasse attribut {
   var nøgle: String?
   var-værdi: String?
  
   init (json: [String: Any]) {
      self.key = json [“key”] som? Snor
      self.value = json [“value”] som? Snor
   }
}

Del 2: Vis model

Vores model er klar, så vi er nødt til at oprette ViewModel. Det vil være ansvarligt for at levere data til vores TableView.

Vi vil oprette 5 forskellige tabelafsnit:

  • Fuld navn og profilbillede
  • Om
  • E-mail
  • Egenskaber
  • venner

De første tre sektioner har hver en celle, de sidste to kan have flere celler afhængigt af indholdet af vores JSON-fil.

Da vores data er dynamiske, antallet af celler ikke er konstant, og vi bruger forskellige tabelViewCeller til hver type data, er vi nødt til at finde den rigtige ViewModel-struktur.

Først skal vi skelne mellem datatyperne, så vi kan bruge en passende celle. Den bedste måde at arbejde med flere genstande på, når du let skal skifte mellem dem hurtigt, er enum. Så lad os begynde at opbygge ViewModel med ViewModelItemType:

enum ProfileViewModelItemType {
   sag navn og billede
   sag om
   sag e-mail
   sag ven
   sag attribut
}

Hver enum-sag repræsenterer den datatype, der kræver den forskellige TableViewCell. Men fordi vi ønsker at bruge vores data inden for den samme tabelvisning, så har vi brug for at have det ene DataModelItem, der vil bestemme alle egenskaber. Vi kan opnå dette ved at bruge protokollen, der giver computereegenskaber til vores poster:

protokol ProfilViewModelItem {

}

Den første ting, vi har brug for at vide om vores vare, er dens type. Så vi opretter en type egenskab til protokollen. Når du opretter en protokolegenskab, skal du angive dens navn, type og specificere, om ejendommen kan fås eller kan indstillelig og gettable. Du kan få mere information og eksempler om protokolegenskaber her. I vores tilfælde vil typen være ProfileViewModelItemType, og vi har kun brug for en getter til denne egenskab:

protokol ProfilViewModelItem {
   var type: ProfileViewModelItemType {get}
}

Den næste egenskab, vi har brug for, er rækkeCount. Det fortæller os, hvor mange rækker hver sektion har. Angiv typen og getter for denne egenskab:

protokol ProfilViewModelItem {
   var type: ProfileViewModelItemType {get}
   var rækkeCount: Int {get}
}

Den sidste ting, der er god at have i denne protokol, er afsnitstitlen. Grundlæggende er et afsnitstitel også en data for tabelvisningen. Som du husker, ved hjælp af MVVM-strukturen ønsker vi ikke at oprette dataene eller nogen form andet sted, men i viewModel:

protokol ProfilViewModelItem {
   var type: ProfileViewModelItemType {get}
   var rækkeCount: Int {get}
   var sectionTitle: String {get}
}

Nu er vi klar til at oprette ViewModelItem til hver af vores datatyper. Hver artikel vil være i overensstemmelse med protokollen. Men før vi gør det, lad os tage et nyt skridt mod det rene og organiserede projekt: give nogle standardværdier til vores protokol. I Swift kan vi levere standardværdier til protokoller ved hjælp af protokoludvidelsen:

udvidelse ProfilViewModelItem {
   var rækkeCount: Int {
      retur 1
   }
}

Nu behøver vi ikke at angive rædetallet for vores varer, hvis rædetallet er et, så det sparer dig et par ekstra linjer med overflødig kode.

Protokoludvidelse kan også give dig mulighed for at oprette de valgfri protokolmetoder uden at bruge @objc-protokoller. Opret bare en protokoludvidelse og placer standardmetoden implementering i denne udvidelse.

Opret det første ViewModeItem til navnet og billedcellen.

klasse ProfilViewModelNameItem: ProfilViewModelItem {
   var type: ProfileViewModelItemType {
      return .nameAndPicture
   }
   var sectionTitle: String {
      returner “Main Info”
   }
}

Som jeg sagde før, behøver vi ikke at angive rækkeantallet, for i dette tilfælde har vi brug for standardværdien 1.

Nu tilføjer vi andre egenskaber, der vil være unikke for dette emne: pictureUrl og userName. Begge vil være de lagrede egenskaber uden startværdi, så vi er også nødt til at give init til denne klasse:

klasse ProfilViewModelNameAndPictureItem: ProfileViewModelItem {
   var type: ProfileViewModelItemType {
      return .nameAndPicture
   }
   var sectionTitle: String {
      returner “Main Info”
   }
   var pictureUrl: String
   var brugernavn: String
   init (pictureUrl: String, userName: String) {
      self.pictureUrl = pictureUrl
      self.userName = brugernavn
   }
}

Nu kan vi oprette de resterende 4 modelelementer:

klasse ProfilViewModel AboutItem: ProfileViewModelItem {
   var type: ProfileViewModelItemType {
      vende tilbage
   }
   var sectionTitle: String {
      returnere "Om"
   }
   var om: String
  
   init (om: String) {
      self.about = ca.
   }
}
class ProfileViewModelEmailItem: ProfileViewModelItem {
   var type: ProfileViewModelItemType {
      returnere .mail
   }
   var sectionTitle: String {
      returner “E-mail”
   }
   var e-mail: String
   init (e-mail: streng) {
      self.email = e-mail
   }
}
class ProfileViewModelAttributeItem: ProfileViewModelItem {
   var type: ProfileViewModelItemType {
      return .attribute
   }
   var sectionTitle: String {
      returner “Attributter”
   }
 
   var rækkeCount: Int {
      return attributes.count
   }
   var attributter: [Attribut]
   init (attributter: [Attribut]) {
      self.attribut = attributter
   }
}
klasse ProfilViewModeFriendsItem: ProfileViewModelItem {
   var type: ProfileViewModelItemType {
      vende tilbage. ven
   }
   var sectionTitle: String {
      returner “venner”
   }
   var rækkeCount: Int {
      returner venner. antal
   }
   var venner: [ven]
   init (venner: [Ven]) {
      self.friends = venner
   }
}

For ProfileViewModeAttributeItem og ProfileViewModeFriendsItem kan vi have flere celler, så RowCount vil være antallet af attributter og antallet af venner tilsvarende.

Det er alt, hvad vi har brug for til dataelementerne. Det sidste trin er klassen ViewModel. Denne klasse kan bruges af enhver ViewController, og dette er en af ​​de vigtigste ideer bag MVVM-strukturen: din ViewModel ved intet om visningen, men den indeholder alle de data, som View muligvis har brug for.

Den eneste egenskab, som ViewModel har, er matrixen af ​​elementer, der repræsenterer matrixen af ​​sektioner for UITableView:

klasse ProfilViewModel: NSObject {
   var items = [ProfileViewModelItem] ()
}

For at initialisere ViewModel bruger vi profilmodellen. Først prøver vi at parse .json-filen til Data:

klasse ProfilViewModel: NSObject {
   var items = [ProfileViewModelItem] ()
   
   tilsidesætte init (profil: Profil) {
      super.init ()
      vakt lad data = dataFromFile ("ServerData"), lad profil = Profil (data: data) andet {
         Vend tilbage
      }
      // initialiseringskode går her
   }
}

Her er den mest interessante del: baseret på modellen konfigurerer vi de ViewModel-emner, vi vil vise.

klasse ProfilViewModel: NSObject {
   var items = [ProfileViewModelItem] ()
   tilsidesætte init () {
      super.init ()
      vakt lad data = dataFromFile ("ServerData"), lad profil = Profil (data: data) andet {
         Vend tilbage
      }
 
      hvis lad name = profile.fullName, lad pictureUrl = profile.pictureUrl {
         let nameAndPictureItem = ProfilViewModelNamePictureItem (navn: navn, billedeUrl: billedeUrl)
         items.append (nameAndPictureItem)
      }
      hvis lad = profile.about {
         let aboutItem = ProfilViewModel AboutItem (om: om)
         items.append (aboutItem)
      }
      hvis lad e-mail = profile.email {
         let dobItem = ProfilViewModelEmailItem (e-mail: e-mail)
         items.append (dobItem)
      }
      lad attributter = profile.profileAttributter
      // vi har kun brug for attributter, hvis attributter ikke er tomme
      if! attributter.is tom {
         let attributesItem = ProfileViewModeAttributeItem (attributter: attributter)
         items.append (attributesItem)
      }
      lad venner = profil. veninder
      // vi har kun brug for venner, hvis venner ikke er tomme
      if! profile.friends.is Tom {
         let friendsItem = ProfilViewModeFriendsItem (venner: venner)
         items.append (friendsItem)
      }
   }
}

Hvis du nu vil ombestille, tilføje eller fjerne elementerne, skal du bare ændre denne ViewModel-elementarray. Temmelig klar, ikke?

Dernæst tilføjer vi UITableViewDataSource til vores ModelView:

udvidelse ViewModel: UITableViewDataSource {
   func numberOfSections (i tableView: UITableView) -> Int {
      returnerer varer. antal
   }
   func tableView (_ tableView: UITableView, numberOfRowsInSection-sektion: Int) -> Int {
      returner varer [sektion] .rowCount
   }
   func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   // vi konfigurerer cellerne her
   }
}

Del 3: Udsigt

Vend tilbage til ViewController og forbered TableView.

Først opretter vi den gemte egenskab ProfilViewModel og initialiserer den. I et rigtigt projekt bliver du nødt til at anmode om dataene først, indlæse disse data til ViewModel og derefter genindlæse TableView ved dataopdatering (tjek måderne til at videregive data i iOS-app her).

Dernæst konfigurerer vi tabelViewDataSource:

tilsidesætte func viewDidLoad () {
   super.viewDidLoad ()
   
   tableView? .dataSource = viewModel
}

Nu er vi klar til at opbygge et UI. Vi er nødt til at oprette fem forskellige typer celler, en for hver af ViewModelItems. At opbygge cellerne er ikke noget, jeg vil dække i denne tutorial, så du kan oprette dine egne celleklasser, design og cellelayout. Som reference vil jeg vise dig det enkle eksempel på, hvad du skal gøre:

NameAndPictureCell og FriendCell-eksempelEmailCell og AboutCell-eksempelEksempel på AttributeCell

Hvis du har brug for en hjælp til at oprette cellen, eller vil finde nogle tip, kan du tjekke en af ​​mine tidligere tutorials om tabelViewCells.

Hver celle skal have en egenskab af typen ProfilViewModelItem, som vi vil bruge til at konfigurere celle UI:

// dette antager, at du allerede har alle celleundervisninger: labels, imagesViews osv
class NameAndPictureCell: UITableViewCell {
    var item: ProfileViewModelItem? {
      sætte {
         // cast ProfilViewModelItem til passende varetype
         vagt lad vare = vare som? ProfilViewModelNamePictureItem andet {
            Vend tilbage
         }
         nameLabel? .text = item.name
         pictureImageView? .image = UIImage (navngivet: item.pictureUrl)
      }
   }
}
klasse AboutCell: UITableViewCell {
   var item: ProfileViewModelItem? {
      sætte {
         vagt lad vare = vare som? ProfilVisModelOm andet {
            Vend tilbage
         }
         aboutLabel? .text = item.about
      }
   }
}
klasse EmailCell: UITableViewCell {
    var item: ProfileViewModelItem? {
      sætte {
         vagt lad vare = vare som? ProfileViewModelEmailItem else {
            Vend tilbage
         }
         emailLabel? .text = item.email
      }
   }
}
klasse FriendCell: UITableViewCell {
    var vare: Ven? {
      sætte {
         vagt lad vare = vare andet {
            Vend tilbage
         }
         hvis lad pictureUrl = item.pictureUrl {
            pictureImageView? .image = UIImage (navngivet: pictureUrl)
         }
         nameLabel? .text = item.name
      }
   }
}
var vare: Attribut? {
   sætte {
      titleLabel? .text = item? .key
      valueLabel? .text = item? .value
   }
}

Nogle af jer kan stille et rimeligt spørgsmål: hvorfor bruger vi ikke den samme celle til ProfileViewModel AboutItem og ProfileViewModelEmailItem, da de begge har en enkelt tekstmærke? Svaret er ja, vi kan bruge den samme celle. Men formålet med denne tutorial er at vise dig, hvordan du bruger forskellige celletyper.

Glem ikke at registrere cellerne, hvis du vil bruge dem som genanvendelige celler: UITableView har metoder til at registrere både celleklasser eller knapfiler, afhængigt af den måde, du oprettede cellen på.

Nu er det tid til at bruge cellerne i vores TableView. Igen vil ViewModel håndtere dette på en meget enkel måde:

tilsidesætte func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let item = items [indexPath.section]
   skift item.type {
   case .nameAndPicture:
      hvis lad cell = tableView.dequeueReusableCell (withIdentifier: NamePictureCell.identifier, for: indexPath) som? NamePictureCell {
         cell.item = vare
         returcelle
      }
   sag. om:
      hvis lad cell = tableView.dequeueReusableCell (withIdentifier: AboutCell.identifier, for: indexPath) som? AboutCell {
         cell.item = vare
         returcelle
      }
   sag. mail:
      hvis lad celle = tableView.dequeueReusableCell (withIdentifier: EmailCell.identifier, for: indexPath) som? EmailCell {
         cell.item = vare
         returcelle
      }
   sag. ven:
      hvis lad cell = tableView.dequeueReusableCell (withIdentifier: FriendCell.identifier, for: indexPath) som? FriendCell {
         cell.item = venner [indexPath.row]
         returcelle
      }
   sag .attribut:
      hvis lad celle = tableView.dequeueReusableCell (withIdentifier: AttributeCell.identifier, for: indexPath) som? AttributCell {
         cell.item = attributter [indexPath.row]
         returcelle
      }
   }
   // returner standardcellen, hvis ingen af ​​ovenstående lykkes
   returner UITableViewCell ()
}
Du kan bruge den samme struktur til at opsætte didSelectRowAt-delegationsmetoden:
tilsidesætte func tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      skifte elementer [indexPath.section] .type {
          // foretage passende handling for hver type
      }
}

Endelig konfigurerer du en headerView:

tilsidesætte func tableView (_ tableView: UITableView, titleForHeaderInSection: Int) -> String? {
   returner artikler [sektion] .sektionstitel
}

Byg og kør dit projekt og nyd den dynamiske tabelvisning!

Resultatbillede

For at teste fleksibiliteten kan du ændre JSON-filen: tilføje eller fjerne nogle venner eller fjerne nogle af dataene helt (bare ikke bryde JSON-strukturen, ellers ser du slet ingen data). Når du genopbygger dit projekt, ser TableView ud og fungerer som det skal uden nogen kodekodifikationer. Du skal kun ændre din ViewModel og ViewController, hvis du ændrer selve modellen: tilføj en ny egenskab eller dramatisk ændrer hele strukturen. Men dette er en helt anden historie.

Du kan tjekke det komplette projekt her:

Tak for at have læst! Hvis du har spørgsmål eller forslag, er du velkommen til at stille det!

I den næste artikel opgraderer vi det eksisterende projekt for at tilføje en dejlig kollaps / udvidelseseffekt for sektionerne.

Opdatering: Tjek her for at lære, hvordan du dynamisk opdaterer denne tabelvisning uden at bruge ReloadData-metoden.

Jeg skriver også til American Express Engineering Blog. Tjek mine andre værker og mine talentfulde medarbejders værker på AmericanExpress.io.