DeclarativeTVC
Declarative UIKit collections
Документация для версии 1
Цель проекта
DeclarativeTVC создана для упрощения работы с UIKit коллекциями переведя взаимодействие с ними к декларативному виду.
Пример
Простейшая таблица может быть реализована таким образом
class TVC: DeclarativeTVC {
var rows: [CellAnyModel] {
didSet {
set(rows: rows)
}
}
override func viewDidLoad() {
super.viewDidLoad()
rows = [
SimpleCellVM(
titleText: "1",
selectCommand: Command { print(1) }
),
SimpleCellVM(
titleText: "2",
selectCommand: Command { print(2) }
)
]
}
}
Features
- Declarative UITableViewController support
- Declarative UITableView without UITableViewController support
- Declarative UICollectionViewController support
- Declarative UICollectionView without UICollectionViewController support
- Storyboard table and collection cells support
- Xib table and collection cells support
- Coded table and collection cells support
- You can mix storyboard, xib and coded cells
- Animated items update for tables and collections
- Support for fixed and autolayout height for table cells
Requirements
- iOS 11.0+
- Swift 4.2+
Installation
DeclarativeTVC is available through CocoaPods.
CocoaPods
To install DeclarativeTVC with CocoaPods, add the following lines to your Podfile
.
pod 'DeclarativeTVC'
Then run pod install
command. For details of the installation and usage of CocoaPods, visit its official website.
Как пользоваться
Рассмотрим создание простой таблицы с использованием DeclarativeTVC.
Создание view ячеек
Первым делом нужно описать ячейки. Библиотека поддерживает работы со всеми типами ячеек: ячейками созданными в storyboard, созданными с использованием xib и программно созданным ячейками. В одной таблице можно смешивать все эти типы ячеек. Рассмотрим примеры для каждого из этих способов.
Stryboard
Ячейка должна быть унаследована от класса UITableViewCell или StoryboardTableViewCell.
class SimpleCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
}
// or
class SimpleCell: StoryboardTableViewCell {
@IBOutlet weak var titleLabel: UILabel!
}
При создании ячейки из сториборда нужно в качестве идентификатора ячейки в сториборде указать названиие класса.
Xib
Ячейка должна быть унаследована от класса XibTableViewCell.
class SimpleXibCellVM: XibTableViewCell {
@IBOutlet weak var titleLabel: UILabel!
}
При создании ячейки из xib нужно в качестве имени файла xib использовать имя класса.
Программные ячейки
Ячейка должна быть унаследована от класса CodedTableViewCell.
class SimpleCodeCellVM: CodedTableViewCell {
@IBOutlet weak var titleLabel: UILabel!
}
Создание view моделей для ячеек
Далее нужно для каждой ячейки создать вьюмодель. Это структура реализующая протокол CellModel.
struct SimpleCellVM: CellModel {
let titleText: String?
func apply(to cell: SimpleCell, containerView: UIScrollView) {
cell.titleLabel.text = titleText
}
}
В ней обычно нужно описать поля, из которых потом будет заполняться ячейка, и создать метод apply(to:,containerView:)
. containerView - это либо tableView для этой ячейки таблицы, либо coolectionView, если это модель я для элемента коллекции.
Выбираемые ячейки
В модели можно реализовать протокол SelectableCellModel, чтобы ячейку можно было выбрать.
struct SimpleCellVM: CellModel, SelectableCellModel {
let titleText: String?
let selectCommand: Command
func apply(to cell: SimpleCell, containerView: UIScrollView) {
cell.titleLabel.text = titleText
}
}
Высота ячейки
По умолчанию высота ячейки рассчитывается с помощью auto layout, но можно задать ее и программно.
struct SimpleCellVM: CellModel, SelectableCellModel {
let titleText: String?
let selectCommand: Command
func apply(to cell: SimpleCell, containerView: UIScrollView) {
cell.titleLabel.text = titleText
}
func height(containerView: UIScrollView) -> CGFloat? { 200 }
}
containerView здесь - это tableView для этой ячейки
Способы различения вьюмоделей
Для расчета анимаций обновления таблицы библиотека должна различать ячейки между собой. Для этого протокол CellModel удовлетворяет протоколу Hashable. И все поля во вьюмодели должны также удовлетворять этому протоколу. Простые типы удовлетворяют ему автоматически. Экземпляры Command различаются по своему id. По умолчаниию id пустой и все команды равны друг другу.
let c1 = Command { print(1) }
let c2 = Command(id: "custom") { print(1) }
Если расчета хеша из коробки недостаточно, то можно определить расчет хеша вручную
struct SimpleCellVM: CellModel, SelectableCellModel {
let titleText: String?
let selectCommand: Command
func apply(to cell: SimpleCell, containerView: UIScrollView) {
cell.titleLabel.text = titleText
}
func height(containerView: UIScrollView) -> CGFloat? { 200 }
func hash(into hasher: inout Hasher) {
hasher.combine(titleText)
hasher.combine("custom")
}
}
Также каждая модель реализует следующие функции
func innerHashValue() -> Int
func innerEquatableValue() -> Int
func innerAnimationEquatableValue() -> Int
innerHashValue
- используется для расчета того, отличается ли ячейка от других в принципе. И если да, то она будет удалена или добавлена во время анимации. По умолчанию она возвращает текущий хэш модели.
innerAnimationEquatableValue
- используется для определия того поменялось ли содержимое ячейки, в случае когда на предыдущем шаге ячейка была признана той же самой. Если поменялось, то она будет анимирована в разделе reload rows для таблицы. По умолчанию также возвращает текущий хэш модели.
innerEquatableValue
- не используется в этой библиотеке, но используется в других библиотеках, написанных на основе этой. Смысл сравнения - выяснить после применения этой функции ко всем моделям таблицы, нужно ли вообще обновлять таблицу или можно пропустить этот шаг. По умолчанию возвращает innerAnimationEquatableValue()
.
Секции
Заголовки и подвалы реализованы аналогично ячейкам. В случая сторибордов они строятся они на базе UITableViewCell, а не UIView. В случае ксибов и реализации в коде они наследуются от UITableViewHeaderFooterView. В случае сторибордов заголовком будет contentView ячейки, поэтому если регистрировать экшены на ячейку, то они работать не будут.
open class XibTableViewHeaderFooterView: UITableViewHeaderFooterView {
}
open class CodedTableViewHeaderFooterView: UITableViewHeaderFooterView {
}
Регистрировать классы и xib не нужно, библиотека это сделает за вас. Но нужно соблюдать те же правила, что и при создании ячеек.
class HeaderView: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
}
struct HeaderViewVM: TableHeaderModel {
let titleText: String?
func apply(to header: HeaderView, containerView: UIScrollView) {
header.titleLabel.text = titleText
}
}
Для текстовых заголовков и подвалов есть специальные модели
public struct TitleWithoutViewTableHeaderModel: TableHeaderModel {
public let title: String
}
public struct TitleWithoutViewTableFooterModel: TableFooterModel {
public let title: String
}
Создание таблицы
Если таблица создается в варианте UITableViewController, то с этой библиотекой нужно использовать DeclarativeTVC.
open class DeclarativeTVC: UITableViewController {
Если в варианте UITableView, то используется TableDS.
open class TableDS: NSObject, UITableViewDelegate, UITableViewDataSource {
DeclarativeTVC
Класс DeclarativeTVC реализует следующие методы
open class DeclarativeTVC: UITableViewController {
open func set(rows: [CellAnyModel], animations: Animations? = nil) {
open func set(model: TableModel, animations: Animations? = nil) {
open override func numberOfSections(in tableView: UITableView) -> Int {
open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
open override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
open override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
open override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
open override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
open override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
open override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
open override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
Соответственно в общем случае он берет на себя ответственность за расчет количества секций и ячеек, созданиие заголовков, подвалов и ячеек, расчет высоты заголовков, подвалов и ячеек, отработку нажатия на ячейку.
С помощью метода set(rows: [CellAnyModel]
можно задать для таблицы массив ранее созданных вьюмоделей, что создаст таблицу без секций.
С помощью метода set(model: TableModel
можно задать для таблицы структуру, где помимо ячеек будут описаны и секциии.
public struct TableModel: Equatable {
public var sections: [TableSection]
public init(sections: [TableSection]) {
public init(rows: [CellAnyModel]) {
...
public struct TableSection {
public let header: TableHeaderAnyModel?
public let footer: TableFooterAnyModel?
public var rows: [CellAnyModel]
Простейшая таблица может быть реализована таким образом
class TVC: DeclarativeTVC {
var rows: [CellAnyModel] {
didSet {
set(rows: rows)
}
}
override func viewDidLoad() {
super.viewDidLoad()
rows = [
SimpleCellVM(
titleText: "1",
selectCommand: Command { print(1) }
),
SimpleCellVM(
titleText: "2",
selectCommand: Command { print(2) }
)
]
}
}
Вариант с различными типами ячеек
func tableRowThreeTypes() -> [CellAnyModel] {
[
SimpleCellVM(titleText: "Storyboard cell"),
SimpleXibCellVM(titleText: "Xib cell"),
SimpleCodeCellVM(titleText: "Coded cell.")
]
}
Вариант с секциямии
func tableWithSections() -> [TableSection] {
[
TableSection(
header: nil,
rows: [
SelectAnimationsCellVM(
titleText: "Select animations",
animationText: state.animationsTitle,
selectCommand: Command {
self.show(SelectAnimationsVC.self)
}),
ApplyAnimationsCellVM(
titleText: "Apply animations",
selectCommand: Command {
state.detailType = .tableWithSections2
self.reload()
})
],
footer: nil),
TableSection(
header: HeaderViewVM(titleText: "Header 1"),
rows: [
SimpleCellVM(titleText: "Paragraph 11"),
SimpleCellVM(titleText: "Paragraph 12"),
SimpleCellVM(titleText: "Paragraph 13"),
],
footer: nil),
TableSection(
header: HeaderViewVM(titleText: "Header 2"),
rows: [
SimpleCellVM(titleText: "Paragraph 21"),
SimpleCellVM(titleText: "Paragraph 22"),
SimpleCellVM(titleText: "Paragraph 23")
],
footer: nil),
]
}
TableDS
Использование TableDS отличается от использования DeclarativeTVC тем, что нужно еще при задании вьюмоделей задавать и tableView.
open class TableDS: NSObject, UITableViewDelegate, UITableViewDataSource {
open func set(tableView: UITableView?, rows: [CellAnyModel], animations: DeclarativeTVC.Animations? = nil) {
open func set(tableView: UITableView?, model: TableModel, animations: DeclarativeTVC.Animations? = nil) {
Нельзя применять TableDS к tableView из UITableViewController. В этом случае используйте DeclarativeTVC.
Анимации
При обновлениии данных таблицы можно задать анимацию для этого обновления.
public extension DeclarativeTVC {
struct Animations: Equatable {
let deleteSectionsAnimation: UITableView.RowAnimation
let insertSectionsAnimation: UITableView.RowAnimation
let reloadSectionsAnimation: UITableView.RowAnimation
let deleteRowsAnimation: UITableView.RowAnimation
let insertRowsAnimation: UITableView.RowAnimation
let reloadRowsAnimation: UITableView.RowAnimation
По умолчанию обновление происходит без анимации.
open func set(rows: [CellAnyModel], animations: DeclarativeTVC.Animations? = nil) {
В библиотеке есть одна предопределенная анимация, остальные делаются подобным образом.
static let fadeAnimations = Animations(deleteSectionsAnimation: .fade,
insertSectionsAnimation: .fade,
reloadSectionsAnimation: .fade,
deleteRowsAnimation: .fade,
insertRowsAnimation: .fade,
reloadRowsAnimation: .fade)
...
set(rows: rows, animations: DeclarativeTVC.fadeAnimations)
Что нужно помнить
- При создании ячейки из сториборда нужно в качестве идентификатора ячейки в сториборде указать названиие класса.
- При создании ячейки из xib нужно в качестве имени файла xib использовать имя класса.
- При использовании анимации для обновления таблицы хеши ячеек должны быть разные. Иначе приложение вылетит.
- При использовании анимации ячейки с одинаковыми хешами в старой и новой версии таблицы не обновляются. Это можно использовать, например, для редактирования UITextView и обновлениия остальной таблицы во время редактирования без потери фокуса на текстовом поле, если сохранять одинаковым хэш ячейки редактирования.
- При расчете высоты ячеек и заголовков посредством auto layout нужно в сториборде соответсвенно настроить параметры расчета высоты.
- Нельзя применять TableDS к tableView из UITableViewController. В этом случае используйте DeclarativeTVC.
- Если анимации не используются, то происходит обновление всех ячеек таблицы вне зависимости от их хэша. Под капотом вызывается tableView.reloadData()
- Библиотека не требует, чтобы вьюмодели ячеек были структурами, но при разработке это неявно подразумевалось и мною вьюмодели никогда не делались классами. На мой взгляд использование классов для вьюмоделей приводит к более проблемному коду.
Источники
Создание библиотеки было вдохновлено выступлением Alexander Zimin