
Bei der Arbeit mit UITableView
wollte ich vermeiden, Vorlagencode zu schreiben, was noch komplizierter ist, wenn Sie den Status der Tabelle animiert aktualisieren müssen. Apple hat auf der WWDC 2019 seine Lösung für dieses Problem vorgestellt, die jedoch nur mit iOS 13 funktioniert. Als Entwicklungsstudio für mobile Anwendungen haben wir nicht den Luxus, die Mindestversion von iOS zu wählen.
Daher haben wir unsere Vision eines datengesteuerten Ansatzes für die Arbeit mit Tabellen verwirklicht und gleichzeitig die Konfiguration von Zellen auf dem Weg vereinfacht. Und sie fügten eine animierte Tabellenaktualisierung hinzu, die auf der automatischen Berechnung der Unterschiede zwischen alten und neuen Daten für die lineare Zeit O (n) basiert . All dies haben wir in einer kleinen Bibliothek namens TableAdapter entworfen.
Was wir getan haben und wie wir dazu gekommen sind, wird im Artikel besprochen.
Vor- und Nachteile von TableAdapter
Profis
- Aktualisierung der Animationstabelle
- Automatische Differenzberechnung in linearer Zeit
- Keine Notwendigkeit, eine Tabelle, eine Zelle oder ein Modell zu erben
- Nicht mehr
dequeReusable...
- Typensichere Zelleneinrichtung, Kopf- / Fußzeile
- Zellinitialisierung in irgendeiner Weise
- Einfache Abschnittseinrichtung
- Einfach zu erweitern
Minuspunkte
, 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
Als Ergebnis haben wir eine Lösung erhalten, mit der Sie Zellen bequem konfigurieren, weniger Vorlagencode schreiben und zusätzlich Animationen erstellen können. Gleichzeitig blieb die Möglichkeit von Tabelleneinstellungen auf niedriger Ebene und ggf. Funktionserweiterungen bestehen.
GitHub-Implementierung