كيف توصلنا إلى TableAdapter وتبسيط العمل مع UITableView


عند العمل مع ، 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
        // 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


ونتيجة لذلك ، حصلنا على حل يتيح لك تكوين الخلايا بشكل ملائم وكتابة كود قالب أقل ورسوم متحركة بالإضافة إلى ذلك. في الوقت نفسه ، لا تزال هناك إمكانية لإعدادات طاولة منخفضة المستوى والتوسع الوظيفي إذا لزم الأمر.


تنفيذ جيثب


All Articles