diff --git a/FastDiff.xcodeproj/project.pbxproj b/FastDiff.xcodeproj/project.pbxproj index 99334c7..0c2661a 100644 --- a/FastDiff.xcodeproj/project.pbxproj +++ b/FastDiff.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 284D483D23C697EA00DD2963 /* Diff+UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284D483B23C6946800DD2963 /* Diff+UIKit.swift */; }; + 284D483E23C697F100DD2963 /* Diff+UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284D483B23C6946800DD2963 /* Diff+UIKit.swift */; }; 2889D0CA22D4D665000E7797 /* FastDiff.h in Headers */ = {isa = PBXBuildFile; fileRef = 2889D0C822D4D665000E7797 /* FastDiff.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2889D0CE22D4D675000E7797 /* Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Diffable.swift */; }; 2889D0CF22D4D675000E7797 /* DiffingAlgorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* DiffingAlgorithm.swift */; }; @@ -65,6 +67,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 284D483B23C6946800DD2963 /* Diff+UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Diff+UIKit.swift"; sourceTree = ""; }; 2889D0C622D4D665000E7797 /* FastDiff.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FastDiff.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2889D0C822D4D665000E7797 /* FastDiff.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FastDiff.h; sourceTree = ""; }; 2889D0C922D4D665000E7797 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -186,7 +189,7 @@ name = Products; sourceTree = BUILT_PRODUCTS_DIR; }; - OBJ_5 /* */ = { + OBJ_5 = { isa = PBXGroup; children = ( OBJ_6 /* Package.swift */, @@ -197,7 +200,6 @@ 2889D0C722D4D665000E7797 /* FastDiff */, OBJ_25 /* Products */, ); - name = ""; sourceTree = ""; }; OBJ_7 /* Sources */ = { @@ -214,6 +216,7 @@ OBJ_9 /* Diffable.swift */, OBJ_10 /* DiffingAlgorithm.swift */, OBJ_11 /* InternalDiff.swift */, + 284D483B23C6946800DD2963 /* Diff+UIKit.swift */, ); name = FastDiffLib; path = Sources/FastDiffLib; @@ -348,9 +351,10 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); - mainGroup = OBJ_5 /* */; + mainGroup = OBJ_5; productRefGroup = OBJ_25 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -382,6 +386,7 @@ buildActionMask = 2147483647; files = ( 2889D0D022D4D675000E7797 /* InternalDiff.swift in Sources */, + 284D483E23C697F100DD2963 /* Diff+UIKit.swift in Sources */, 2889D0CE22D4D675000E7797 /* Diffable.swift in Sources */, 2889D0CF22D4D675000E7797 /* DiffingAlgorithm.swift in Sources */, ); @@ -410,6 +415,7 @@ buildActionMask = 0; files = ( OBJ_49 /* Diffable.swift in Sources */, + 284D483D23C697EA00DD2963 /* Diff+UIKit.swift in Sources */, OBJ_50 /* DiffingAlgorithm.swift in Sources */, OBJ_51 /* InternalDiff.swift in Sources */, ); diff --git a/Sources/FastDiffLib/Diff+UIKit.swift b/Sources/FastDiffLib/Diff+UIKit.swift new file mode 100644 index 0000000..d5474e0 --- /dev/null +++ b/Sources/FastDiffLib/Diff+UIKit.swift @@ -0,0 +1,115 @@ +// +// Diff+UIKit.swift +// AlgoChecker +// +// Created by Vijaya Prakash Kandel on 08.01.20. +// + +import Foundation + +/** + These below functions support the use of Diff naturally to UITableView or UICollectionView. + */ + +internal func packingConsequetiveDeleteAddWithUpdate(from diffResult: [DiffOperation.Simple]) -> [DiffOperation.Simple] { + if diffResult.isEmpty { return [] } + + var currentSeekIndex = 0 // This is the index that is not processed. + + var accumulator: [DiffOperation.Simple] = [] + while currentSeekIndex < diffResult.count { + let thisItem = diffResult[currentSeekIndex] + let nextIndex = currentSeekIndex.advanced(by: 1) + + if nextIndex < diffResult.count { + let nextItem = diffResult[nextIndex] + switch (thisItem, nextItem) { + case let (.delete(di, dIndex), .add(ai, aIndex)) where dIndex == aIndex: + let update = DiffOperation.Simple.update(di, ai, dIndex) + accumulator.append(update) + default: + accumulator.append(thisItem) + accumulator.append(nextItem) + } + currentSeekIndex = nextIndex.advanced(by: 1) + } else { + // This is the last item + accumulator.append(thisItem) + // This breaks the iteration + currentSeekIndex = nextIndex + } + } + return accumulator +} + + +/** + Entire Tree/Graph diffing is possible. + However not something the library encourages due to added complexity O(n^2). + If you so choose to diff then please use this function. + */ +public func diffAllLevel(_ oldContent: [T], _ newContent: [T]) -> [DiffOperation] where T: Diffable, T.InternalItemType == T { + if oldContent.isEmpty && newContent.isEmpty { return [] } + var accumulator: [DiffOperation] = [] + let thisLevelDiff = diff(oldContent, newContent) + for index in thisLevelDiff { + if case let .update(o, n, _) = index { + accumulator = accumulator + diffAllLevel(o.children, n.children) + } else { + accumulator.append(index) + } + } + return accumulator +} + + +/** + Orders diff operation in way UIKit can process as is. Only orderedDiffOperations can be applied back to old items for merge. + + This is a helper function which assumes that + 1. Deletion happens first from end index on original [T] + 2. Insertions follows + 3. Update Follows + + - Note: This is the case with UIKit (UITableView and UICollectionView dataSources) + + - Limitation: Can't extend a protocol with a generic typed enum (generic type in general) + extension Array where Element: Operation { } + */ +public func orderedOperation(from operations: [DiffOperation]) -> [DiffOperation.Simple] { + /// Deletions need to happen from higher index to lower (to avoid corrupted indexes) + /// [x, y, z] will be corrupt if we attempt [d(0), d(2), d(1)] + /// d(0) succeeds then array is [x,y]. Attempting to delete at index 2 produces out of bounds error. + /// Therefore we sort in descending order of index + var deletions = [Int: DiffOperation.Simple]() + var insertions = [DiffOperation.Simple]() + var updates = [DiffOperation.Simple]() + + for oper in operations { + switch oper { + case let .update(item, newItem, index): + updates.append(.update(item, newItem, index)) + case let .add(item, atIndex): + insertions.append(.add(item, atIndex)) + case let .delete(item, from): + deletions[from] = .delete(item, from) + case let .move(item, from, to): + insertions.append(.add(item, to)) + deletions[from] = .delete(item, from) + } + } + let descendingOrderedIndexDeletions = deletions.sorted(by: {$0.0 > $1.0 }).map{ $0.1 } + return descendingOrderedIndexDeletions + insertions + updates +} + + +/** + Optimizes the diff for easy usage for UIKit List (UITableView / UICollectionView) Datasources integration. + + This takes care of: + - emitting ordered operations (needed to merge. This is what iOS datasources expect.) + - emitting optimized operation: consequetive add and delete on same index is replaced by update. + */ +public func diffOptimizingForUIKitUsage(_ old: [T], new: [T]) -> [DiffOperation.Simple] where T: Diffable { + return packingConsequetiveDeleteAddWithUpdate(from: orderedOperation(from: diff(old, new))) +} diff --git a/Sources/FastDiffLib/DiffingAlgorithm.swift b/Sources/FastDiffLib/DiffingAlgorithm.swift index 20830fc..80271cf 100644 --- a/Sources/FastDiffLib/DiffingAlgorithm.swift +++ b/Sources/FastDiffLib/DiffingAlgorithm.swift @@ -253,35 +253,6 @@ public func diff(_ oldContent: [T], _ newContent: [T]) -> [DiffOperation] return operations } -/** Limitation: Can't extend a protocol with a generic typed enum (generic type in general) - extension Array where Element: Operation { } - */ -public func orderedOperation(from operations: [DiffOperation]) -> [DiffOperation.Simple] { - /// Deletions need to happen from higher index to lower (to avoid corrupted indexes) - /// [x, y, z] will be corrupt if we attempt [d(0), d(2), d(1)] - /// d(0) succeeds then array is [x,y]. Attempting to delete at index 2 produces out of bounds error. - /// Therefore we sort in descending order of index - var deletions = [Int: DiffOperation.Simple]() - var insertions = [DiffOperation.Simple]() - var updates = [DiffOperation.Simple]() - - for oper in operations { - switch oper { - case let .update(item, newItem, index): - updates.append(.update(item, newItem, index)) - case let .add(item, atIndex): - insertions.append(.add(item, atIndex)) - case let .delete(item, from): - deletions[from] = .delete(item, from) - case let .move(item, from, to): - insertions.append(.add(item, to)) - deletions[from] = .delete(item, from) - } - } - let descendingOrderedIndexDeletions = deletions.sorted(by: {$0.0 > $1.0 }).map{ $0.1 } - return descendingOrderedIndexDeletions + insertions + updates -} - extension Array where Element: Hashable {