
在工作时,UITableView我想避免编写模板代码,如果您需要动态更新表的状态,则模板代码会更加复杂。Apple在WWDC 2019上展示了针对此问题的解决方案,但它仅适用于iOS13。作为移动应用程序开发工作室,我们没有选择最低版本的iOS的奢望。
因此,我们实现了以数据驱动的方式处理表的愿景,同时简化了沿途单元的配置。并且他们添加了一个动画表更新,该更新基于自动计算线性时间O(n)的新旧数据之间的差异。所有这些我们都设计到一个名为TableAdapter的小型库中。
我们将在本文中讨论我们的工作以及如何做到这一点。
TableAdapter的优缺点
优点
- 动画表更新
- 自动线性时间差计算
- 无需继承表,单元格或模型
- 不再 dequeReusable...
- 类型安全的单元设置,页眉/页脚
- 以任何方式初始化单元
- 容易的部分设置
- 易于扩展
缺点
, Hashable. , - Swift, .. . - .
– :
1. , /
1.1
, , Hashable.
extension User: Hashable { ... }
1.2
Configurable, . , .
extension Cell: Configurable {
    public func setup(with item: User) {
        textLabel?.text = item.name
    }
}
, , , . , setup(with:) .
1.3 /
/ Configurable, . / 1.3 .
2.
() , /. Section<Item, SectionId>, (Item) (SectionId). (id) Hashable, .
/
/ header/footer.
/
/ headerIdentifier/footerIdentifier, . , Configurable ( 1.3).
let section = Section<User, Int>(
    id: 0,
    items: users,
    header: "Users Section",
    footer: "Users Section",
    headerIdentifier: "HeaderReuseIdentifier",
    footerIdentifier: "FooterReuseIdentifier"
)
id .
3.
TableAdapter<Item, SectionId>, (Item) (SectionId). , .
CellReuseIdentifierProvider, . , IndexPath , .
ellDidSelectHandler, , IndexPath .
lazy var adapter = TableAdapter<User, Int>(
    tableView: tableView,
    cellIdentifierProvider: { (indexPath, item) -> String? in
        
    },
    cellDidSelectHandler: { [weak self] (table, indexPath, item) in
        
    }
)
:
adapter.update(with: [section], animated: true)
class ViewController: UIViewController {
    let tableView = ...
    lazy var adapter = TableAdapter<User, Int>(
        tableView: tableView,
        cellIdentifierProvider: { (indexPath, item) -> String? in
            return "CellReuseIdentifier"
        },
        cellDidSelectHandler: { [weak self] (table, indexPath, item) in
            
        }
    )
    let users: [User] = [...]
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTable()
        let section = Section<User, Int>(
            id: 0,
            items: users,
            header: "Users Section",
            footer: "Users Section",
            headerIdentifier: "HeaderReuseIdentifier",
            footerIdentifier: "FooterReuseIdentifier"
        )
        adapter.update(with: [section]], animated: true)
    }
    func setupTable() {
        tableView.register(
            Cell.self,
            forCellReuseIdentifier: "CellReuseIdentifier"
        )
        tableView.register(
            Header.self,
            forHeaderFooterViewReuseIdentifier identifier: "HeaderReuseIdentifier"
        )
        tableView.register(
            Footer.self,
            forHeaderFooterViewReuseIdentifier identifier: "FooterReuseIdentifier"
        )
    }
}
Hashable , (associated value).
, CellReuserIdentifierProvider. A , "Cell" :
tableView.register(
    Cell.self,
    forCellReuseIdentifier: adapter.defaultCellIdentifier
)
/
headerIdentifier/footerIdentifier . / , "Header"/"Footer".
tableView.register(
    HeaderView.self,
    forHeaderFooterViewReuseIdentifier identifier: adapter.defaultHeaderIdentifier
)
tableView.register(
    FooterView.self,
    forHeaderFooterViewReuseIdentifier identifier: adapter.defaultFooterIdentifier
)
Sender
, , , , TableAdapter'a sender. , / SenderConfigurable.
class ViewController: UIViewController {
    lazy var adapter = TableAdapter<User, Int>(
        tableView: tableView,
        sender: self
    )
}
extension Cell: SenderConfigurable {
    func setup(with item: User, sender: ViewController) {
        textLabel?.text = item.name
        delegate = sender
    }
}
: , . , setup(with:sender:) .
, CellProvider, :
lazy var adapter = TableAdapter<User, Int>(
    tableView: tableView,
    cellProvider: { (table, indexPath, user) in
        let cell = table.dequeueReusableCell(
            withIdentifier: "CellReuseIdentifier",
            for: indexPath
        ) as! Cell
        cell.setup(with: user)
        return cell
    }
)
/ TableAdapter .
(diff) , . , A technique for isolating differences between files. O(n) . Hashable.
, , performBatchUpdates(_:completion:) . . .
- () (id). .
let sectionsDiff = try calculateDiff(form: oldSections, to: newSections)
- , , . , , id. , , , . , , .
let intermediateSections = applyDiff(sectionsDiff, to: oldSections)
- ( id) . . , , .
let rowsDiff = try calculateRowsDiff(from: intermediateSections, to: newSections)
:
let diff = Diff(
    sections: sectionsDiff,
    rows: rowsDiff,
    intermediateData: intermediateSections,
    resultData: newSections
)
- : ( ) , .
data = diff.intermediateData
tableView.insertSections(diff.sections.inserts, with: animationType)
tableView.deleteSections(diff.sections.deletes, with: animationType)
diff.sections.moves.forEach { tableView.moveSection($0.from, toSection: $0.to) }
- : () , .
data = diff.resultData
tableView.deleteRows(at: diff.rows.deletes, with: animationType)
tableView.insertRows(at: diff.rows.inserts, with: animationType)
diff.rows.moves.forEach { tableView.moveRow(at: $0.from, to: $0.to) }
1: . «» «» .

2: , .. . , , , , . .. , . adapter.update(with: sections, animated: false) .
Configurable
, :
let user = users[indexPath.row]
let cell = tableView.dequeueReusableCell(
    withIdentifier: cellId,
    for: indexPath
) as! Cell
cell.setup(with: user)
. (associatedtype).
Configurable, :
protocol Configurable {
    associatedtype ItemType: Any
    func setup(with item: ItemType)
}
, dequeueReusableCell(withIdentifier:for:) Configurable - ItemType. AnyConfigurable:
protocol AnyConfigurable {
    func anySetup(with item: Any)
}
, :
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
if let cell = cell as? AnyConfigurable {
    cell.anySetup(with: item)
}
Configurable AnyConfigurable :
extension Configurable {
    func anySetup(with item: Any) {
        if let item = item as? ItemType {
            setup(with: item)
        }
    }
}
, - «» , . , .
, Swift type-erased wrappers. AnyHashable, AnyIndex.
: . : .
GitHub
结果,我们得到了一个解决方案,可让您方便地配置单元,另外编写更少的模板代码和动画。同时,如有必要,仍然可能进行低级表设置和功能扩展。
GitHub实施