iOS: Как да се изгради изглед на таблица с разглобяеми секции
Част 2. Продължете приемането на протоколи и MVVM с изгледи на таблици

Това е втората част от моята поредица от уроци в табличен изглед с множество типове клетки.
След като прочетох множеството отговори и съвети за първата част, реших да добавя няколко основни актуализации.
UITableViewController се променя на UIViewController с TableView като подвид.
Сега ViewModel отговаря на протокола TableViewDataSource. NumberOfRowsInSection, cellForRowAt и numberOfSections са част от ViewModel. Това поддържа ViewController и ViewModel разделени.
Моля, намерете окончателния актуализиран проект тук.
Благодаря на всички за приноса!
В първата част създадохме следния изглед на таблица:

В тази статия ще направим някои промени, за да може секцията да се сгъва:

За да добавим сгъваемото поведение, трябва да знаем две неща за секцията:
- секцията е сгъваема или не
- текущото състояние на секцията: свит / разгънат
Можем да добавим и двете свойства към съществуващ протокол ProfileViewModelItem:
протокол ProfileViewModelItem { тип вар: ProfileViewModelItemType {get} var sectionTitle: String {get} var rowCount: Int {get} var isCollapsible: Bool {get} var isCollapsed: Bool {get set} }
Обърнете внимание, че свойството isCollapsible има само getter, защото няма да е необходимо да го модифицираме.
След това добавяме по подразбиране стойност еCollapsible към разширението на протокола. Задаваме стойността по подразбиране на true:
разширение ProfileViewModelItem { var rowCount: Int { връщане 1 } var isCollapsible: Bool { връщане вярно } }
След като промените протокола, ще видите множество грешки при компилиране във всеки от ProfileViewModelItems. Поправете го, като добавите това свойство към всеки ViewModelItem:
клас ProfileViewModelNamePictureItem: ProfileViewModelItem { var isCollapsed = true }
Това са всички промени, които трябва да внесем в ViewModel. Останалата част е да промените изгледа, така че да може да се справи с действията за свиване / разгъване.
Няма начин за добавяне на сгъваемо поведение към tableView, така че ще имитираме по много прост начин: когато секцията е свита, ще зададем броя на нейните редове на нула. Когато се разшири, ще използваме по подразбиране rowCount за този раздел. За TableView можем да предоставим тази информация по метода numberOfRowsInSection:
замени функцията tableView (_ tableView: UITableView, numberOfRowsInSection раздел: Int) -> Int { нека item = viewModel.items [раздел] ако item.isCollapsible && item.isCollapsed { върнете 0 } връщане item.rowCount }
Сега трябва да създадем персонализиран изглед на заглавката, който ще съдържа заглавието и етикета със стрелка. Създайте подклас на UITableViewHeaderFooterView и задайте оформлението в xib или код:

клас HeaderView: UITableViewHeaderFooterView { @IBOutlet слаб вар заглавиеLabel: UILabel? @IBOutlet слаб var arrowLabel: UILabel?
вар раздел: Int = 0 }
Ще използваме променливата на секцията, за да съхраним текущия индекс на секцията, който ще ни трябва по-късно.
Когато потребителят докосне секцията, изгледът със стрелка трябва да се завърти надолу. Това можем да постигнем с разширение UIView:
разширение UIView { func rotate (_ toValue: CGFloat, продължителност: CFTimeInterval = 0.2) { нека анимация = CABasicAnimation (keyPath: „преобразуване.ротация“) animation.toValue = toValue animation.duration = продължителност animation.isRemovedOnCompletion = false animation.fillMode = kCAFillModeForwards self.layer.add (анимация, forKey: nil) } }
Това е само един от възможните начини за анимиране на въртенето на изгледа
Използвайки този метод на разширение, добавете следния код в класа HeaderView:
func setCollapsed (свит: Bool) { arrowLabel? .rotate (свит? 0.0: .pi) }
Когато наречем този метод за свито състояние, той ще завърти стрелката в първоначалното положение, за разширено състояние ще завърти стрелката към pi radians.
След това трябва да настроим текущото заглавие на секцията. Както направихме за клетките в предишния урок, създайте променливата на елемента и използвайте наблюдателя 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: клас { func toggleSection (заглавие: HeaderView, раздел: Int) }
Добавете свойство на делегат в HeaderView:
слаб вар делегат: HeaderViewDelegate?
И накрая, извикайте този метод на делегат от селектора на tapHeader:
@objc private func tapHeader (gestureRecognizer: UITapGestureRecognizer) { делегат? .toggleSection (заглавие: самостоятелно, раздел: раздел) }
HeaderView вече е готов за употреба. Нека го свържем с нашия ViewController.
Отворете ViewModel и го приведете в съответствие с TableViewDelegate:
разширение ProfileViewModel: UITableViewDelegate {
}
След това премахнете метода titleForHeaderInSection. Тъй като използваме персонализирана заглавка, ще зададем заглавие по друг начин:
func tableView (_ tableView: UITableView, viewForHeaderInSection раздел: Int) -> UIView? {
ако нека headerView = tableView.dequeueReusableHeaderFooterView (withIdentifier: HeaderView.identifier) като? HeaderView { headerView.item = viewModel.items [раздел] headerView.section = раздел headerView.delegate = себе // не забравяйте този ред !!!
връщане headerView } върнете UIView () }
За да работи dequeueReusableHeaderFooterView, не забравяйте да регистрирате headerView за tableView
След като настроите headerView.delegate за самостоятелно, ще забележите грешката в компилатора, защото нашата ViewModel все още не е в съответствие с протокола. Поправете го, като добавите друго разширение:
разширение ProfileViewModel: HeaderViewDelegate { func toggleSection (заглавие: HeaderView, раздел: Int) { var item = елементи [раздел]
ако item.isCollapsible {
// Превключване на колапса нека свит =! item.isCollapsed item.isCollapsed = свит header.setCollapsed (свит: свит)
// Регулирайте броя на редовете вътре в секцията } } }
Трябва да зададем начин да презаредим раздела TableView, така че той ще актуализира потребителския интерфейс. В по-сложните ViewModels, които изискват да актуализират, добавят или премахват tableViewRows, ще има смисъл да се използва делегат с множество методи. В нашия проект се нуждаем само от един метод (ReloadSection), така че можем да ни върнем обаждането:
клас ProfileViewModel: NSObject { var items = [ProfileViewModelItem] ()
// обратно извикване за презареждане на tableViewSections var reloadSections: ((_ раздел: Int) -> void)?
..... }
Извикайте този обратен разговор в toggleSection:
разширение ProfileViewModel: HeaderViewDelegate { func toggleSection (заглавие: HeaderView, раздел: Int) { var item = елементи [раздел]
ако item.isCollapsible {
// Превключване на колапса нека свит =! item.isCollapsed item.isCollapsed = свит header.setCollapsed (свит: свит)
// Регулиране на броя на редовете вътре в секцията reloadSections? (раздел) } } }
В ViewController използваме този обратен сигнал за презареждане на секциите tableView:
замени функцията viewDidLoad () { super.viewDidLoad () viewModel.reloadSections = {[слабо себе си] (раздел: Int) в самостоятелно? .tableView? .beginUpdates () самостоятелно? .tableView? .reloadSections ([раздел], с: .fade) самостоятелно? .tableView? .endUpdates () } ... }
Ако изградите и стартирате проекта, ще видите това хубаво анимационно поведение в колапс.
Можете да разгледате окончателния проект тук.
Има някои потенциални надстройки за тази функция:
- Опитайте се да измислите начина, по който да разрешите само един раздел да бъде разширен. Така че, когато потребителят докосне друг раздел, той първо ще свие разширения и след това ще разшири новия.
- Когато раздела се разширява, превъртете tableView, за да покажете последния ред в този раздел.
Моля, споделете мислите си в коментарите по-долу, за да можем да ги обсъдим.
Благодаря за четенето!