iOS: Как да се изгради изглед на таблица с няколко типа клетки

Част 1. Как да не се изгубите в кода за спагети

Има таблични изгледи със статичните клетки, където броят на клетките и редът на клетките са постоянни. Изпълнението на този изглед на таблица е много просто и не се различава много от обикновения UIView.

Има изгледи на таблици с динамични клетки от един тип: броят и редът на клетките се променят динамично, но всички клетки имат един и същ тип съдържание. Тук се появяват клетките за многократна употреба. Това е и най-популярният тип, ако изгледи на таблица.

Също така са таблични изгледи с динамични клетки, които имат различни типове съдържание: числото, редът и типовете клетки са динамични. Тези изгледи на таблици са най-интересните и най-предизвикателните за изпълнение.

Представете си приложението, където трябва да изградите този екран:

Всички данни идват от бекенда и ние нямаме контрол върху това какви данни ще бъдат получени при следващата заявка: може би няма да има информация за „“ или галерията ще бъде празна. В този случай изобщо не е необходимо да показваме тези клетки. И накрая, трябва да знаем какъв тип клетка използва потребителят и да реагираме съответно.

Първо, нека да определим проблема.

Това е подходът, който често виждам в различни проекти: конфигуриране на клетката въз основа на нейния индекс в UITableView.

замени функцията tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   ако indexPath.row == 0 {
        // конфигурирайте клетка тип 1
   } else, ако indexPath.row == 1 {
        // конфигурирайте клетка тип 2
   }
   ....
}

Почти същия код се използва за метода на делегата didSelectRowAt:

замени функцията tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
ако indexPath.row == 0 {
        // конфигуриране на действие при натискане на клетка 1
   } else, ако indexPath.row == 1 {
        // конфигуриране на действие при натискане на клетка 1
   }
   ....
}

Това ще работи както се очаква до момента, в който искате да пренаредите клетките или да премахнете / добавите нови клетки в tableView. Ако промените един индекс, цялата структура на изгледа на таблица ще бъде разбита и ще трябва да актуализирате ръчно всички индекси в методите cellForRowAt и didSelectRowAt.

С други думи, тя не може да се използва повторно, не е лесно четена и не следва никакви модели на програмиране, тъй като смесва заедно изгледа и модела.

Какъв е по-добрият начин?

В този проект ще използваме MVVM модела. MVVM означава „Model-View-ViewModel и този модел е много полезен, когато се нуждаете от допълнителен слой между вашия модел и изгледа. Можете да прочетете повече за всички основни модели на дизайн на iOS тук.

В първата част на тази поредица от уроци ще изградим динамичния изглед на таблица, използвайки JSON като източник на данни. Ще разгледаме следните теми и концепции: протоколи, разширения на протоколи, изчислени свойства, превключвателни операции и други.

В следващия урок ще го вземем едно ниво нагоре: направете раздела сгъваем само с няколко реда код.

Част 1: Модел

Първо, създайте нов проект, добавете TableView по подразбиране ViewController, прикрепете tableView към ViewController и вградете ViewController в Navigation Controller и се уверете, че проектът се компилира и изпълнява според очакванията. Това е основната стъпка и тук няма да бъде обхванато. Ако имате проблеми с тази част, вероятно е твърде рано да се задълбочите по тази тема.

Вашият клас ViewController ще изглежда така:

клас ViewController: UIViewController {
   @IBOutlet слаб var tableView: UITableView?
 
   замени функцията viewDidLoad () {
      super.viewDidLoad ()
   }
}

Създадох прости данни от JSON, които имитират отговора на сървъра. Можете да го изтеглите от моя Dropbox тук. Запишете този файл в папката на проекта и се уверете, че файлът има името на проекта, както е насочено в инспектора на файлове:

Ще ви трябват и някои изображения, които можете да намерите тук. Изтеглете архива, разархивирайте го и добавете снимките в папката с активи. Не преименувайте изображения.

Трябва да създадем модел, който да съхранява всички данни, които четем от JSON.

клас профил {
   var fullName: String?
   var pictureUrl: String?
   var имейл: String?
   var about: String?
   вар приятели = [приятел] ()
   var profileAttributes = [Атрибут] ()
}
Приятел на класа {
   var име: String?
   var pictureUrl: String?
}
клас Атрибут {
   var ключ: String?
   стойност на var: String?
}

Ще добавим инициализатор, използвайки JSON обект, така че можете лесно да картографирате JSON в Модела. Първо, ни е необходим начин да извлечем съдържанието от .json файла и да го представим като Данни:

public func dataFromFile (_ име на файл: String) -> Данни? {
   @objc клас TestClass: NSObject {}
   нека пакет = пакет (за: TestClass.self)
   ако нека path = bundle.path (forResource: име на файл, ofType: "json") {
      връщане (опитайте? Данни (contentOf: URL (fileURLWithPath: path)))
   }
   връщане нула
}

Използвайки Данните, можем да инициализираме Профила. Има много различни начини да анализирате JSON бързо, като използвате както собствени, така и серийни серийни устройства, така че можете да използвате този, който харесвате. Ще се придържам към стандартната Swift JSONSerialization, за да поддържа проекта прост и да не го претоварвам с външни библиотеки:

клас профил {
   var fullName: String?
   var pictureUrl: String?
   var имейл: String?
   var about: String?
   вар приятели = [приятел] ()
   var profileAttributes = [Атрибут] ()
   init? (данни: Данни) {
      направете {
         ако оставите json = опитайте JSONSerialization.jsonObject (с: данни) като? [String: Any], нека body = json [„данни“] като? [Низ: Някой] {
            self.fullName = body [“fullName”] както? низ
            self.pictureUrl = body ["pictureUrl"] като? низ
            self.about = body [„около“] като? низ
            self.email = body [„имейл“] като? низ
            ако нека приятели = тяло [“приятели”] като? [[String: Any]] {
               self.friends = friends.map {Приятел (json: $ 0)}
            }
            ако нека profileAttributes = body [“profileAttributes”] като? [[String: Any]] {
               self.profileAttributes = profileAttributes.map {Attribute (json: $ 0)}
            }
         }
      } улов {
         печат („Грешка десериализиране на JSON: \ (грешка)“)
         връщане нула
      }
   }
}
Приятел на класа {
   var име: String?
   var pictureUrl: String?
   init (json: [String: Any]) {
      self.name = json [„име“] като? низ
      self.pictureUrl = json [“pictureUrl”] като? низ
   }
}
клас Атрибут {
   var ключ: String?
   стойност на var: String?
  
   init (json: [String: Any]) {
      self.key = json [“ключ”] като? низ
      self.value = json [„стойност“] като? низ
   }
}

Част 2: Вижте модел

Нашият модел е готов, така че трябва да създадем ViewModel. Той ще бъде отговорен за предоставянето на данни на нашия TableView.

Ще създадем 5 различни таблични секции:

  • Пълно име и снимка на профила
  • относно
  • електронна поща
  • Атрибути
  • Приятели

Първите три секции имат по една клетка всяка, а последните две могат да имат няколко клетки в зависимост от съдържанието на нашия JSON файл.

Тъй като нашите данни са динамични, броят на клетките не е постоянен и използваме различни tableViewCells за всеки тип данни, трябва да измислим правилната структура на ViewModel.

Първо, трябва да разграничим типовете данни, за да можем да използваме подходяща клетка. Най-добрият начин за работа с множество елементи, когато трябва лесно да превключвате между тях бързо, е enum. Затова нека започнем да изграждаме ViewModel с ViewModelItemType:

enum ProfileViewModelItemType {
   case nameAndPicture
   дело за
   случай имейл
   случай приятел
   атрибут на случая
}

Всеки случай на enum представлява типа данни, който изисква различен TableViewCell. Но тъй като искаме да използваме нашите данни в рамките на една и съща tableView, така че трябва да имаме единния DataModelItem, който ще определи всички свойства. Ние можем да постигнем това с помощта на протокола, който ще предостави изчислени свойства на нашите елементи:

протокол ProfileViewModelItem {

}

Първото нещо, което трябва да знаем за нашия артикул, е неговият вид. Така създаваме свойство тип за протокола. Когато създавате свойство на протокол, трябва да посочите неговото име, тип и да определите дали свойството може да се получи или да се зададе и да се получи. Можете да получите повече информация и примери за свойствата на протокола тук. В нашия случай типът ще бъде ProfileViewModelItemType, а за този имот се нуждаем само от гетьор:

протокол ProfileViewModelItem {
   тип вар: ProfileViewModelItemType {get}
}

Следващото свойство, от което се нуждаем, е rowCount. Той ще ни каже колко редове ще има всеки раздел. Въведете типа и получателя за този имот:

протокол ProfileViewModelItem {
   тип вар: ProfileViewModelItemType {get}
   var rowCount: Int {get}
}

Последното нещо, което е добре да има в този протокол, е заглавието на секцията. По принцип заглавието на секцията също е данни за tableView. Както си спомняте, използвайки структурата на MVVM, ние не искаме да създаваме данни или какъвто и да е вид никъде другаде, но в viewModel:

протокол ProfileViewModelItem {
   тип вар: ProfileViewModelItemType {get}
   var rowCount: Int {get}
   var sectionTitle: String {get}
}

Сега сме готови да създадем ViewModelItem за всеки от нашите типове данни. Всеки елемент ще съответства на протокола. Но преди да го направим, нека направим още една стъпка към чистия и организиран проект: предоставете някои стойности по подразбиране за нашия протокол. В Swift можем да предоставим стойностите по подразбиране на протоколите с помощта на разширението на протокола:

разширение ProfileViewModelItem {
   var rowCount: Int {
      връщане 1
   }
}

Сега не е нужно да предоставяме брой на реда за нашите елементи, ако броят на реда е един, така че ще ви спести няколко допълнителни реда от излишен код.

Разширението на протокол може също да ви позволи да направите незадължителните методи на протокол, без да използвате @objc протоколите. Просто създайте разширение за протокол и поставете прилагането на метода по подразбиране в това разширение.

Създайте първия ViewModeItem за клетката Име и Картина.

клас ProfileViewModelNameItem: ProfileViewModelItem {
   тип вар: ProfileViewModelItemType {
      връщане .nameAndPicture
   }
   var sectionTitle: String {
      върнете „Основна информация“
   }
}

Както казах преди, не е необходимо да предоставяме брой на реда, защото в този случай се нуждаем от стойността по подразбиране 1.

Сега добавяме други свойства, които ще бъдат уникални за този елемент: pictureUrl и userName. И двете ще бъдат запаметените свойства без начална стойност, така че ние също трябва да предоставим init за този клас:

клас ProfileViewModelNameAndPictureItem: ProfileViewModelItem {
   тип вар: ProfileViewModelItemType {
      връщане .nameAndPicture
   }
   var sectionTitle: String {
      върнете „Основна информация“
   }
   var pictureUrl: String
   var userName: String
   init (pictureUrl: String, userName: String) {
      self.pictureUrl = pictureUrl
      self.userName = userName
   }
}

Сега можем да създадем останалите 4 модела:

клас ProfileViewModelAboutItem: ProfileViewModelItem {
   тип вар: ProfileViewModelItemType {
      връщане
   }
   var sectionTitle: String {
      върнете „Относно“
   }
   var about: String
  
   init (about: String) {
      self.about = около
   }
}
клас ProfileViewModelEmailItem: ProfileViewModelItem {
   тип вар: ProfileViewModelItemType {
      връщане .имейл
   }
   var sectionTitle: String {
      върнете „Имейл“
   }
   var имейл: String
   init (имейл: String) {
      self.email = имейл
   }
}
клас ProfileViewModelAttributeItem: ProfileViewModelItem {
   тип вар: ProfileViewModelItemType {
      връщане .attribute
   }
   var sectionTitle: String {
      върнете „Атрибути“
   }
 
   var rowCount: Int {
      return attributes.count
   }
   var атрибути: [Attribute]
   init (атрибути: [Атрибут]) {
      self.attributes = атрибути
   }
}
клас ProfileViewModeFriendsItem: ProfileViewModelItem {
   тип вар: ProfileViewModelItemType {
      връщане. приятел
   }
   var sectionTitle: String {
      върнете „Приятели“
   }
   var rowCount: Int {
      върнете friends.count
   }
   вар приятели: [Приятел]
   init (приятели: [Приятел]) {
      self.friends = приятели
   }
}

За ProfileViewModeAttributeItem и ProfileViewModeFriendsItem можем да имаме няколко клетки, така че RowCount ще бъде съответно броят на атрибутите и броя на приятелите.

Това е всичко, което ни е необходимо за елементите с данни. Последната стъпка ще бъде класът ViewModel. Този клас може да се използва от всеки ViewController и това е една от ключовите идеи зад структурата на MVVM: вашата ViewModel не знае нищо за изгледа, но предоставя всички данни, от които View може да се нуждае.

Единственото свойство, което ViewModel ще има, е масив от елементи, които ще представляват масива от секции за UITableView:

клас ProfileViewModel: NSObject {
   var items = [ProfileViewModelItem] ()
}

За да инициализираме ViewModel, ще използваме модела на профила. Първо се опитваме да анализираме .json файла на Data:

клас ProfileViewModel: NSObject {
   var items = [ProfileViewModelItem] ()
   
   override init (профил: Профил) {
      super.init ()
      пази нека данни = dataFromFile ("ServerData"), нека профил = Профил (данни: данни) друго {
         връщане
      }
      // кодът за инициализация ще отиде тук
   }
}

Ето най-интересната част: въз основа на модела, ние ще конфигурираме елементите ViewModel, които искаме да покажем.

клас ProfileViewModel: NSObject {
   var items = [ProfileViewModelItem] ()
   отменя init () {
      super.init ()
      пази нека данни = dataFromFile ("ServerData"), нека профил = Профил (данни: данни) друго {
         връщане
      }
 
      ако нека name = profile.fullName, нека pictureUrl = profile.pictureUrl {
         нека nameAndPictureItem = ProfileViewModelNamePictureItem (име: име, снимкаUrl: pictureUrl)
         items.append (nameAndPictureItem)
      }
      ако нека за = profile.about {
         нека aboutItem = ProfileViewModelAboutItem (about: about)
         items.append (aboutItem)
      }
      ако оставите имейл = profile.email {
         нека dobItem = ProfileViewModelEmailItem (имейл: имейл)
         items.append (dobItem)
      }
      нека attributes = profile.profileAttributes
      // имаме нужда от елемент атрибути само ако атрибутите не са празни
      ако! attributes.isEmpty {
         нека attributesItem = ProfileViewModeAttributeItem (attributes: attributes)
         items.append (attributesItem)
      }
      нека приятели = profile.friends
      // имаме нужда от приятел, само ако приятелите не са празни
      if! profile.friends.isEmpty {
         нека friendsItem = ProfileViewModeFriendsItem (приятели: приятели)
         items.append (friendsItem)
      }
   }
}

Сега, ако искате да пренаредите, добавите или премахнете елементите, просто трябва да промените този масив от ViewModel елементи. Доста ясно, нали?

След това ще добавим UITableViewDataSource към нашия ModelView:

разширение ViewModel: UITableViewDataSource {
   func numberOfSections (в tableView: UITableView) -> Int {
      върнете items.count
   }
   func tableView (_ tableView: UITableView, numberOfRowsInSection раздел: Int) -> Int {
      връщане на артикули [раздел] .rowCount
   }
   func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   // тук ще конфигурираме клетките
   }
}

Част 3: Преглед

Върнете се към ViewController и подгответе TableView.

Първо създаваме запаметената собственост ProfileViewModel и я инициализираме. В реален проект трябва първо да поискате данните, да ги подадете в ViewModel и след това да презаредите TableView за актуализация на данните (вижте начините за предаване на данни в iOS приложение тук).

След това конфигурираме tableViewDataSource:

замени функцията viewDidLoad () {
   super.viewDidLoad ()
   
   tableView? .dataSource = viewModel
}

Сега сме готови да изградим потребителски интерфейс. Трябва да създадем пет различни типа клетки, по една за всяка от ViewModelItems. Изграждането на клетките не е нещо, което ще покрия в този урок, така че можете да създадете свои собствени класови клетки, дизайн и оформление на клетките. Като справка ще ви покажа простия пример за това, което трябва да направите:

Пример NameAndPictureCell и FriendCellПример за EmailCell и AboutCellПример AttributeCell

Ако имате нужда от помощ при създаването на клетката или искате да намерите някои съвети, проверете един от предишните ми уроци за tableViewCells.

Всяка клетка трябва да има свойството на елемент от тип ProfileViewModelItem, което ще използваме за настройка на потребителския потребител на клетката:

// това предполага, че вече имате всички подвидове на клетката: етикети, imagesViews и т.н.
клас NameAndPictureCell: UITableViewCell {
    вар елемент: ProfileViewModelItem? {
      didSet {
         // прехвърлете ProfileViewModelItem към подходящ тип елемент
         пази нека артикул = артикул като? ProfileViewModelNamePictureItem else {
            връщане
         }
         nameLabel? .text = item.name
         pictureImageView? .image = UIImage (с име: item.pictureUrl)
      }
   }
}
клас AboutCell: UITableViewCell {
   вар елемент: ProfileViewModelItem? {
      didSet {
         пази нека артикул = артикул като? ProfileViewModel AboutItem else {
            връщане
         }
         aboutLabel? .text = item.about
      }
   }
}
клас EmailCell: UITableViewCell {
    вар елемент: ProfileViewModelItem? {
      didSet {
         пази нека артикул = артикул като? ProfileViewModelEmailItem else {
            връщане
         }
         emailLabel? .text = item.email
      }
   }
}
клас FriendCell: UITableViewCell {
    вар артикул: Приятел? {
      didSet {
         пази нека елемент = елемент друго {
            връщане
         }
         ако нека pictureUrl = item.pictureUrl {
            pictureImageView? .image = UIImage (с име: pictureUrl)
         }
         nameLabel? .text = item.name
      }
   }
}
var item: Атрибут? {
   didSet {
      titleLabel? .text = item? .key
      valueLabel? .text = item? .value
   }
}

Някои от вас могат да зададат разумен въпрос: защо не използваме една и съща клетка за ProfileViewModelAboutItem и ProfileViewModelEmailItem, тъй като и двамата имат един текстов етикет? Отговорът е да, можем да използваме същата клетка. Но целта на този урок е да ви покаже начина на използване на различни типове клетки.

Не забравяйте да регистрирате клетките, ако искате да ги използвате като многократно използващи се клетки: UITableView има методи за регистриране на класовете на клетките или файловете в зависимост от начина, по който сте създали клетката.

Сега е време да използваме клетките в нашия TableView. Отново ViewModel ще се справи с това по много прост начин:

замени функцията tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   нека item = items [indexPath.section]
   Switch item.type {
   case .nameAndPicture:
      ако оставим cell = tableView.dequeueReusableCell (withIdentifier: NamePictureCell.identifier, for: indexPath) като? NamePictureCell {
         cell.item = елемент
         връща клетка
      }
   случай .about:
      ако нека cell = tableView.dequeueReusableCell (withIdentifier: AboutCell.identifier, for: indexPath) като? AboutCell {
         cell.item = елемент
         връща клетка
      }
   case .email:
      ако нека cell = tableView.dequeueReusableCell (withIdentifier: EmailCell.identifier, for: indexPath) като? EmailCell {
         cell.item = елемент
         връща клетка
      }
   случай. приятел:
      ако нека cell = tableView.dequeueReusableCell (withIdentifier: FriendCell.identifier, for: indexPath) като? FriendCell {
         cell.item = приятели [indexPath.row]
         връща клетка
      }
   case .attribute:
      ако нека cell = tableView.dequeueReusableCell (withIdentifier: AttributeCell.identifier, for: indexPath) като? AttributeCell {
         cell.item = атрибути [indexPath.row]
         връща клетка
      }
   }
   // върнете клетката по подразбиране, ако никой от горните не успее
   върнете UITableViewCell ()
}
Можете да използвате същата структура, за да настроите метода на делегата didSelectRowAt:
замени функцията tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      превключване на елементи [indexPath.section] .type {
          // направете подходящи действия за всеки тип
      }
}

И накрая, конфигурирайте headerView:

замени функцията tableView (_ tableView: UITableView, titleForHeaderInSection раздел: Int) -> String? {
   връщане на елементи [раздел] .sectionTitle
}

Изградете и стартирайте проекта си и се насладете на динамичния изглед на маса!

Изображение на резултата

За да тествате гъвкавостта, можете да промените файла JSON: добавете или премахнете някои приятели или премахнете част от данните напълно (просто не разрушавайте структурата на JSON, в противен случай няма да видите никакви данни). Когато преизграждате вашия проект, tableView ще изглежда и работи по начина, по който трябва, без никакви модификации на кода. Ще трябва да модифицирате ViewModel и ViewController само ако промените самия Модел: добавете ново свойство или драматично променете цялата му структура. Но това е съвсем различна история.

Можете да разгледате целия проект тук:

Благодаря за четенето! Ако имате въпроси или предложения - не се колебайте да питате!

В следващата статия ще надстроим съществуващия проект, за да добавим хубав ефект на колапс / разширяване на секциите.

Актуализация: проверете тук, за да научите как динамично да актуализирате този tableView, без да използвате метода ReloadData.

Пиша и за блога на American Express Engineering. Вижте и другите ми творби и произведенията на моите талантливи колеги в AmericanExpress.io.