tonyarnold / Differ

Swift library to generate differences and patches between collections.
https://tonyarnold.github.io/Differ/
MIT License
664 stars 74 forks source link

Reordering section and rows ends up in a total mess #85

Open Donny1995 opened 2 years ago

Donny1995 commented 2 years ago

I have performed the following:

data before:

[
    TestData(id: "SECTION_0", rows: [
        "S0_R0",
        "S0_R1",
        "S0_R2",
        "S0_R3",
        "S0_R4",
        "S0_R5"
    ]),
    TestData(id: "SECTION_1", rows: [
        "S1_R0",
        "S1_R1"
    ]),
]

data after:

[
    TestData(id: "SECTION_1", rows: [
        "S1_R0",
        "S1_R1"
    ]),
    TestData(id: "SECTION_0", rows: [
        "S0_R0",
        "S0_R4", // <-
        "S0_R1",
        "S0_R2",
        "S0_R3", // ->
        "S0_R5"
    ]),
]

We move 1 section up, and we move 1 row up As a result i see a total mess of rows and sections What am i doing wrong?

This behavior is easily reproduced by the following code:

import Foundation
import UIKit

struct TestData: CollectionDecorator {
    let id: String
    let rows: [String]

    typealias InnerCollectionType = [String]
    var collection: [String] { rows }
}

class TestController2: UIViewController {

    let mTableView = UITableView()

    var data: [TestData] = [
        TestData(id: "SECTION_0", rows: [
            "S0_R0",
            "S0_R1",
            "S0_R2",
            "S0_R3",
            "S0_R4",
            "S0_R5"
        ]),
        TestData(id: "SECTION_1", rows: [
            "S1_R0",
            "S1_R1"
        ]),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        addSubview(mTableView)
        mTableView.translatesAutoresizingMaskIntoConstraints = false
        mTableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        mTableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        mTableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        mTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        mTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        mTableView.dataSource = self

        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
            guard let self = self else { return }
            self.recursiveShuffle()
        }
    }

    func recursiveShuffle() {

        let newData = [
            TestData(id: "SECTION_1", rows: [
                "S1_R0",
                "S1_R1"
            ]),
            TestData(id: "SECTION_0", rows: [
                "S0_R0",
                "S0_R4", // <-
                "S0_R1",
                "S0_R2",
                "S0_R3", // ->
                "S0_R5"
            ]),
        ]

        updateTable(newData: newData) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
                guard let self = self else { return }

                let newDataBack = [
                    TestData(id: "SECTION_0", rows: [
                        "S0_R0",
                        "S0_R1", //->
                        "S0_R2",
                        "S0_R3",
                        "S0_R4", //<-
                        "S0_R5"
                    ]),
                    TestData(id: "SECTION_1", rows: [
                        "S1_R0",
                        "S1_R1"
                    ]),
                ]

                self.updateTable(newData: newDataBack) {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
                        guard let self = self else { return }
                        self.recursiveShuffle()
                    }
                }
            }
        }
    }
}

extension TestController2: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return data.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data[section].count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let text = data[indexPath.section][indexPath.row]
        cell.textLabel?.text = text

        if text.hasPrefix("S0") {
            cell.backgroundColor = .green
        } else {
            cell.backgroundColor = .red
        }

        return cell
    }

    func updateTable(newData: [TestData], completion: @escaping () -> Void) {

        let diff = data.nestedExtendedDiff(to: newData, isEqualSection: { $0.id == $1.id }, isEqualElement: { $0 == $1 })

        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        data = newData
        mTableView.apply(diff, indexPathTransform: { $0 }, sectionTransform: { $0 })
        CATransaction.commit()
    }
}

/// Simply need to make something a collection is easy, if it has a collection inside
protocol CollectionDecorator: Collection {
    associatedtype InnerCollectionType: Collection
    var collection: InnerCollectionType { get }
}

extension CollectionDecorator {

    typealias Index = InnerCollectionType.Index
    typealias Element = InnerCollectionType.Element
    typealias Iterator = InnerCollectionType.Iterator
    typealias SubSequence = InnerCollectionType.SubSequence
    typealias Indices = InnerCollectionType.Indices

    func makeIterator() -> InnerCollectionType.Iterator { collection.makeIterator() }
    var underestimatedCount: Int { collection.underestimatedCount }
    func withContiguousStorageIfAvailable<R>(_ body: (UnsafeBufferPointer<Element>) throws -> R) rethrows -> R? {
        try collection.withContiguousStorageIfAvailable(body)
    }

    var startIndex: Self.Index { collection.startIndex }
    var endIndex: Self.Index { collection.endIndex }

    subscript(position: Self.Index) -> Self.Element {
        return collection[position]
    }

    subscript(bounds: Range<Self.Index>) -> Self.SubSequence {
        return collection[bounds]
    }

    var indices: Self.Indices { return collection.indices }
    var isEmpty: Bool { return collection.isEmpty }
    var count: Int { return collection.count }

    func index(_ i: Self.Index, offsetBy distance: Int) -> Self.Index {
        return collection.index(i, offsetBy: distance)
    }

    func index(_ i: Self.Index, offsetBy distance: Int, limitedBy limit: Self.Index) -> Self.Index? {
        return collection.index(i, offsetBy: distance, limitedBy: limit)
    }

    func distance(from start: Self.Index, to end: Self.Index) -> Int {
        return collection.distance(from: start, to: end)
    }

    func index(after i: Self.Index) -> Self.Index {
        collection.index(after: i)
    }

    func formIndex(after i: inout Self.Index) {
        collection.formIndex(after: &i)
    }
}
tonyarnold commented 2 years ago

How are you providing equality for these types? Based on the id property?

Donny1995 commented 2 years ago

How are you providing equality for these types? Based on the id property?

Yeah, by 'id' My active hypothesis for this is: "There can be only one change per item", it is true for rows, and for sections So, if someone moves a section, maybe the number of items in it must stay the same?

But in my implementation of table animator i disabled the actual moving of sections, because it messed things up the same way: (rows appeared where they did not belong, and after some iterations table crashes)