
When working with, UITableView
I wanted to avoid writing template code, which is even more complicated if you need to update the state of the table animatedly. Apple presented its solution to this problem at WWDC 2019, but it only works with iOS 13. And we, as a mobile application development studio, do not have the luxury of choosing the minimum version of iOS.
Therefore, we realized our vision of a data-driven approach for working with tables, while simplifying the configuration of cells along the way. And they added an animated table update, which is based on the automatic calculation of the differences between old and new data for linear time O (n) . All of this we have designed into a small library called TableAdapter.
What we did and how we came to this will be discussed in the article.
Pros and Cons of TableAdapter
pros
- Animation table update
- Automatic difference calculation for linear time
- No need to inherit a table, cell or model
- No more
dequeReusable...
- Type-safe cell setup, header / footer
- Cell initialization in any way
- Easy section setup
- Easy to expand
Minuses
, 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
As a result, we got a solution that allows you to conveniently configure cells, write less template code and animation in addition. At the same time, there remained the possibility of low-level table settings and functional expansion if necessary.
GitHub implementation