How we came up with the TableAdapter and simplified the work with UITableView


When working with, UITableViewI 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
        // 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


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


All Articles