
عند العمل مع ، UITableViewأردت تجنب كتابة رمز القالب ، والذي يكون أكثر تعقيدًا إذا كنت بحاجة إلى تحديث حالة الجدول بشكل متحرك. قدمت Apple حلها لهذه المشكلة في WWDC 2019 ، لكنها تعمل فقط مع iOS 13. ونحن ، بصفتنا استوديو تطوير تطبيقات الهاتف المحمول ، ليس لدينا ترف في اختيار الحد الأدنى من إصدار iOS.
لذلك ، أدركنا رؤيتنا لنهج قائم على البيانات للعمل مع الجداول ، مع تبسيط تكوين الخلايا على طول الطريق. وأضافوا تحديث جدول متحرك ، يعتمد على الحساب التلقائي للاختلافات بين البيانات القديمة والجديدة للوقت الخطي O (n) . كل هذا قمنا بتصميمه في مكتبة صغيرة تسمى TableAdapter.
ما قمنا به وكيف وصلنا إلى هذا سيتم مناقشته في المقالة.
إيجابيات وسلبيات TableAdapter
الايجابيات
- تحديث جدول الرسوم المتحركة
 - حساب الفرق التلقائي في الوقت الخطي
 - لا حاجة لوراثة جدول أو خلية أو نموذج
 - لا أكثر 
dequeReusable... - إعداد خلية آمنة من النوع ، رأس / تذييل
 - تهيئة الخلية بأي شكل من الأشكال
 - إعداد القسم السهل
 - سهل التوسع
 
السلبيات
, 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
ونتيجة لذلك ، حصلنا على حل يتيح لك تكوين الخلايا بشكل ملائم وكتابة كود قالب أقل ورسوم متحركة بالإضافة إلى ذلك. في الوقت نفسه ، لا تزال هناك إمكانية لإعدادات طاولة منخفضة المستوى والتوسع الوظيفي إذا لزم الأمر.
تنفيذ جيثب