DeklarativTVC
声明式 UIKit 集合
版本 1 的文档
项目目标
DeklarativTVC 旨在通过将它们转换为声明式视图简化与 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) }
)
]
}
}
特性
- 支持声明式 UITableViewController
- 不支持 UITableViewController 的声明式 UITableView
- 支持声明式 UICollectionViewController
- 不支持 UICollectionViewController 的声明式 UICollectionView
- 支持 Storyboard 表格和集合单元格
- 支持 Xib 表格和集合单元格
- 支持编码的表格和集合单元格
- 您可以混合使用 storyboard、xib 和编码的单元格
- 表格和集合的动画项更新
- 支持表格单元格固定和自动布局高度
需求
- iOS 11.0+
- Swift 4.2+
安装
DeclarativeTVC支持通过CocoaPods进行获取。
CocoaPods
要使用CocoaPods安装DeclarativeTVC,请在您的Podfile
中添加以下行。
pod 'DeclarativeTVC'
然后运行pod install
命令。有关CocoaPods的安装和使用细节,请访问其官方网站。
如何使用
让我们看看如何使用DeclarativeTVC创建一个简单的表格。
创建视图单元格
首先需要定义单元格。库支持与所有类型的单元格一起工作:在Storyboard中创建的单元格,使用xib创建的单元格和程序化创建的单元格。在同一个表格中可以混合使用这些类型的单元格。这里将展示每种方式的示例。
Storyboard
单元格必须继承自UITableViewCell或StoryboardTableViewCell类。
class SimpleCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
}
// or
class SimpleCell: StoryboardTableViewCell {
@IBOutlet weak var titleLabel: UILabel!
}
在从Storyboard创建单元格时,必须在Storyboard中指定单元格的标识符为类名。
Xib
单元格必须继承自XibTableViewCell类。
class SimpleXibCellVM: XibTableViewCell {
@IBOutlet weak var titleLabel: UILabel!
}
在从xib创建单元格时,必须使用与类名相同的xib文件名。
程序单元格
单元格必须继承自CodedTableViewCell类。
class SimpleCodeCellVM: CodedTableViewCell {
@IBOutlet weak var titleLabel: UILabel!
}
创建单元格的视图模型
接下来,为每个单元格创建一个视图模型。这是一个实现CellModel协议的结构。
struct SimpleCellVM: CellModel {
let titleText: String?
func apply(to cell: SimpleCell, containerView: UIScrollView) {
cell.titleLabel.text = titleText
}
}
通常需要描述填充单元格的字段,并创建一个apply(to: containerView:)
方法。containerView是表单元格对应的tableView,或者是如果这是集合元素模型,则是collectionView。
可选择的单元格
在模型中可以实现SelectableCellModel协议,以便可以选择单元格。
struct SimpleCellVM: CellModel, SelectableCellModel {
let titleText: String?
let selectCommand: Command
func apply(to cell: SimpleCell, containerView: UIScrollView) {
cell.titleLabel.text = titleText
}
}
单元格高度
默认情况下,单元格的高度通过自动布局计算,但也可以程序化设置它。
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()
。
章节
标题和页脚的实现与单元格类似。在 Storyboard 中,它们基于 UITableViewCell 而不是 UIView 构建。在 XIB 以及代码实现的情况下,它们继承自 UITableViewHeaderFooterView。在 Storyboard 中,标题将是单元格的 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 应用于 UITableViewController 的 tableView。在这种情况下,请使用 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)
需要注意的事项
- 在从Storyboard创建单元格时,必须在Storyboard中指定单元格的标识符为类名。
- 在从xib创建单元格时,必须使用与类名相同的xib文件名。
- 在使用更新表格动画时,单元格的哈希值 必须 不同。否则,应用程序会崩溃。
- 在使用动画时,当旧版和新版表格中的单元格哈希值相同时,单元格不会被更新。这可以用于例如编辑 UITextView 的同时更新其余表格,而不会失去对文本框的关注,如果保留编辑单元格的哈希值相同。
- 通过自动布局计算单元格和标题的高度时,需要在 Storiesboard 中相应地设置高度计算参数。
- 不能 将 TableDS 应用于 UITableViewController 的 tableView。在这种情况下,请使用 DeclarativeTVC。
- 如果不使用动画,则会更新表格中的所有单元格,而不考虑它们的哈希值。底层调用 tableView.reloadData()
- 库不需要单元格视图模型为结构,但在开发中这通常是隐含的,并且我从未将视图模型做成类。我认为使用类来制作视图模型会导致更困难的代码。