Wie wir auf den TableAdapter gekommen sind und die Arbeit mit UITableView vereinfacht haben


Bei der Arbeit mit UITableViewwollte 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
        // 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


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


All Articles