
Ao trabalhar com, UITableView
eu queria evitar escrever código de modelo, o que é ainda mais complicado se você precisar atualizar o estado da tabela animadamente. A Apple apresentou sua solução para esse problema na WWDC 2019, mas funciona apenas com o iOS 13. E nós, como estúdio de desenvolvimento de aplicativos móveis, não temos o luxo de escolher a versão mínima do iOS.
Portanto, percebemos nossa visão de uma abordagem orientada a dados para trabalhar com tabelas, simplificando a configuração das células ao longo do caminho. E eles adicionaram uma atualização de tabela animada, que se baseia no cálculo automático das diferenças entre dados antigos e novos para o tempo linear O (n) . Tudo isso foi criado em uma pequena biblioteca chamada TableAdapter.
O que fizemos e como chegamos a isso será discutido no artigo.
Prós e contras do TableAdapter
profissionais
- Atualização da tabela de animação
- Cálculo automático de diferença para tempo linear
- Não há necessidade de herdar uma tabela, célula ou modelo
- Não mais
dequeReusable...
- Configuração de célula de tipo seguro, cabeçalho / rodapé
- Inicialização celular de qualquer maneira
- Configuração fácil da seção
- Fácil de expandir
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
Como resultado, obtivemos uma solução que permite configurar convenientemente as células, além de escrever menos código de modelo e animação. Ao mesmo tempo, havia a possibilidade de configurações de tabela de baixo nível e expansão funcional, se necessário.
Implementação do GitHub