DeklarativTVC 2.0.0

DeklarativTVC 2.0.0

Kocherovets Dmitry 维护。



  • Dmitry Kocherovets

DeklarativTVC

CocoaPods Version License Platforms Swift Version

声明式 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中指定单元格的标识符为类名。

Screenshot 2020-02-08 at 22 55 43

Screenshot 2020-02-08 at 22 55 53

 Xib

单元格必须继承自XibTableViewCell类。

class SimpleXibCellVM: XibTableViewCell {
    @IBOutlet weak var titleLabel: UILabel!
}

在从xib创建单元格时,必须使用与类名相同的xib文件名。

Screenshot 2020-02-09 at 10 02 24

 程序单元格

单元格必须继承自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()
  • 库不需要单元格视图模型为结构,但在开发中这通常是隐含的,并且我从未将视图模型做成类。我认为使用类来制作视图模型会导致更困难的代码。

来源

创建库的灵感来源于 演讲 亚历山大·齐眠