Como criamos o TableAdapter e simplificamos o trabalho com o UITableView


Ao trabalhar com, UITableVieweu 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
        // Return cell reuse identifier for item at indexPath
    },
    cellDidSelectHandler: { [weak self] (table, indexPath, item) in
        // Handle cell selection for item at indexPath
    }
)

:


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
            // Handle cell selection for item at indexPath
        }
    )

    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:) . . .



  1. () (id). .

let sectionsDiff = try calculateDiff(form: oldSections, to: newSections)

  1. , , . , , id. , , , . , , .

let intermediateSections = applyDiff(sectionsDiff, to: oldSections)

  1. ( id) . . , , .

let rowsDiff = try calculateRowsDiff(from: intermediateSections, to: newSections)

:


let diff = Diff(
    sections: sectionsDiff,
    rows: rowsDiff,
    intermediateData: intermediateSections,
    resultData: newSections
)


  1. : ( ) , .

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) }

  1. : () , .

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


All Articles