
Cuando trabajaba, UITableView
quería evitar escribir código de plantilla, lo que es aún más complicado si necesita actualizar el estado de la tabla de forma animada. Apple presentó su solución a este problema en WWDC 2019, pero solo funciona con iOS 13. Y nosotros, como estudio de desarrollo de aplicaciones móviles, no tenemos el lujo de elegir la versión mínima de iOS.
Por lo tanto, nos dimos cuenta de nuestra visión de un enfoque basado en datos para trabajar con tablas, al tiempo que simplificamos la configuración de las células en el camino. Y agregaron una actualización animada de la tabla, que se basa en el cálculo automático de las diferencias entre los datos antiguos y nuevos para el tiempo lineal O (n) . Todo esto lo hemos diseñado en una pequeña biblioteca llamada TableAdapter.
Lo que hicimos y cómo llegamos a esto se discutirá en el artículo.
Pros y contras de TableAdapter
pros
- Actualización de la tabla de animación
- Cálculo automático de diferencia en tiempo lineal
- No es necesario heredar una tabla, celda o modelo
- No más
dequeReusable...
- Configuración de celda de tipo seguro, encabezado / pie de página
- Inicialización celular de cualquier manera
- Configuración de sección fácil
- Fácil de expandir
Menos
, 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
Como resultado, obtuvimos una solución que le permite configurar convenientemente celdas, escribir menos código de plantilla y animación además. Al mismo tiempo, quedaba la posibilidad de configuraciones de tabla de bajo nivel y expansión funcional si fuera necesario.
Implementación de GitHub