ListDiffable

ListDiffable is the basis for data driven updates to IGListKit, which is a Protocol that defines our data sources.

- (nonnull id<NSObject>)diffIdentifier;
Copy the code

The above method is used to provide each of our data with a unique identifier, same diffIdentifier data will be filtered through objectsWithDuplicateIdentifiersRemoved before refresh, so when you define multiple data sources, but only a display, There is a high probability that you have a diffIdentifier problem.

- (BOOL)isEqualToDiffableObject:(nullable id<IGListDiffable>)object;
Copy the code

In IGListDiff, if the new element appears in the old array, the above method is used to compare whether it has changed. If it has changed, if (n! = o && ! [n isEqualToDiffableObject: o]) has been marked as needs to be updated. The specific Diff algorithm is not analyzed, you can view it if you are interested

static id IGListDiffing(BOOL returnIndexPaths, NSInteger fromSection, NSInteger toSection, NSArray
       
        > *oldArray, NSArray
        
         > *newArray, IGListDiffOption option, IGListExperiment experiments)
        
       
Copy the code

With this method we get a final result with insert, move, DELETE, and UPDATE list information.

If you’re happy to use IGListKit at this point, you might run into some other problems with complex UicollectionViews.

case

Below by analyzing a simple IGListBindingSectionController used to analyze the possible problems in the development.

Requirements: we need a IGListBindingSectionController achieve the function of a history search click delete.

We prepare four classes: HistorySection, HistorySectionController, HistoryItem, and HistoryCell

class HistorySection: ListDiffable {
    var items: [HistoryItem] = []
    
    // collectionView has only one history partition
    /// So the fixed string is returned, and the HistorySection should be considered one even if it is recreated
    func diffIdentifier(a) -> NSObjectProtocol {
        return "HistorySection" as NSString
    }
    
    IGListDiff -> if (n! = o && ! [] n isEqualToDiffableObject: o) understand the meaning of returns true
    func isEqual(toDiffableObject object: ListDiffable?). -> Bool {
        return true}}class HistoryItem: ListDiffable {
    var keyword: String
    
    init(keyword: String) {
        self.keyword = keyword
    }
    
    /// The identity of each historical search is related to the keyword
    func diffIdentifier(a) -> NSObjectProtocol {
        return "HistoryItem.\(keyword)" as NSString
    }
    
    func isEqual(toDiffableObject object: ListDiffable?). -> Bool {
        return true}}class HistorySectionController: ListBindingSectionController<HistorySection>, ListBindingSectionControllerDataSource {
    override init(a) {
        super.init()
        dataSource = self
        inset = .init(top: 10, left: 10, bottom: 10, right: 10)
        minimumLineSpacing = 10
        minimumInteritemSpacing = 10
    }
    
    func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, viewModelsFor object: Any)- > [ListDiffable] {
        return self.object?.items ??[]}func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, cellForViewModel viewModel: Any.at index: Int) -> UICollectionViewCell & ListBindable {
        return collectionContext!.dequeueReusableCell(of: HistoryCell.self, for: self, at: index) as! HistoryCell
    }
    
    func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, sizeForViewModel viewModel: Any.at index: Int) -> CGSize {
        return CGSize(width: 100, height: 30)}}class HistoryCell: UICollectionViewCell.ListBindable {
    let label = UILabel().then {
        $0.textAlignment = .center
        $0.backgroundColor = .red
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        contentView.addSubview(label)
    }
    
    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")}override func layoutSubviews(a) {
        super.layoutSubviews()
        label.frame = self.bounds
    }
    
    func bindViewModel(_ viewModel: Any) {
        guard let item = viewModel as? HistoryItem else { return }
        label.text = item.keyword
    }
}


class HistoryViewController: UIViewController.ListAdapterDataSource.ListBindingSectionControllerSelectionDelegate {
    .Omit other codefunc objects(for listAdapter: ListAdapter)- > [ListDiffable] {
        return [historySection]
    }
    
    func listAdapter(_ listAdapter: ListAdapter.sectionControllerFor object: Any) -> ListSectionController {
        return HistorySectionController().then {
            $0.selectionDelegate = self}}func emptyView(for listAdapter: ListAdapter) -> UIView? {
        return nil
    }
    
    func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, didSelectItemAt index: Int.viewModel: Any) {
        historySection.items.remove(at: index)
        adapter.performUpdates(animated: true, completion: nil)}}Copy the code

Soon you finish writing the above code, and based on what you know, you think the current code is ok. Then you hit Delete, and the program doesn’t seem to respond except for the crash caused by clicking delete repeatedly.

At this point you may go back to the source of the problem, or you may try directly, as in the following example

        let newSection = HistorySection(historySection.items)
        newSection.items.remove(at: index)
        historySection = newSection
        adapter.performUpdates(animated: true, completion: nil)
Copy the code

And then I run it, and obviously it works. But why?

// this method is what updates the "source of truth" // this should only be called just before the collection view is updated - (void)_updateObjects:(NSArray *)objects dataSource:(id<IGListAdapterDataSource>)dataSource { ... Omit some code... for (id object in objects) { ... Omit some code... // check if the item has changed instances or is new const NSInteger oldSection = [map sectionForObject:object]; if (oldSection == NSNotFound || [map objectForSection:oldSection] ! = object) { [updatedObjects addObject:object]; }... Omit some code... }... Omit some code... for (id object in updatedObjects) { [[map sectionControllerForObject:object] didUpdateToObject:object]; }... Omit some code... }Copy the code

With this part of the code, we get updatedObjects based on the Section object change, Which Section corresponding SectionController call didUpdateToObject refresh (interested can go to check what did IGListBindingSectionController). This is what we need to achieve externally with the Diff algorithm unchanged, but internally with the refresh.

Given the flow of the refresh call, is there anything in the code above that needs to be optimized? $0. SelectionDelegate = self; change the HistorySection to a new object through the callback. And finally for the subsequent call to didUpdateToObject. So we can make the following changes to remove all the extra code from the ViewController.

class HistoryViewController: UIViewController.ListAdapterDataSource.ListBindingSectionControllerSelectionDelegate {
    .Omit other codefunc objects(for listAdapter: ListAdapter)- > [ListDiffable] {
        return [historySection]
    }
    
    func listAdapter(_ listAdapter: ListAdapter.sectionControllerFor object: Any) -> ListSectionController {
        return HistorySectionController()}func emptyView(for listAdapter: ListAdapter) -> UIView? {
        return nil}}Copy the code

HistorySectionController below the add method, also can get the same effect (in need can go to consult ListBindingSectionController didUpdateToObject)

    override func didSelectItem(at index: Int) {
        super.didSelectItem(at: index)
        self.object?.items.remove(at: index)
        update(animated: true, completion: nil)}Copy the code

Above it records the flow of a refresh and some reasons for optimization to prevent redundant logical code from being written