Swift library to generate differences and patches between collections.

Overview

Differ

Build Status codecov

Differ generates the differences between Collection instances (this includes Strings!).

It uses a fast algorithm (O((N+M)*D)) to do this.

Features

  • ⚡️ It is fast
  • Differ supports three types of operations:
    • Insertions
    • Deletions
    • Moves (when using ExtendedDiff)
  • Arbitrary sorting of patches (Patch)
  • Utilities for updating UITableView and UICollectionView in UIKit, and NSTableView and NSCollectionView in AppKit
  • Calculating differences between collections containing collections (use NestedDiff)

Why do I need it?

There's a lot more to calculating diffs than performing table view animations easily!

Wherever you have code that propagates added/removed/moved callbacks from your model to your user interface, you should consider using a library that can calculate differences. Animating small batches of changes is usually going to be faster and provide a more responsive experience than reloading all of your data.

Calculating and acting on differences should also aid you in making a clear separation between data and user interface, and hopefully provide a more declarative approach: your model performs state transition, then your UI code performs appropriate actions based on the calculated differences to that state.

Diffs, patches and sorting

Let's consider a simple example of using a patch to transform string "a" into "b". The following steps describe the patches required to move between these states:

Change Result
Delete the item at index 0 ""
Insert b at index 0 "b"

If we want to perform these operations in different order, simple reordering of the existing patches won't work:

Change Result
Insert b at index 0 "ba"
Delete the item at index 0 "a"

...whoops!

To get to the correct outcome, we need to shift the order of insertions and deletions so that we get this:

Change Result
Insert b at index 1 "ab"
Delete the item at index 0 "b"

Solution

In order to mitigate this issue, there are two types of output:

  • Diff
    • A sequence of deletions, insertions, and moves (if using ExtendedDiff) where deletions point to locations of an item to be deleted in the source and insertions point to the items in the output. Differ produces just one Diff.
  • Patch
    • An ordered sequence of steps to be applied to the source collection that will result in the second collection. This is based on a Diff, but it can be arbitrarily sorted.

Practical sorting

In practice, this means that a diff to transform the string 1234 into 1 could be described as the following set of steps:

DELETE 1
DELETE 2
DELETE 3

The patch to describe the same change would be:

DELETE 1
DELETE 1
DELETE 1

However, if we decided to sort it so that deletions and higher indices are processed first, we get this patch:

DELETE 3
DELETE 2
DELETE 1

How to use

Table and Collection Views

The following will automatically animate deletions, insertions, and moves:

tableView.animateRowChanges(oldData: old, newData: new)

collectionView.animateItemChanges(oldData: old, newData: new, updateData: { self.dataSource = new })

It can work with sections, too!

tableView.animateRowAndSectionChanges(oldData: old, newData: new)

collectionView.animateItemAndSectionChanges(oldData: old, newData: new, updateData: { self.dataSource = new })

You can also calculate diff separately and use it later:

// Generate the difference first
let diff = dataSource.diff(newDataSource)

// This will apply changes to dataSource.
let dataSourceUpdate = { self.dataSource = newDataSource }

// ...

tableView.apply(diff)

collectionView.apply(diff, updateData: dataSourceUpdate)

Please see the included examples for a working sample.

Note about updateData

Since version 2.0.0 there is now an updateData closure which notifies you when it's an appropriate time to update dataSource of your UICollectionView. This addition refers to UICollectionView's performbatchUpdates:

If the collection view's layout is not up to date before you call this method, a reload may occur. To avoid problems, you should update your data model inside the updates block or ensure the layout is updated before you call performBatchUpdates(_:completion:).

Thus, it is recommended to update your dataSource inside updateData closure to avoid potential crashes during animations.

Using Patch and Diff

When you want to determine the steps to transform one collection into another (e.g. you want to animate your user interface according to changes in your model), you could do the following:

let from: T
let to: T

// patch() only includes insertions and deletions
let patch: [Patch<T.Iterator.Element>] = patch(from: from, to: to)

// extendedPatch() includes insertions, deletions and moves
let patch: [ExtendedPatch<T.Iterator.Element>] = extendedPatch(from: from, to: to)

When you need additional control over ordering, you could use the following:

let insertionsFirst = { element1, element2 -> Bool in
    switch (element1, element2) {
    case (.insert(let at1), .insert(let at2)):
        return at1 < at2
    case (.insert, .delete):
        return true
    case (.delete, .insert):
        return false
    case (.delete(let at1), .delete(let at2)):
        return at1 < at2
    default: fatalError() // Unreachable
    }
}

// Results in a list of patches with insertions preceding deletions
let patch = patch(from: from, to: to, sort: insertionsFirst)

An advanced example: you would like to calculate the difference first, and then generate a patch. In certain cases this can result in a performance improvement.

D is the length of a diff:

  • Generating a sorted patch takes O(D^2) time.
  • The default order takes O(D) to generate.
// Generate the difference first
let diff = from.diff(to)

// Now generate the list of patches utilising the diff we've just calculated
let patch = diff.patch(from: from, to: to)

If you'd like to learn more about how this library works, Graph.playground is a great place to start.

Performance notes

Differ is fast. Many of the other Swift diff libraries use a simple O(n*m) algorithm, which allocates a 2 dimensional array and then walks through every element. This can use a lot of memory.

In the following benchmarks, you should see an order of magnitude difference in calculation time between the two algorithms.

Each measurement is the mean time in seconds it takes to calculate a diff, over 10 runs on an iPhone 6.

Diff Dwifft
same 0.0213 52.3642
created 0.0188 0.0033
deleted 0.0184 0.0050
diff 0.1320 63.4084

You can run these benchmarks yourself by checking out the Diff Performance Suite.

All of the above being said, the algorithm used by Diff works best for collections with small differences between them. However, even for big differences this library is still likely to be faster than those that use the simple O(n*m) algorithm. If you need better performance with large differences between collections, please consider implementing a more suitable approach such as Hunt & Szymanski's algorithm and/or Hirschberg's algorithm.

Requirements

Differ requires at least Swift 5.4 or Xcode 12.5 to compile.

Installation

You can add Differ to your project using Carthage, CocoaPods, Swift Package Manager, or as an Xcode subproject.

Carthage

github "tonyarnold/Differ"

CocoaPods

pod 'Differ'

Acknowledgements

Differ is a modified fork of Wojtek Czekalski's Diff.swift - Wojtek deserves all the credit for the original implementation, I am merely its present custodian.

Please, file issues with this fork here in this repository, not in Wojtek's original repository.

Comments
  • Fix NSTableView animations

    Fix NSTableView animations

    NSTableView

    • Now based on patches, because NSTableView needs moves to be executed in order
    • Changed Interface to use rowIndices, because there are not IndexPaths involved in the row animations of it
    • I deprecated the previous methods for compatibility

    UITableView

    • ~~prevent beginUpdates/endUpdates for empty diff~~ removed after discussion
    opened by hannesoid 11
  • Fix stack overflow when diffing large collections on background threads

    Fix stack overflow when diffing large collections on background threads

    Fixes https://github.com/tonyarnold/Differ/issues/63. Fixes https://github.com/tonyarnold/Differ/issues/44.

    As I mentioned over there, the issue is a stack overflow in large data sets due to recursive implementations of various methods operating on linked lists. I am unsure as to why this wasn't an issue on the main thread, but this fix should be an improvement nonetheless.

    The recursive methods could easily be converted to be iterative or removed altogether by conforming the linked lists to Sequence and using for in loops to iterate over them.

    I'm not sure if we should include the large, previously-failing test case as it takes quite a few seconds to run. It needs to be large enough to cause the stack overflow though.

    opened by jenox 7
  • Does it work asynchronously?

    Does it work asynchronously?

    I tried to use code like DispatchQueue.global().async { ....patch(from:to:)... } and it always throws a "EXC_BAD_ACCESS" error with long texts(4000 elements each). What can I do about it?

    Bug 
    opened by cointowitcher 7
  • Extended diff produces unexpected results

    Extended diff produces unexpected results

    I am not sure if this is an issue, or I just don't understand how it works. I am really confused and looking for help, thanks in advance for your support.

    I have an array of Int, from 1 to 4. I swap first and last elements:

    let old = [1, 2, 3, 4]
    let new = [4, 2, 3, 1]
    old.diff(new).elements // [D(0), I(0), D(3), I(3)]
    old.extendedDiff(new).elements // [M(0,3), M(3,0)]
    

    I can do the same for array of Int with more elements:

    let old = [1, 2, 3, 4, 5]
    let new = [5, 2, 3, 4, 1]
    old.diff(new).elements // [D(0), I(0), D(4), I(4)]
    old.extendedDiff(new).elements // [M(0,4), M(4,0)]
    

    However, for array with only three elements, the result is something that I don't understand:

    let old = [1, 2, 3]
    let new = [3, 2, 1]
    old.diff(new).elements // [D(0), D(1), I(1), I(2)]
    old.extendedDiff(new).elements // [M(0,2), M(1,1)]
    

    Is it a bug or expected behavior?

    Question 
    opened by darrarski 7
  • Can't compile Differ with Carthage to generate a StaticFramework.

    Can't compile Differ with Carthage to generate a StaticFramework.

    Following the steps identified here: https://github.com/Carthage/Carthage/blob/master/Documentation/StaticFrameworks.md

    I can't compile Differ to generate a StaticFramework.

    `*** Building scheme "Differ" in Differ.xcodeproj Build Failed Task failed with exit code 65: /usr/bin/xcrun xcodebuild -project /Users/peter/Carthage/Checkouts/Differ/Differ.xcodeproj -scheme Differ -configuration Release -derivedDataPath /Users/peter/Library/Caches/org.carthage.CarthageKit/DerivedData/9.4_9F1027a/Differ/1.2.3 -sdk watchos ONLY_ACTIVE_ARCH=NO BITCODE_GENERATION_MODE=bitcode CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES archive -archivePath /var/folders/2x/_53hdv7j2ts7yrqbhbjbghgw0000gn/T/Differ SKIP_INSTALL=YES GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=NO CLANG_ENABLE_CODE_COVERAGE=NO STRIP_INSTALLED_PRODUCT=NO (launched in /Users/peter/Carthage/Checkouts/Differ)

    This usually indicates that project itself failed to compile. Please check the xcodebuild log for more details: /var/folders/2x/_53hdv7j2ts7yrqbhbjbghgw0000gn/T/carthage-xcodebuild.wzz23x.log`

    carthage-xcodebuild.wzz23x.log

    Bug 
    opened by pablonosh 6
  • Can't build via Carthage

    Can't build via Carthage

    Xcode Version 9.3 (9E145) Swift 4.1 Command which I use: carthage bootstrap --platform ios --no-use-binaries As far as I know this issue is connected with New Xcode Build system.

    Bug 
    opened by Davtyanag 6
  • Add public inits and allow mutating diff elements

    Add public inits and allow mutating diff elements

    On some scenarios, it's useful to be able to create or mutate a Diff instance (or another variant), for example:

    • exclude some changes from the calculated diff
    • manually build our own diff, for simple scenarios

    Since init(elements:) is auto generated by the compiler, it gets the default internal visibility, meaning it's not accessible outside Differ's module. Adding a public init(elements:) enables this usage.

    Additional ExpressibleByArrayLiteral conformance was added, to make the creation of custom instances even easier.

    The elements property was made var, because it enables mutating the struct when the instance is itself mutable.

    The following changes was applied only to Diff, NestedDiff and NestedExtendedDiff. ExtendedDiff was not modified because it contains additional properties (i.e. source, sourceIndex, reorderedIndex and moveIndices) which are used in ExtendedPatch operations.

    opened by p4checo 6
  • What classes/files to read to learn from the solution and apply it on my own

    What classes/files to read to learn from the solution and apply it on my own

    Hi, I have a use case for favorites table in tab controller, it needs to be updated with whatever products added/removed from other tabs while user experiencing the products there.

    I cannot upgrade Xcode for now, I thought to ask, what classes/files to read as a beginner to learn how to approach a solution for this use case.

    I am asking because I am kinda overwhelmed.

    Sorry if not the appropriate place to open this issue.

    opened by devahmedshendy 5
  • Fix NSTableView batch update 1234 -> 3412

    Fix NSTableView batch update 1234 -> 3412

    I had an issue where NSTableView wouldn't execute the moves correctly for the scenario 01232301

    I realized that the moves produced by extendedDiff were [M(0,2), M(1,3)]

    Applying them in order would mean: 0123 + M(0,2)1203 1203 + M(1,3)1032 This (1032) is also what NSTableView showed after applying the extended diff, but not what we wanted to achieve in the first place

    Applying it in reversed order means 0123 + M(1,3)0231 0231 + M(0,2)2301 which is what we wanted

    So reversing the moves fixes this problem for NSTableView.

    UITableView seems to not have these problems, I think it sorts the moves by itself somehow.

    Unfortunately the related test 0123 -> 3120, resulting in [M(3,0), M(0,3)] (or reversed depending on this PR) is not animated by NSTableView at all. I think this is because it applies them in sequence and the two moves basically cancel each other out. Is there something else we could use for NSTableView which would work sequentially?

    Again, UITableView seems to not have these problems.

    opened by hannesoid 5
  • Stack Overflow with an array of 2000 rows

    Stack Overflow with an array of 2000 rows

    I have an array with an item count of 4 in which we add 2000 items. When I call Differ to generate a patch based on an extendedDiff, it crashes in shiftPatchElement of GenericPatch.swift when generating the patch. This method make recursive calls to itself which is raising a stack overflow after 1753 calls.

    I read the code but I cannot see how to fix it without rewriting a lot of things in this function. Can you help me? Thank you!

    Bug 
    opened by julien1619 5
  • Refactored implementation of the fix from merge #56

    Refactored implementation of the fix from merge #56

    I've been looking into the change from #56 and found a couple of issues with it:

    1. Making apply generic and passing data to it was excessive for a few reasons 1.1. it is not used in the method in any way besides simply passing it back in the appropriate time. 1.2. passing it back in updateData is redundant anyway since you pass updateData within the same call to apply which means you have access to your data source, thus can update it with new data source you used to get diff in the first place.
    let previousDataSource = dataSource
    let diff = newDataSource.diff(previousDataSource)
    collectionView.apply(diff, updateData: { self.dataSource = newDataSource })
    
    1. Marking updateData as optional with default value set to nil kinda makes all these changes meaningless: if one forgets to pass updateData closure it will effectively work as it did before.

    In the PR I've addressed these issues.

    P.S. Also based on this I'd suggest avoiding incompatible API changes outside of major releases. (Thus making this particular change be released as 2.0.0)

    Enhancement 
    opened by adya 4
  • Differ does not perform row reloading if the changes is happening on same row. (macOS)

    Differ does not perform row reloading if the changes is happening on same row. (macOS)

    Hey there,

    I've started implementing Differ in our app to drive changes to our TableView. As our app needs to support 10.15+, we cannot use the native DiffableSource implementation.

    So using extendingDiff and patch, I realized that the current AppKit extension that applies the patch to the NSTableView is not using the reloadData(forRowIndexes: IndexSet, columnIndexes: IndexSet) when changes are applicable to a single row.

    Here's our patch description:

    EmailList DiffPatch [D(4), I(4,<Ocean.EmailListSource: 0x600002d323c0>)]
    

    Why is this important?

    By using Delete and Insert, if the selected row was 4, then it is automatically deselected. This behavior, instead of using reloadDate(forRow...) makes for a pretty bad UX in our case.

    Any suggestions on how to circumvent that? Thanks

    Enhancement 
    opened by martindufort 3
  • Reordering section and rows ends up in a total mess

    Reordering section and rows ends up in a total mess

    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)
        }
    }
    
    opened by Donny1995 2
  • Add tests for out of bounds error

    Add tests for out of bounds error

    Some patches will cause an out of bounds exception (which would crash if directly applied to any tables). This PR adds two test scenarios that show the behavior.

    I am not sure how to fix this, but wanted to share the results of my research.

    I also generated a couple of more test cases which will cause the same error:

    [0, 7, 3, 7, 4] -> [9, 9, 1, 8, 4, 7, 2, 7, 7, 5] 
    [6, 2, 2, 3] -> [10, 4, 8, 3, 6, 2] 
    [7, 0, 8, 1, 3, 9, 7] -> [7, 7, 4, 8, 6, 9, 5, 1, 3] 
    [7, 0, 0, 2] -> [5, 4, 8, 2, 10, 5, 7, 9, 0, 10] 
    [2, 1, 4, 7] -> [10, 7, 4, 3, 2, 0, 5, 1, 5, 8] 
    [8, 10, 6, 7, 6, 2] -> [4, 5, 1, 9, 3, 2, 8, 6, 3, 9] 
    [10, 1, 5, 8, 3, 7] -> [0, 10, 10, 6, 10, 7, 2, 10, 5, 3] 
    [7, 0, 10, 2, 5] -> [9, 9, 7, 9, 5, 0, 10] 
    [9, 10, 1, 10] -> [5, 5, 7, 10, 8, 6, 9, 10] 
    [10, 2, 4, 10, 6] -> [7, 0, 7, 8, 6, 1, 0, 10, 2] 
    [5, 5, 0, 9, 0, 6] -> [1, 4, 10, 1, 2, 6, 0, 3, 0, 9] 
    [7, 7, 6, 7, 1] -> [0, 9, 8, 4, 1, 9, 7, 7, 4, 3] 
    [1, 2, 10, 7] -> [3, 3, 5, 7, 5, 4, 2, 10, 8] 
    [3, 1, 9, 10] -> [5, 0, 2, 10, 0, 1, 9, 9, 0] 
    [2, 3, 2, 1, 4] -> [9, 0, 4, 1, 0, 3, 2] 
    [7, 1, 6, 6] -> [0, 0, 2, 6, 2, 7, 4, 9, 8, 6] 
    [8, 0, 10, 10, 5, 6] -> [3, 8, 1, 8, 4, 6, 10, 2, 5, 4] 
    [0, 10, 3, 4, 6] -> [9, 8, 8, 1, 6, 0, 3, 4, 3] 
    [6, 1, 4, 3] -> [9, 5, 5, 3, 4, 1, 4] 
    [9, 0, 10, 5] -> [1, 2, 7, 5, 6, 5, 10, 6, 9, 10] 
    [5, 7, 6, 7, 4] -> [10, 8, 10, 3, 4, 5, 3, 7, 10, 6] 
    [4, 5, 1, 0] -> [3, 3, 3, 0, 6, 4, 3, 5, 4] 
    [10, 0, 5] -> [7, 7, 5, 10, 7, 0, 6] 
    [10, 5, 7, 7] -> [0, 0, 2, 7, 10, 7, 10, 2, 1] 
    [9, 2, 1] -> [7, 5, 1, 1, 9, 8, 2] 
    [2, 0, 5, 9, 10] -> [4, 4, 10, 9, 4, 0, 7, 5, 3] 
    [6, 0, 1] -> [5, 9, 1, 9, 7, 5, 8, 0, 6, 0] 
    [3, 9, 8] -> [0, 5, 8, 1, 3, 0, 10, 9] 
    [10, 8, 1] -> [5, 0, 1, 6, 4, 10, 9, 8] 
    [3, 10, 4, 7] -> [4, 3, 9, 7, 2, 7, 10, 4] 
    [2, 10, 6, 4, 0, 8] -> [8, 0, 9, 4, 2, 2, 1, 5, 10] 
    [8, 5, 1] -> [7, 9, 1, 2, 2, 8, 5, 8, 0, 7] 
    [1, 9, 0] -> [8, 10, 0, 3, 1, 6, 2, 9] 
    [2, 4, 0, 5] -> [7, 8, 6, 5, 2, 1, 9, 8, 4, 4] 
    [1, 9, 10, 7, 7, 9] -> [6, 8, 0, 7, 9, 9, 10, 2, 0, 7] 
    [5, 9, 2] -> [10, 1, 2, 5, 0, 9, 3] 
    [0, 9, 5] -> [10, 7, 5, 5, 0, 3, 3, 9] 
    [4, 5, 3, 9] -> [0, 4, 0, 9, 7, 5, 4, 3, 8, 7] 
    [2, 8, 6, 6] -> [7, 9, 10, 6, 2, 6, 0, 1, 10] 
    [7, 4, 7, 6, 1] -> [3, 5, 6, 10, 1, 7, 4, 4, 6] 
    [0, 6, 10, 10] -> [8, 7, 7, 10, 5, 0, 3, 10, 4] 
    [10, 5, 7] -> [1, 8, 7, 5, 10, 3, 8, 5, 0, 0] 
    [0, 7, 1, 8] -> [5, 6, 6, 8, 2, 7, 5, 1] 
    [9, 7, 8] -> [1, 6, 8, 6, 2, 9, 9, 7, 5] 
    [0, 8, 3] -> [10, 10, 3, 0, 4, 7, 1, 1, 10, 8] 
    [0, 3, 2] -> [8, 9, 2, 2, 2, 0, 7, 3, 6] 
    [3, 8, 1, 3, 10] -> [6, 4, 2, 4, 10, 7, 3, 3, 3] 
    [2, 9, 6, 8, 10] -> [0, 7, 9, 1, 10, 2, 10, 9, 6] 
    [6, 2, 9, 0, 10, 2, 8] -> [5, 10, 0, 5, 0, 7, 8, 10, 2, 0] 
    [4, 7, 7] -> [5, 3, 7, 6, 4, 7, 1, 6, 8] 
    [2, 10, 2, 9] -> [6, 0, 7, 9, 4, 5, 1, 9, 2, 2] 
    [3, 8, 8, 7] -> [2, 2, 9, 7, 10, 8, 1, 10, 8, 9] 
    [3, 2, 3, 8, 2, 1] -> [0, 9, 4, 3, 5, 1, 3, 3, 2, 5] 
    [2, 0, 5] -> [3, 3, 5, 3, 4, 5, 2, 0, 1] 
    [2, 3, 6] -> [5, 0, 6, 1, 6, 0, 6, 0, 2, 3] 
    
    opened by paxos 2
  • animateRowAndSectionChanges works for particular types of nested collections only

    animateRowAndSectionChanges works for particular types of nested collections only

    I have the following code:

    class Item {
    ...
    }
    class Group {
    ...
    var items = [Item]()
    }
    var groups = [Group]()
    

    groups is a nested collection too but your code doesn't work with it.

    opened by gerchicov-bp 1
  • High Memory Usage extended diffing two arrays of quite differently ordered results

    High Memory Usage extended diffing two arrays of quite differently ordered results

    Hello, I've come across a particular case where I'm seeing a really high memory usage and wanted to make sure I wasn't doing something wrong or extended diffing in some unintended way.

    I was extended diffing two seemingly small arrays of items which happened to be the same (or very similar, give or take a few items) but in two different orders. Though my data was from a different source, I've reproduced the issue with the following example but just creating an array of 5,000 random Ints, uniquing and then shuffling the first array of items to create the second set of items to diff.

    DispatchQueue(label: "test").async {
       let pool = (0...999_999)
       let getRandom: () -> [Int] = {
           (0..<5_000).map { _ in
               pool.randomElement()!
           }
       }
       let previous = Set(getRandom()).shuffled()
       let current = previous.shuffled()
       let before = Date()
       let _ = previous.extendedDiff(current)
       print("finished. took:", Date().timeIntervalSince(before))
    }
    

    Running this code appears to reach a memory usage of around ~1GB, and take ~27 seconds. For reference, 10,000 items seems to hit ~6GB and take ~111 seconds.

    Also for reference, using two random sets of 10,000 items also hits about ~5GB and takes ~113 seconds. eg.:

    let previous = Set(getRandom()).sorted()
    let current = Set(getRandom()).sorted()
    

    The reason this is a problem is that iOS (or at least debugging device in Xcode) seems to have a memory limit of about 2GB before you get terminated.

    In my case, all I really care about is the deleted indexes and not the order, so I've found that in the case where the two arrays are very similar but in different orders, if I sort both arrays of items beforehand so that they're as similar as possible and then run the diff, it takes negligible memory and finishes in no time at all even with 10,000 items. Unfortunately in the case of two random sets of items, sorting won't help and will hit those limits/take a while.

    I just wanted to make sure that I'm not doing something else wrong here, or if there's a different approach I should be taking for extended diffing two sets of items that could for all I know be two sets of different items.

    Edit: Clarify that I was extended diffing (both seem to be problematic, but still)

    opened by tysonkerridge 0
  • Revision of UIKit Overlay's API

    Revision of UIKit Overlay's API

    I would like to revise Differ's public API in terms of its UIKit overlay. The changes I would like to make are the following:

    1. Optional completion handlers for methods on UITableView

    The UICollectionView already has this parameter and it seems reasonable to support this on UITableView as well. The intuitive approach to implement this would be using performBatchUpdates(_:completion:) instead of beginUpdates()/endUpdates(). This method, however, is only available on iOS 11+.

    Alternatively, we could keep the old methods and add extra @available(iOS 11.0, *) methods with completion handlers.

    Another idea would be to hook into CATransaction, wrapping beginUpdates()/endUpdates() in CATransaction.begin()/CATransaction.commit() and registering the completion handler using CATransaction.setCompletionBlock(_:). I highly doubt that this would have the same semantics as using performBatchUpdates(_:completion:)'s completion handler.

    Personally, I would make this a major version bump (see other changes below), drop support for iOS 10 and earlier, and simply use the modern performBatchUpdates(_:completion:) API.

    The update block passed to performBatchUpdates(_:completion:) is also optional, meaning it is implicitly escaping, requiring us to annotate things like indexPathTransform as @escaping as well. I don't think that's a huge deal, though. I'm not sure if the block even escapes; we could try using withoutActuallyEscaping(_:do) if there are strong reasons to not annotate the closures as @escaping, though I'd try to avoid that if possible in case the block actually does escape.

    2. Mandatory updateData parameter for UITableView

    Again, this is a feature we already have for UICollectionView, and we should port it to UITableView as well. Currently, it is very easy to use the Differ/UITableView/UICollectionView APIs incorrectly, as #62 shows. Adding this parameter alone is not going to solve these issues, but it at least tells users of the library at which point they are supposed to update the data source.

    Unfortunately, the intuitive way of using Differ calls things like animateRowChanges(oldData:newData:) when the underlying data has already changed, e.g., from inside a didSet property observer, which is incorrect. This causes crashes when a UITableView/UICollectionView has not queried its data source or the cache has been invalidated prior to scheduling an animation. In these cases, the UITableView/UICollectionView query its data twice, before and after the changes specified by the diff, but ends up querying the new data twice, thus crashing with an NSInternalInconsistencyException. Problems like this may be anchored deeply within an app's architecture and it might not be feasible to call Differ before the data changes and update the data source only once Differ tells you to.

    We could try to prevent the crashes by checking if a diff is compatible with the current state of a UITableView/UICollectionView before applying it, and calling reloadData() instead if it is not. I'm not sure how this interacts with nested calls to performBatchUpdates(_:completion:) and will need to investigate a bit more. If you have any other ideas to address this issue, please let me know.

    Even with this safeguard in place, I believe we should make the parameter mandatory to force users to think about when they update the data source. This way, users would need to opt in to using the APIs incorrectly.

    Personally, I have no experience with making breaking changes in a library. Do you think we should keep the old signatures around and annotate them as @available(*, deprecated, message: "") to make migration to 2.0 easier?

    3. Optional animated: Bool parameter for UITableView and UICollectionView.

    Sometimes, we may want to update the UITableView's data without animation, for example, if the UITableView is currently offscreen or if the user has the "Reduce Motion" accessibility feature enabled. In these cases, updating the relevant parts of the UITableView via performBatchUpdates(_:completion:) is much more efficient than simply calling reloadData(), which causes all cells to be reloaded. reloadData() also has the nasty side effect of cancelling touches if the user is currently interacting with the cells.

    If animated is false, we could simply wrap the call to performBatchUpdates(_:completion:) in UIView.performWithoutAnimation(_:).


    In summary, here are the changes I would like to make to the UIKit overlay. Analogous changes to the animateRowChanges, animateRowAndSectionChanges, animateItemChanges, and animateItemAndSectionChanges families of methods are intentionally left out here. Having the methods begin with "animated" and then having an animated: Bool parameter might be a bit strange — maybe we should rename the methods to "applyRowChanges" or "transitionRows" or something else?

    extension UITableView {
        func apply(
            _ diff: ExtendedDiff,
            deletionAnimation: DiffRowAnimation = .automatic,
            insertionAnimation: DiffRowAnimation = .automatic,
    -       indexPathTransform: (IndexPath) -> IndexPath = { $0 },
    +       indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 },
    +       updateData: () -> Void,
    +       animated: Bool = true,
    +       completion: ((_ finished: Bool) -> Void)? = nil,
        )
    
        func apply(
            _ diff: NestedExtendedDiff,
            rowDeletionAnimation: DiffRowAnimation = .automatic,
            rowInsertionAnimation: DiffRowAnimation = .automatic,
            sectionDeletionAnimation: DiffRowAnimation = .automatic,
            sectionInsertionAnimation: DiffRowAnimation = .automatic,
    -       indexPathTransform: (IndexPath) -> IndexPath,
    +       indexPathTransform: @escaping (IndexPath) -> IndexPath,
    -       sectionTransform: (Int) -> Int,
    +       sectionTransform: @escaping (Int) -> Int,
    +       updateData: () -> Void,
    +       animated: Bool = true,
    +       completion: ((_ finished: Bool) -> Void)? = nil,
        )
    }
    
    extension UICollectionView {
        func apply(
            _ diff: ExtendedDiff,
            updateData: () -> Void,
    +       animated: Bool = true,
            completion: ((Bool) -> Void)? = nil,
            indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 },
        )
    
        func apply(
            _ diff: NestedExtendedDiff,
            indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 },
            sectionTransform: @escaping (Int) -> Int = { $0 },
            updateData: () -> Void,
    +       animated: Bool = true,
            completion: ((Bool) -> Void)? = nil,
        )
    }
    
    Enhancement Work In Progress Task Help Wanted 
    opened by jenox 2
Releases(1.4.6)
  • 1.4.6(Feb 11, 2022)

    What's Changed

    • Fix NSTableView animations by @hannesoid in https://github.com/tonyarnold/Differ/pull/70
    • Enable building for Apple Silicon Macs by @deborahgoldsmith in https://github.com/tonyarnold/Differ/pull/73
    • Xcode 12 beta support by @tonyarnold in https://github.com/tonyarnold/Differ/pull/75
    • Update project to Swift 5.4 by @tonyarnold in https://github.com/tonyarnold/Differ/pull/79
    • Differ is now packaged as a pre-built XCFramework alongside each release.

    New Contributors

    • @hannesoid made their first contribution in https://github.com/tonyarnold/Differ/pull/70
    • @deborahgoldsmith made their first contribution in https://github.com/tonyarnold/Differ/pull/73

    Full Changelog: https://github.com/tonyarnold/Differ/compare/1.4.5...1.4.6

    Source code(tar.gz)
    Source code(zip)
    Differ.xcframework.zip(14.42 MB)
  • 1.4.5(Mar 29, 2020)

  • 1.4.4(Jan 5, 2020)

  • 1.4.3(May 25, 2019)

  • 1.4.2(Apr 27, 2019)

  • 1.4.1(Apr 19, 2019)

  • 1.4.0(Mar 30, 2019)

    This release includes Swift 5.0 and Xcode 10.2 compatibility.

    Changes

    • [bc40b8e] Merge pull request #50 from tonyarnold/swift-5.0
    • [890d832] Gather coverage data only when it's generated
    • [629577c] Fixes to build configuration
    • [4c915b0] Try out a new TravisCI test matrix
    • [3a40e3b] Bump tooling versions to Swift 5.0
    • [e345775] Perform Xcode’s recommended configuration updates
    • [83f3d14] Update ORGANIZATIONNAME to “Differ Project”
    • [82c829f] Enable Base localization
    • [2250f5a] Bump Xcode compatibility version to 10.0
    • [189ae5c] Remove redundant access modifiers from extension methods
    • [91c45b3] Bump SWIFT_VERSION to 5.0
    • [5e80d18] Initial fixes for Swift 5.0
    • [83c0781] Fix URL pointing to TravisCI
    • [9b62b30] Update README to reference new TravisCI server address
    • [0769c44] Merge pull request #49 from dineshba/refactoringBatchUpdate
    • [b66656c] Refactored to loop the diff once instead of three times
    • [5f31966] Moved the common logic into BatchUpdate
    • [1cdb445] Merge pull request #47 from tonyarnold/fix/remove-patch-from-parameter
    • [288cda5] Merge pull request #48 from dineshba/master
    • [74e95b3] Refactored to use map instead of for-loop
    • [bfda6ae] Add test for diffTraces in Diff
    • [1e38ecc] Remove unnecessary “from” parameter from Diff.patch(…)
    • [0d07963] Force TravisCI to install a prerelease of CocoaPods
    • [6bdcd9c] Update README.md to reflect the current build requirements
    Source code(tar.gz)
    Source code(zip)
    Differ.framework.zip(5.55 MB)
  • 1.3.0(Sep 26, 2018)

    This release includes Swift 4.2 compatibility.

    If you have not transitioned to Xcode 10/Swift 4.2, please remain on v1.2.3.

    Thanks to @maurovc and @rayfix for these changes!

    Source code(tar.gz)
    Source code(zip)
  • 1.2.3(May 6, 2018)

  • 1.2.2(Apr 21, 2018)

  • 1.2.1(Apr 20, 2018)

    • Remove Equatable restriction from Patch (#25).
    • Use animator() proxy for NSCollectionView animations (#27)
    • Fixes Swift Package Manager building - it's no longer necessary to provide the minimum macOS SDK target, and it should now work on Linux (#28)
    • Graph.playground has been added to the included sample project.

    Thanks to @garnett, @rudedogg, @chrisco314 and @kirbyfan64 for these contributions!

    Source code(tar.gz)
    Source code(zip)
  • 1.2.0(Apr 5, 2018)

    This release introduces support for applying diffs to NSTableView and NSCollectionView under AppKit (#24).

    Thanks to @interstateone for this contribution!

    Source code(tar.gz)
    Source code(zip)
  • 1.1.1(Apr 4, 2018)

  • 1.1.0(Apr 3, 2018)

    • Updated for Swift 4.1. You must be running Xcode 9.3 or later as of this version.
    • Fixes that optimize the algorithm for space: https://github.com/tonyarnold/Differ/pull/16
    • Diffs can now be mutated and initialized: https://github.com/tonyarnold/Differ/pull/21

    Thanks to @Damonvvong and @p4checo for these contributions!

    Source code(tar.gz)
    Source code(zip)
  • 1.0.3(Apr 3, 2018)

    • Code coverage is disabled in the workspace
    • UIKit-dependent code is no longer included when building for Linux

    Thanks to @mihai8804858 and @eXhausted for these contributions!

    Source code(tar.gz)
    Source code(zip)
  • 1.0.2(Sep 28, 2017)

  • 1.0.1(Sep 19, 2017)

  • 1.0.0(Sep 18, 2017)

Owner
Tony Arnold
Lead Developer at @ittybittyapps. Cocoa-wielding adventure seeker. Slayer of bugs and misconfiguration.
Tony Arnold
A Swift probability and statistics library

Probably Probably is a set of Swift structures for computing the probability and cumulative distributions of different probablistic functions. Right n

Harlan Haskins 270 Dec 2, 2022
KeyPathKit is a library that provides the standard functions to manipulate data along with a call-syntax that relies on typed keypaths to make the call sites as short and clean as possible.

KeyPathKit Context Swift 4 has introduced a new type called KeyPath, with allows to access the properties of an object with a very nice syntax. For in

Vincent Pradeilles 406 Dec 25, 2022
Simple diff library in pure Swift

Diff Simple diffing library in pure Swift. Installing You can use Carthage or Swift Package Manager to install Diff. Usage Start by importing the pack

Sam Soffes 120 Sep 9, 2022
A fast Swift diffing library.

HeckelDiff Pure Swift implementation of Paul Heckel's A Technique for Isolating Differences Between Files Features This is a simple diff algorithm tha

Matias Cudich 166 Oct 6, 2022
Algorithm is a library of tools that is used to create intelligent applications.

Welcome to Algorithm Algorithm is a library of tools that is used to create intelligent applications. Features Probability Tools Expected Value Progra

Cosmicmind 820 Dec 9, 2022
Swift-extensions - Swift package extending the Swift programming language.

swift-extensions A package containing extensions for the Swift programming language. Contribution Reporting a bug If you find a bug, please open a bug

Alexandre H. Saad 2 Jun 12, 2022
Examples of commonly used data structures and algorithms in Swift.

Swift Structures This project provides a framework for commonly used data structures and algorithms written in a new iOS development language called S

Wayne Bishop 2.1k Dec 28, 2022
Swift μ-framework for efficient array diffs and datasource adapters.

Buffer Swift μ-framework for efficient array diffs, collection observation and data source implementation. C++11 port here Installation cd {PROJECT_RO

Alex Usbergo 348 Aug 2, 2022
Algorithms and data structures in Swift, with explanations!

Welcome to the Swift Algorithm Club! Here you'll find implementations of popular algorithms and data structures in everyone's favorite new language Sw

raywenderlich 27.3k Jan 8, 2023
💻 A fast and flexible O(n) difference algorithm framework for Swift collection.

A fast and flexible O(n) difference algorithm framework for Swift collection. The algorithm is optimized based on the Paul Heckel's algorithm. Made wi

Ryo Aoyama 3.3k Jan 4, 2023
:droplet: A generic view model for both basic and complex scenarios

Brick Description Brick is a generic view model for both basic and complex scenarios. Mapping a basic table view cells is as easy as pie, if you have

HyperRedink 59 Jul 31, 2021
Differific - a fast and convenient diffing framework.

Differific Description Differific is a diffing tool that helps you compare Hashable objects using the Paul Heckel's diffing algorithm. Creating a chan

Christoffer Winterkvist 127 Jun 3, 2022
Extension of Diffable API which allow not duplicate code and use less models. Included example for SideBar.

SPDiffable Apple's diffable API requerid models for each object type. If you want use it in many place, you pass many time to implemenet and get over

Ivan Vorobei 114 Jan 3, 2023
Swivl - A set of BLAS-accelerated linerar algebra structures and functions

Swivl - Swift Vector Library A set of BLAS-accelerated linerar algebra structure

null 0 Jan 19, 2022
Commonly used data structures for Swift

Swift Collections is an open-source package of data structure implementations for the Swift programming language.

Apple 2.7k Jan 5, 2023
A functional tool-belt for Swift Language similar to Lo-Dash or Underscore.js in Javascript

Dollar Dollar is a Swift library that provides useful functional programming helper methods without extending any built in objects. It is similar to L

Ankur Patel 4.2k Jan 4, 2023
Swift type modelling the success/failure of arbitrary operations.

Result This is a Swift µframework providing Result<Value, Error>. Result<Value, Error> values are either successful (wrapping Value) or failed (wrappi

Antitypical 2.5k Dec 26, 2022
A Graph Data Structure in Pure Swift

SwiftGraph SwiftGraph is a pure Swift (no Cocoa) implementation of a graph data structure, appropriate for use on all platforms Swift supports (iOS, m

David Kopec 700 Dec 16, 2022
A Generic Priority Queue in Pure Swift

SwiftPriorityQueue SwiftPriorityQueue is a pure Swift (no Cocoa) implementation of a generic priority queue data structure, appropriate for use on all

David Kopec 350 Dec 22, 2022