├── Supporting Files ├── Configurations │ ├── Deployment-Targets.xcconfig │ ├── Universal-Target-Base.xcconfig │ └── Universal-Framework-Target.xcconfig ├── Differ.h ├── FrameworkTests-Info.plist └── Framework-Info.plist ├── Differ.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ ├── xcbaselines │ └── C9838FF01D29571000691BE8.xcbaseline │ │ ├── 5ACB3D1B-D608-4861-BD7C-4E5E202E879C.plist │ │ └── Info.plist │ └── xcschemes │ └── Differ.xcscheme ├── Examples └── TableViewExample │ ├── Graph.playground │ ├── contents.xcplayground │ ├── Contents.swift │ └── Sources │ │ ├── CharacterLabels.swift │ │ ├── Arrows.swift │ │ ├── GraphView.swift │ │ └── ViewController.swift │ ├── TableViewExample.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── TableViewExample.xcscheme │ └── project.pbxproj │ └── TableViewExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Info.plist │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── TableViewController.swift │ └── NestedTableViewController.swift ├── .codecov.yml ├── Package.swift ├── .github ├── workflows │ ├── deploy-release-artifacts.yml │ └── continuous-integration.yml └── CODE_OF_CONDUCT.md ├── Sources └── Differ │ ├── Patch+Apply.swift │ ├── BatchUpdate.swift │ ├── ExtendedPatch+Apply.swift │ ├── NestedBatchUpdate.swift │ ├── Patch.swift │ ├── Patch+Sort.swift │ ├── LinkedList.swift │ ├── GenericPatch.swift │ ├── NestedDiff.swift │ ├── ExtendedPatch.swift │ ├── NestedExtendedDiff.swift │ ├── ExtendedDiff.swift │ ├── Diff.swift │ └── Diff+AppKit.swift ├── LICENSE.md ├── .gitignore ├── Differ.podspec ├── Tests └── DifferTests │ ├── PatchApplyTests.swift │ ├── BatchUpdateTests.swift │ ├── ExtendedPatchSortTests.swift │ ├── DiffTests.swift │ ├── NestedExtendedDiffTests.swift │ ├── NestedDiffTests.swift │ └── PatchSortTests.swift ├── Scripts └── build-xcframework.sh └── README.md /Supporting Files/Configurations/Deployment-Targets.xcconfig: -------------------------------------------------------------------------------- 1 | IPHONEOS_DEPLOYMENT_TARGET = 9.0 2 | MACOSX_DEPLOYMENT_TARGET = 10.12 3 | TVOS_DEPLOYMENT_TARGET = 9.0 4 | WATCHOS_DEPLOYMENT_TARGET = 4.0 5 | -------------------------------------------------------------------------------- /Differ.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/TableViewExample/Graph.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Supporting Files/Differ.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | //! Project version number for Differ. 4 | FOUNDATION_EXPORT double DifferVersionNumber; 5 | 6 | //! Project version string for Differ. 7 | FOUNDATION_EXPORT const unsigned char DifferVersionString[]; 8 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Differ.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Differ.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Latest 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 9 | return true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/TableViewExample/Graph.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import UIKit 4 | import PlaygroundSupport 5 | import Differ 6 | 7 | let viewController = GraphViewController(string1: "kitten", string2: "sitting") 8 | viewController.view.frame = CGRect(x: 0, y: 0, width: 500, height: 500) 9 | PlaygroundPage.current.liveView = viewController.view 10 | 11 | print("Done") 12 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: true 4 | comment: 5 | behavior: default 6 | layout: header, diff 7 | require_changes: false 8 | coverage: 9 | precision: 2 10 | range: 11 | - 70.0 12 | - 100.0 13 | round: down 14 | status: 15 | changes: false 16 | patch: false 17 | project: false 18 | parsers: 19 | gcov: 20 | branch_detection: 21 | conditional: true 22 | loop: true 23 | macro: false 24 | method: false 25 | javascript: 26 | enable_partials: false 27 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.4 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Differ", 6 | platforms: [ 7 | .iOS(.v9), 8 | .macOS(.v10_12), 9 | .tvOS(.v9), 10 | .watchOS(.v4) 11 | ], 12 | products: [ 13 | .library(name: "Differ", targets: ["Differ"]), 14 | ], 15 | targets: [ 16 | .target(name: "Differ"), 17 | .testTarget(name: "DifferTests", dependencies: [ 18 | .target(name: "Differ") 19 | ]), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /Differ.xcodeproj/xcshareddata/xcbaselines/C9838FF01D29571000691BE8.xcbaseline/5ACB3D1B-D608-4861-BD7C-4E5E202E879C.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | BatchUpdateTests 8 | 9 | testCellsPerformance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.14952 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Supporting Files/Configurations/Universal-Target-Base.xcconfig: -------------------------------------------------------------------------------- 1 | ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES 2 | SUPPORTED_PLATFORMS = macosx iphonesimulator iphoneos watchos watchsimulator appletvos appletvsimulator 3 | SUPPORTS_MACCATALYST = YES 4 | 5 | // Dynamic linking uses different default copy paths 6 | LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks' 7 | LD_RUNPATH_SEARCH_PATHS[sdk=iphone*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 8 | LD_RUNPATH_SEARCH_PATHS[sdk=watch*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 9 | LD_RUNPATH_SEARCH_PATHS[sdk=appletv*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/deploy-release-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | # Create a new release whenever a version is tagged 4 | on: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | create_release: 10 | name: Create Release 11 | runs-on: macos-latest 12 | steps: 13 | - name: Checkout Sources 14 | uses: actions/checkout@v2 15 | 16 | - name: Create XCFramework 17 | run: ./Scripts/build-xcframework.sh 18 | env: 19 | PROJECT_FILE: Differ.xcodeproj 20 | SCHEME_NAME: Differ 21 | OUTPUT_DIR: ${{ runner.temp }} 22 | 23 | - name: Release XCFramework 24 | uses: softprops/action-gh-release@v1 25 | with: 26 | fail_on_unmatched_files: true 27 | files: ${{ runner.temp }}/Differ.xcframework.zip 28 | -------------------------------------------------------------------------------- /Supporting Files/Configurations/Universal-Framework-Target.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Universal-Target-Base.xcconfig" 2 | 3 | // macOS-specific default settings 4 | FRAMEWORK_VERSION[sdk=macosx*] = A 5 | COMBINE_HIDPI_IMAGES[sdk=macosx*] = YES 6 | 7 | // iOS-specific default settings 8 | TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2 9 | 10 | // TV-specific default settings 11 | TARGETED_DEVICE_FAMILY[sdk=appletv*] = 3 12 | 13 | // Watch-specific default settings 14 | TARGETED_DEVICE_FAMILY[sdk=watch*] = 4 15 | 16 | ENABLE_BITCODE[sdk=macosx*] = NO 17 | ENABLE_BITCODE[sdk=watch*] = YES 18 | ENABLE_BITCODE[sdk=iphonesimulator*] = NO 19 | ENABLE_BITCODE[sdk=iphone*] = YES 20 | ENABLE_BITCODE[sdk=appletv*] = YES 21 | -------------------------------------------------------------------------------- /Supporting Files/FrameworkTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Supporting Files/Framework-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2017 Tony Arnold. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /Sources/Differ/Patch+Apply.swift: -------------------------------------------------------------------------------- 1 | public extension RangeReplaceableCollection { 2 | /// Applies the changes described by the provided patches to a copy of this collection and returns the result. 3 | /// 4 | /// - Parameter patches: a collection of ``Patch`` instances to apply. 5 | /// - Returns: A copy of the current collection with the provided patches applied to it. 6 | func apply(_ patches: C) -> Self where C.Element == Patch { 7 | var mutableSelf = self 8 | 9 | for patch in patches { 10 | switch patch { 11 | case let .insertion(i, element): 12 | let target = mutableSelf.index(mutableSelf.startIndex, offsetBy: i) 13 | mutableSelf.insert(element, at: target) 14 | case let .deletion(i): 15 | let target = mutableSelf.index(mutableSelf.startIndex, offsetBy: i) 16 | mutableSelf.remove(at: target) 17 | } 18 | } 19 | 20 | return mutableSelf 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Tony Arnold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Sources/Differ/BatchUpdate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct BatchUpdate { 4 | public struct MoveStep: Equatable { 5 | public let from: IndexPath 6 | public let to: IndexPath 7 | } 8 | 9 | public let deletions: [IndexPath] 10 | public let insertions: [IndexPath] 11 | public let moves: [MoveStep] 12 | 13 | public init( 14 | diff: ExtendedDiff, 15 | indexPathTransform: (IndexPath) -> IndexPath = { $0 } 16 | ) { 17 | (deletions, insertions, moves) = diff.reduce(([IndexPath](), [IndexPath](), [MoveStep]()), { (acc, element) in 18 | var (deletions, insertions, moves) = acc 19 | switch element { 20 | case let .delete(at): 21 | deletions.append(indexPathTransform([0, at])) 22 | case let .insert(at): 23 | insertions.append(indexPathTransform([0, at])) 24 | case let .move(from, to): 25 | moves.append(MoveStep(from: indexPathTransform([0, from]), to: indexPathTransform([0, to]))) 26 | } 27 | return (deletions, insertions, moves) 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | 3 | ## Build generated 4 | build/ 5 | DerivedData/ 6 | 7 | ## Various settings 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata/ 17 | 18 | ## Other 19 | *.moved-aside 20 | *.xccheckout 21 | *.xcscmblueprint 22 | 23 | ## Obj-C/Swift specific 24 | *.hmap 25 | *.ipa 26 | *.dSYM.zip 27 | *.dSYM 28 | 29 | ## Playgrounds 30 | timeline.xctimeline 31 | playground.xcworkspace 32 | 33 | ## Carthage 34 | **/Carthage/Checkouts 35 | **/Carthage/Build 36 | 37 | ## Swift Package Manager 38 | .build/ 39 | .swiftpm/ 40 | Package.resolved 41 | 42 | # macOS 43 | ## General 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | 48 | ## Icon must end with two \r 49 | Icon 50 | 51 | ## Thumbnails 52 | ._* 53 | 54 | ## Files that might appear in the root of a volume 55 | .DocumentRevisions-V100 56 | .fseventsd 57 | .Spotlight-V100 58 | .TemporaryItems 59 | .Trashes 60 | .VolumeIcon.icns 61 | .com.apple.timemachine.donotpresent 62 | 63 | ## Directories potentially created on remote AFP share 64 | .AppleDB 65 | .AppleDesktop 66 | Network Trash Folder 67 | Temporary Items 68 | .apdisk 69 | -------------------------------------------------------------------------------- /Differ.xcodeproj/xcshareddata/xcbaselines/C9838FF01D29571000691BE8.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 5ACB3D1B-D608-4861-BD7C-4E5E202E879C 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i5 17 | cpuSpeedInMHz 18 | 2900 19 | logicalCPUCoresPerPackage 20 | 4 21 | modelCode 22 | MacBookPro12,1 23 | physicalCPUCoresPerPackage 24 | 2 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | i386 30 | targetDevice 31 | 32 | modelCode 33 | iPhone5,1 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Differ.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Differ" 3 | s.version = "1.4.6" 4 | s.summary = "A very fast difference calculation library written in Swift." 5 | s.homepage = "https://github.com/tonyarnold/Diff" 6 | s.description = <<-DESC 7 | Differ generates the differences between `Collection` instances (this includes Strings!). 8 | 9 | It uses a fast algorithm `(O((N+M)*D))` to do this. 10 | 11 | Also included are utilities for easily applying diffs and patches to `UICollectionView`/`UITableView`. 12 | DESC 13 | 14 | s.license = { :type => "MIT", :file => "LICENSE.md" } 15 | s.authors = { 16 | "Tony Arnold" => "tony@thecocoabots.com" 17 | } 18 | 19 | s.source = { :git => "https://github.com/tonyarnold/Differ.git", :tag => "1.4.6" } 20 | s.source_files = "Sources/Differ" 21 | 22 | s.platforms = { :ios => "9.0", :osx => "10.12", :tvos => "9.0", :watchos => "4.0" } 23 | s.swift_versions = ['5.4'] 24 | 25 | s.ios.exclude_files = [ 26 | "Sources/Differ/Diff+AppKit.swift" 27 | ] 28 | s.osx.exclude_files = [ 29 | "Sources/Differ/Diff+UIKit.swift" 30 | ] 31 | s.tvos.exclude_files = [ 32 | "Sources/Differ/Diff+AppKit.swift" 33 | ] 34 | s.watchos.exclude_files = [ 35 | "Sources/Differ/Diff+UIKit.swift", 36 | "Sources/Differ/Diff+AppKit.swift", 37 | "Sources/Differ/NestedBatchUpdate.swift" 38 | ] 39 | end 40 | -------------------------------------------------------------------------------- /Examples/TableViewExample/Graph.playground/Sources/CharacterLabels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterLabels.swift 3 | // Graph 4 | // 5 | // Created by Wojciech Czekalski on 25.03.2016. 6 | // Copyright © 2016 wczekalski. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension String { 12 | func length() -> Int { 13 | return lengthOfBytes(using: String.Encoding.utf8) 14 | } 15 | 16 | func characterLabels(withFrames frames: [CGRect]) -> [UILabel] { 17 | guard count == frames.count else { 18 | return [] 19 | } 20 | 21 | let labels = map { $0.label() } 22 | let sizes = labels.map { $0.frame.size } 23 | let frames = inset(rects: frames, to: sizes) 24 | zip(labels, frames).forEach { label, frame in label.frame = frame } 25 | return labels 26 | } 27 | } 28 | 29 | extension Character { 30 | func label() -> UILabel { 31 | let l = UILabel() 32 | l.textColor = .white 33 | l.text = String(self) 34 | l.sizeToFit() 35 | return l 36 | } 37 | } 38 | 39 | func inset(rects: [CGRect], to: [CGSize]) -> [CGRect] { 40 | return zip(to, rects).map { size, rect -> CGRect in 41 | rect.inset(to: size) 42 | } 43 | } 44 | 45 | extension CGRect { 46 | func inset(to size: CGSize) -> CGRect { 47 | return insetBy(dx: (width - size.width) / 2, dy: (height - size.height) / 2).standardized 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Differ/ExtendedPatch+Apply.swift: -------------------------------------------------------------------------------- 1 | public extension RangeReplaceableCollection { 2 | /// Applies the changes described by the provided patches to a copy of this collection and returns the result. 3 | /// 4 | /// - Parameter patches: a collection of ``ExtendedPatch`` instances to apply. 5 | /// - Returns: A copy of the current collection with the provided patches applied to it. 6 | func apply(_ patches: C) -> Self where C.Element == ExtendedPatch { 7 | var mutableSelf = self 8 | 9 | for patch in patches { 10 | switch patch { 11 | case let .insertion(i, element): 12 | let target = mutableSelf.index(mutableSelf.startIndex, offsetBy: i) 13 | mutableSelf.insert(element, at: target) 14 | case let .deletion(i): 15 | let target = mutableSelf.index(mutableSelf.startIndex, offsetBy: i) 16 | mutableSelf.remove(at: target) 17 | case let .move(from, to): 18 | let fromIndex = mutableSelf.index(mutableSelf.startIndex, offsetBy: from) 19 | let toIndex = mutableSelf.index(mutableSelf.startIndex, offsetBy: to) 20 | let element = mutableSelf.remove(at: fromIndex) 21 | mutableSelf.insert(element, at: toIndex) 22 | } 23 | } 24 | 25 | return mutableSelf 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIStatusBarTintParameters 32 | 33 | UINavigationBar 34 | 35 | Style 36 | UIBarStyleDefault 37 | Translucent 38 | 39 | 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | env: 4 | DEVELOPER_DIR: "/Applications/Xcode_13.2.1.app/Contents/Developer" 5 | 6 | on: 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | push: 10 | branches: [main] 11 | 12 | jobs: 13 | xcode-build: 14 | name: Xcode Build 15 | runs-on: macos-11 16 | strategy: 17 | matrix: 18 | 19 | destination: [ 20 | "platform=macOS", 21 | "platform=iOS Simulator,name=iPhone 11", 22 | "platform=tvOS Simulator,name=Apple TV", 23 | "platform=watchOS Simulator,name=Apple Watch Series 5 - 44mm" 24 | ] 25 | 26 | steps: 27 | - uses: actions/checkout@v1 28 | - name: xcrun xcodebuild build 29 | run: xcrun xcodebuild build -destination "${{ matrix.destination }}" -project "Differ.xcodeproj" -scheme "Differ" 30 | 31 | xcode-test: 32 | name: Xcode Test 33 | runs-on: macos-11 34 | strategy: 35 | matrix: 36 | destination: [ 37 | "platform=macOS", 38 | "platform=iOS Simulator,name=iPhone 11", 39 | "platform=tvOS Simulator,name=Apple TV" 40 | ] 41 | 42 | steps: 43 | - uses: actions/checkout@v1 44 | - name: xcrun xcodebuild test 45 | run: | 46 | xcrun xcodebuild test -destination "${{ matrix.destination }}" -project "Differ.xcodeproj" -scheme "Differ" 47 | bash <(curl -s https://codecov.io/bash) 48 | 49 | swift-package-manager-test: 50 | name: Swift Package Manager 51 | runs-on: macos-11 52 | 53 | steps: 54 | - uses: actions/checkout@v1 55 | - name: swift test 56 | run: swift test 57 | 58 | cocoapods-verify: 59 | name: CocoaPods 60 | runs-on: macos-11 61 | 62 | steps: 63 | - uses: actions/checkout@v1 64 | - name: Lint CocoaPods specification 65 | run: pod lib lint 66 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample/TableViewController.swift: -------------------------------------------------------------------------------- 1 | import Differ 2 | import UIKit 3 | 4 | class TableViewController: UITableViewController { 5 | 6 | var objects = [ 7 | [ 8 | "🌞", 9 | "🐩", 10 | "👌🏽", 11 | "🦄", 12 | "👋🏻", 13 | "🙇🏽‍♀️", 14 | "🔥", 15 | ], 16 | [ 17 | "🐩", 18 | "🌞", 19 | "👌🏽", 20 | "🙇🏽‍♀️", 21 | "🔥", 22 | "👋🏻", 23 | ] 24 | ] 25 | 26 | 27 | var currentObjects = 0 { 28 | didSet { 29 | tableView.animateRowChanges( 30 | oldData: objects[oldValue], 31 | newData: objects[currentObjects], 32 | deletionAnimation: .right, 33 | insertionAnimation: .right) 34 | } 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | // Do any additional setup after loading the view, typically from a nib. 40 | 41 | let addButton = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refresh(_:))) 42 | self.navigationItem.rightBarButtonItem = addButton 43 | } 44 | 45 | @IBAction func refresh(_ sender: Any) { 46 | currentObjects = currentObjects == 0 ? 1 : 0; 47 | } 48 | 49 | // MARK: - Table View 50 | 51 | override func numberOfSections(in tableView: UITableView) -> Int { 52 | return 1 53 | } 54 | 55 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 56 | return objects[currentObjects].count 57 | } 58 | 59 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 60 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 61 | cell.textLabel?.text = objects[currentObjects][indexPath.row] 62 | return cell 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Examples/TableViewExample/Graph.playground/Sources/Arrows.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | // https://gist.github.com/mayoff/4146780 rewritten in Swift 5 | 6 | public struct Arrow { 7 | let from: CGPoint 8 | let to: CGPoint 9 | let tailWidth: CGFloat 10 | let headWidth: CGFloat 11 | let headLength: CGFloat 12 | } 13 | 14 | public extension UIBezierPath { 15 | convenience init(arrow: Arrow) { 16 | let length = CGFloat(hypotf(Float(arrow.to.x - arrow.from.x), Float(arrow.to.y - arrow.from.y))) 17 | var points = [CGPoint]() 18 | 19 | let tailLength = length - arrow.headLength 20 | points.append(CGPoint(x: 0, y: arrow.tailWidth / 2)) 21 | points.append(CGPoint(x: tailLength, y: arrow.tailWidth / 2)) 22 | points.append(CGPoint(x: tailLength, y: arrow.headWidth / 2)) 23 | points.append(CGPoint(x: length, y: 0)) 24 | points.append(CGPoint(x: tailLength, y: -arrow.headWidth / 2)) 25 | points.append(CGPoint(x: tailLength, y: -arrow.tailWidth / 2)) 26 | points.append(CGPoint(x: 0, y: -arrow.tailWidth / 2)) 27 | 28 | let transform = UIBezierPath.transform(from: arrow.from, to: arrow.to, length: length) 29 | let path = CGMutablePath() 30 | path.addLines(between: points, transform: transform) 31 | path.closeSubpath() 32 | self.init(cgPath: path) 33 | } 34 | 35 | private static func transform(from start: CGPoint, to: CGPoint, length: CGFloat) -> CGAffineTransform { 36 | let cosine = (to.x - start.x) / length 37 | let sine = (to.y - start.y) / length 38 | return CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y) 39 | } 40 | 41 | func shapeLayer() -> CAShapeLayer { 42 | let l = CAShapeLayer() 43 | let bounds = self.bounds 44 | let origin = bounds.origin 45 | let path = copy() as! UIBezierPath 46 | 47 | path.apply(CGAffineTransform(translationX: -origin.x, y: -origin.y)) 48 | 49 | l.path = path.cgPath 50 | l.frame = bounds 51 | return l 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Differ/NestedBatchUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 wczekalski. All rights reserved. 3 | 4 | #if !os(watchOS) 5 | import Foundation 6 | 7 | public struct NestedBatchUpdate { 8 | public let itemDeletions: [IndexPath] 9 | public let itemInsertions: [IndexPath] 10 | public let itemMoves: [(from: IndexPath, to: IndexPath)] 11 | public let sectionDeletions: IndexSet 12 | public let sectionInsertions: IndexSet 13 | public let sectionMoves: [(from: Int, to: Int)] 14 | 15 | public init( 16 | diff: NestedExtendedDiff, 17 | indexPathTransform: (IndexPath) -> IndexPath = { $0 }, 18 | sectionTransform: (Int) -> Int = { $0 } 19 | ) { 20 | var itemDeletions: [IndexPath] = [] 21 | var itemInsertions: [IndexPath] = [] 22 | var itemMoves: [(IndexPath, IndexPath)] = [] 23 | var sectionDeletions: IndexSet = [] 24 | var sectionInsertions: IndexSet = [] 25 | var sectionMoves: [(from: Int, to: Int)] = [] 26 | 27 | diff.forEach { element in 28 | switch element { 29 | case let .deleteElement(at, section): 30 | itemDeletions.append(indexPathTransform([section, at])) 31 | case let .insertElement(at, section): 32 | itemInsertions.append(indexPathTransform([section, at])) 33 | case let .moveElement(from, to): 34 | itemMoves.append((indexPathTransform([from.section, from.item]), indexPathTransform([to.section, to.item]))) 35 | case let .deleteSection(at): 36 | sectionDeletions.insert(sectionTransform(at)) 37 | case let .insertSection(at): 38 | sectionInsertions.insert(sectionTransform(at)) 39 | case let .moveSection(moveFrom, moveTo): 40 | sectionMoves.append((sectionTransform(moveFrom), sectionTransform(moveTo))) 41 | } 42 | } 43 | 44 | self.itemInsertions = itemInsertions 45 | self.itemDeletions = itemDeletions 46 | self.itemMoves = itemMoves 47 | self.sectionMoves = sectionMoves 48 | self.sectionInsertions = sectionInsertions 49 | self.sectionDeletions = sectionDeletions 50 | } 51 | } 52 | 53 | #endif 54 | 55 | -------------------------------------------------------------------------------- /Examples/TableViewExample/Graph.playground/Sources/GraphView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphView.swift 3 | // Graph 4 | // 5 | // Created by Wojciech Czekalski on 19.03.2016. 6 | // Copyright © 2016 wczekalski. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | @testable import Differ 11 | 12 | public struct Grid { 13 | let x: Int 14 | let y: Int 15 | } 16 | 17 | public struct Graph { 18 | let grid: Grid 19 | let bounds: CGRect 20 | } 21 | 22 | public extension Graph { 23 | func gridLayers() -> [CALayer] { 24 | guard grid.x > 0 && grid.y > 0 else { 25 | return [] 26 | } 27 | 28 | var layers = [CALayer]() 29 | let lineWidth: CGFloat = 1 30 | let layer: (CGRect) -> CALayer = { rect in 31 | let layer = CALayer() 32 | layer.frame = rect 33 | layer.backgroundColor = UIColor.white.cgColor 34 | return layer 35 | } 36 | 37 | for i in 0 ... (grid.x) { 38 | let rect = CGRect(x: x(at: i), y: y(at: 0), width: lineWidth, height: bounds.height) 39 | layers.append(layer(rect)) 40 | } 41 | 42 | for i in 0 ... (grid.y) { 43 | let rect = CGRect(x: x(at: 0), y: y(at: i), width: bounds.width, height: lineWidth) 44 | layers.append(layer(rect)) 45 | } 46 | 47 | return layers 48 | } 49 | 50 | func rect(at point: Point) -> CGRect { 51 | let origin = coordinates(at: point) 52 | let width = x(at: point.x + 1) - origin.x 53 | let height = y(at: point.y + 1) - origin.y 54 | return CGRect(origin: origin, size: CGSize(width: width, height: height)) 55 | } 56 | 57 | func rects(row y: Int) -> [CGRect] { 58 | return (0 ..< grid.x).map { 59 | rect(at: Point(x: $0, y: y)) 60 | } 61 | } 62 | 63 | func rects(column x: Int) -> [CGRect] { 64 | return (0 ..< grid.y).map { 65 | rect(at: Point(x: x, y: $0)) 66 | } 67 | } 68 | 69 | func coordinates(at point: Point) -> CGPoint { 70 | return CGPoint(x: x(at: point.x), y: y(at: point.y)) 71 | } 72 | 73 | func x(at x: Int) -> CGFloat { 74 | return bounds.width / CGFloat(grid.x) * CGFloat(x) + bounds.origin.x 75 | } 76 | 77 | func y(at y: Int) -> CGFloat { 78 | return bounds.height / CGFloat(grid.y) * CGFloat(y) + bounds.origin.y 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Differ/Patch.swift: -------------------------------------------------------------------------------- 1 | /// Single step in a patch sequence. 2 | public enum Patch { 3 | /// A single patch step containing an insertion index and an element to be inserted 4 | case insertion(index: Int, element: Element) 5 | /// A single patch step containing a deletion index 6 | case deletion(index: Int) 7 | 8 | func index() -> Int { 9 | switch self { 10 | case let .insertion(index, _): 11 | return index 12 | case let .deletion(index): 13 | return index 14 | } 15 | } 16 | } 17 | 18 | public extension Diff { 19 | 20 | /// Generates a patch sequence based on a diff. It is a list of steps to be applied to obtain the `to` collection from the `from` one. 21 | /// 22 | /// - Complexity: O(N) 23 | /// 24 | /// - Parameters: 25 | /// - to: The target collection (usually the target collecetion of the callee) 26 | /// - Returns: A sequence of steps to obtain `to` collection from the `from` one. 27 | func patch(to: T) -> [Patch] { 28 | var shift = 0 29 | return map { element in 30 | switch element { 31 | case let .delete(at): 32 | shift -= 1 33 | return .deletion(index: at + shift + 1) 34 | case let .insert(at): 35 | shift += 1 36 | return .insertion(index: at, element: to.itemOnStartIndex(advancedBy: at)) 37 | } 38 | } 39 | } 40 | } 41 | 42 | /// Generates a patch sequence. 43 | /// 44 | /// The generated sequence contains a list of steps to be applied to obtain the `to` collection from the `from` one. 45 | /// 46 | /// - Complexity: ```O((N+M)*D)``` 47 | /// 48 | /// - Parameters: 49 | /// - from: The source collection 50 | /// - to: The target collection 51 | /// - Returns: A sequence of steps to obtain `to` collection from the `from` one. 52 | public func patch( 53 | from: T, 54 | to: T 55 | ) -> [Patch] where T.Element: Equatable { 56 | return from.diff(to).patch(to: to) 57 | } 58 | 59 | extension Patch: CustomDebugStringConvertible { 60 | public var debugDescription: String { 61 | switch self { 62 | case let .deletion(at): 63 | return "D(\(at))" 64 | case let .insertion(at, element): 65 | return "I(\(at),\(element))" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Differ/Patch+Sort.swift: -------------------------------------------------------------------------------- 1 | /// Generates an arbitrarly sorted patch sequence. 2 | /// 3 | /// The patch sequence contains a list of steps to be applied to obtain the `to` collection from the `from` one. The sorting function lets you sort the output e.g. you might want the output patch to have insertions first. 4 | /// 5 | /// - Complexity: O((N+M)*D) 6 | /// 7 | /// - Parameters: 8 | /// - from: The source collection 9 | /// - to: The target collection 10 | /// - sort: A sorting function 11 | /// - Returns: Arbitrarly sorted sequence of steps to obtain `to` collection from the `from` one. 12 | public func patch( 13 | from: T, 14 | to: T, 15 | sort: Diff.OrderedBefore 16 | ) -> [Patch] where T.Element: Equatable { 17 | return from.diff(to).patch(from: from, to: to, sort: sort) 18 | } 19 | 20 | public extension Diff { 21 | 22 | typealias OrderedBefore = (_ fst: Diff.Element, _ snd: Diff.Element) -> Bool 23 | 24 | /// Generates an arbitrarly sorted patch sequence based on the callee. 25 | /// 26 | /// The patch sequence contains a list of steps to be applied to obtain the `to` collection from the `from` one. The sorting function lets you sort the output e.g. you might want the output patch to have insertions first. 27 | /// 28 | /// - Complexity: O(D^2) 29 | /// 30 | /// - Parameters: 31 | /// - from: The source collection (usually the source collecetion of the callee) 32 | /// - to: The target collection (usually the target collecetion of the callee) 33 | /// - sort: A sorting function 34 | /// - Returns: Arbitrarly sorted sequence of steps to obtain `to` collection from the `from` one. 35 | func patch( 36 | from: T, 37 | to: T, 38 | sort: OrderedBefore 39 | ) -> [Patch] { 40 | let shiftedPatch = patch(to: to) 41 | return shiftedPatchElements(from: sortedPatchElements( 42 | from: shiftedPatch, 43 | sortBy: sort 44 | )).map { $0.value } 45 | } 46 | 47 | private func sortedPatchElements(from source: [Patch], sortBy areInIncreasingOrder: OrderedBefore) -> [SortedPatchElement] { 48 | let sorted = indices.map { (self[$0], $0) } 49 | .sorted { areInIncreasingOrder($0.0, $1.0) } 50 | return sorted.indices.map { i in 51 | let p = sorted[i] 52 | return SortedPatchElement( 53 | value: source[p.1], 54 | sourceIndex: p.1, 55 | sortedIndex: i) 56 | }.sorted(by: { (fst, snd) -> Bool in 57 | fst.sourceIndex < snd.sourceIndex 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/DifferTests/PatchApplyTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Differ 3 | 4 | class PatchApplyTests: XCTestCase { 5 | func testString() { 6 | 7 | let testCases: [(String, String, String)] = [ 8 | ("", "I(0,A)I(0,B)I(0,C)", "CBA"), 9 | ("", "I(0,A)I(1,B)I(1,C)", "ACB"), 10 | ("AB", "D(1)I(1,B)I(1,C)", "ACB"), 11 | ("AB", "I(1,B)D(0)I(1,C)", "BCB"), 12 | ("A", "I(0,B)D(0)", "A") 13 | ] 14 | 15 | testCases.forEach { args in 16 | let (seed, patchString, result) = args 17 | XCTAssertEqual(seed.apply(stringPatch(from: patchString)), result) 18 | } 19 | } 20 | 21 | func testCollection() { 22 | 23 | let testCases: [([Int], String, [Int])] = [ 24 | ([], "I(0,0)I(0,1)I(0,2)", [2, 1, 0]), 25 | ([], "I(0,0)I(1,1)I(1,2)", [0, 2, 1]), 26 | ([0, 1], "D(1)I(1,1)I(1,2)", [0, 2, 1]), 27 | ([0, 1], "I(1,1)D(0)I(1,2)", [1, 2, 1]), 28 | ([0], "I(0,1)D(0)", [0]) 29 | ] 30 | 31 | testCases.forEach { args in 32 | let (seed, patchString, result) = args 33 | XCTAssertEqual(seed.apply(intPatch(from: patchString)), result) 34 | } 35 | } 36 | } 37 | 38 | func stringPatch(from textualRepresentation: String) -> [Patch] { 39 | return textualRepresentation.components(separatedBy: ")").compactMap { string in 40 | if string == "" { 41 | return nil 42 | } 43 | let type = string.prefix(1) 44 | if type == "D" { 45 | let startIndex = string.index(string.startIndex, offsetBy: 2) 46 | let index = Int(string[startIndex...])! 47 | return .deletion(index: index) 48 | } else if type == "I" { 49 | let startIndex = string.index(string.startIndex, offsetBy: 2) 50 | let indexAndElement = string[startIndex...].components(separatedBy: ",") 51 | return .insertion(index: Int(indexAndElement[0])!, element: indexAndElement[1].first!) 52 | } else { 53 | return nil 54 | } 55 | } 56 | } 57 | 58 | func intPatch(from textualRepresentation: String) -> [Patch] { 59 | return textualRepresentation.components(separatedBy: ")").compactMap { string in 60 | if string == "" { 61 | return nil 62 | } 63 | let type = string.prefix(1) 64 | if type == "D" { 65 | let startIndex = string.index(string.startIndex, offsetBy: 2) 66 | let index = Int(string[startIndex...])! 67 | return .deletion(index: index) 68 | } else if type == "I" { 69 | let startIndex = string.index(string.startIndex, offsetBy: 2) 70 | let indexAndElement = string[startIndex...].components(separatedBy: ",") 71 | let index = Int(indexAndElement[0])! 72 | let element = Int(indexAndElement[1])! 73 | return .insertion(index: index, element: element) 74 | } else { 75 | return nil 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/DifferTests/BatchUpdateTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Differ 2 | import XCTest 3 | 4 | private func IP(_ row: Int, _ section: Int) -> IndexPath { 5 | return [section, row] 6 | } 7 | 8 | class BatchUpdateTests: XCTestCase { 9 | private struct Expectation { 10 | let orderBefore: [Int] 11 | let orderAfter: [Int] 12 | let insertions: [IndexPath] 13 | let deletions: [IndexPath] 14 | let moves: [BatchUpdate.MoveStep] 15 | } 16 | 17 | private let cellExpectations: [Expectation] = [ 18 | Expectation(orderBefore: [1, 2, 3, 4], orderAfter: [1, 2, 3, 4], insertions: [], deletions: [], moves: []), 19 | Expectation(orderBefore: [1, 2, 3, 4], orderAfter: [4, 2, 3, 1], insertions: [], deletions: [], moves: [BatchUpdate.MoveStep(from: IP(0, 0), to: IP(3, 0)), BatchUpdate.MoveStep(from: IP(3, 0), to: IP(0, 0))]), 20 | Expectation(orderBefore: [1, 2, 3, 4], orderAfter: [2, 3, 1], insertions: [], deletions: [IP(3, 0)], moves: [BatchUpdate.MoveStep(from: IP(0, 0), to: IP(2, 0))]), 21 | Expectation(orderBefore: [1, 2, 3, 4], orderAfter: [5, 2, 3, 4], insertions: [IP(0, 0)], deletions: [IP(0, 0)], moves: []), 22 | Expectation(orderBefore: [1, 2, 3, 4], orderAfter: [4, 1, 3, 5], insertions: [IP(3, 0)], deletions: [IP(1, 0)], moves: [BatchUpdate.MoveStep(from: IP(3, 0), to: IP(0, 0))]), 23 | Expectation(orderBefore: [1, 2, 3, 4], orderAfter: [4, 2, 3, 4], insertions: [IP(0, 0)], deletions: [IP(0, 0)], moves: []), 24 | Expectation(orderBefore: [1, 2, 3, 4], orderAfter: [1, 2, 4, 4], insertions: [IP(3, 0)], deletions: [IP(2, 0)], moves: []), 25 | Expectation(orderBefore: [1, 2, 3, 4], orderAfter: [5, 6, 7, 8], insertions: [IP(0, 0), IP(1, 0), IP(2, 0), IP(3, 0)], deletions: [IP(0, 0), IP(1, 0), IP(2, 0), IP(3, 0)], moves: []), 26 | Expectation(orderBefore: [1, 2, 3, 4], orderAfter: [5, 6, 7, 1], insertions: [IP(0, 0), IP(1, 0), IP(2, 0)], deletions: [IP(1, 0), IP(2, 0), IP(3, 0)], moves: []) 27 | ] 28 | 29 | func testCells() { 30 | self._testCells() 31 | } 32 | 33 | func testCellsWithTransform() { 34 | self._testCellsWithTransform() 35 | } 36 | 37 | func _testCells() { 38 | for expectation in self.cellExpectations { 39 | let batch = BatchUpdate(diff: expectation.orderBefore.extendedDiff(expectation.orderAfter)) 40 | XCTAssertEqual(batch.deletions, expectation.deletions) 41 | XCTAssertEqual(batch.insertions, expectation.insertions) 42 | XCTAssertEqual(batch.moves, expectation.moves) 43 | } 44 | } 45 | 46 | func _testCellsWithTransform() { 47 | let transform: (IndexPath) -> IndexPath = { IP($0.item + 1, $0.section + 2) } 48 | 49 | for expectation in self.cellExpectations { 50 | let batch = BatchUpdate(diff: expectation.orderBefore.extendedDiff(expectation.orderAfter), indexPathTransform: transform) 51 | XCTAssertEqual(batch.deletions, expectation.deletions.map(transform)) 52 | XCTAssertEqual(batch.insertions, expectation.insertions.map(transform)) 53 | XCTAssertEqual(batch.moves, expectation.moves.map { BatchUpdate.MoveStep(from: transform($0.from), to: transform($0.to)) }) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at me@wczekalski.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Sources/Differ/LinkedList.swift: -------------------------------------------------------------------------------- 1 | class LinkedList: Sequence { 2 | let next: LinkedList? 3 | let value: T 4 | 5 | init(next: LinkedList?, value: T) { 6 | self.next = next 7 | self.value = value 8 | } 9 | 10 | init?(array: [T]) { 11 | let reversed = array.reversed() 12 | guard let first = array.first else { 13 | return nil 14 | } 15 | 16 | var tailLinkedList: LinkedList? 17 | 18 | for i in 0 ..< reversed.count - 1 { 19 | tailLinkedList = LinkedList(next: tailLinkedList, value: reversed.itemOnStartIndex(advancedBy: i)) 20 | } 21 | 22 | next = tailLinkedList 23 | value = first 24 | } 25 | 26 | /// Non-consuming iterator. 27 | func makeIterator() -> AnyIterator { 28 | var node = self as Optional 29 | 30 | return AnyIterator({ 31 | if let unwrapped = node { 32 | node = unwrapped.next 33 | return unwrapped.value 34 | } else { 35 | return nil 36 | } 37 | }) 38 | } 39 | } 40 | 41 | class DoublyLinkedList: Sequence { 42 | let next: DoublyLinkedList? 43 | private(set) weak var previous: DoublyLinkedList? 44 | var head: DoublyLinkedList { 45 | guard let previous = previous else { 46 | return self 47 | } 48 | return previous.head 49 | } 50 | 51 | var value: T 52 | 53 | init(next: DoublyLinkedList?, value: T) { 54 | self.value = value 55 | self.next = next 56 | self.next?.previous = self 57 | } 58 | 59 | init?(array: [T]) { 60 | let reversed = array.reversed() 61 | guard let first = array.first else { 62 | return nil 63 | } 64 | 65 | var tailDoublyLinkedList: DoublyLinkedList? 66 | 67 | for i in 0 ..< reversed.count - 1 { 68 | let nextTail = DoublyLinkedList(next: tailDoublyLinkedList, value: reversed.itemOnStartIndex(advancedBy: i)) 69 | tailDoublyLinkedList?.previous = nextTail 70 | tailDoublyLinkedList = nextTail 71 | } 72 | 73 | value = first 74 | next = tailDoublyLinkedList 75 | next?.previous = self 76 | } 77 | 78 | convenience init?(linkedList: LinkedList?) { 79 | guard let linkedList = linkedList else { 80 | return nil 81 | } 82 | self.init(array: Array(linkedList)) 83 | } 84 | 85 | /// Non-consuming iterator. 86 | func makeIterator() -> AnyIterator { 87 | var node = self as Optional 88 | 89 | return AnyIterator({ 90 | if let unwrapped = node { 91 | node = unwrapped.next 92 | return unwrapped.value 93 | } else { 94 | return nil 95 | } 96 | }) 97 | } 98 | 99 | /// Non-consuming iterator. 100 | func makeNodeIterator() -> AnyIterator> { 101 | var node = self as Optional 102 | 103 | return AnyIterator({ 104 | if let unwrapped = node { 105 | node = unwrapped.next 106 | return unwrapped 107 | } else { 108 | return nil 109 | } 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Differ/GenericPatch.swift: -------------------------------------------------------------------------------- 1 | struct SortedPatchElement { 2 | var value: Patch 3 | let sourceIndex: Int 4 | let sortedIndex: Int 5 | } 6 | 7 | enum Direction { 8 | case left 9 | case right 10 | } 11 | 12 | enum EdgeType { 13 | case cycle 14 | case neighbor(direction: Direction) 15 | case jump(direction: Direction) 16 | } 17 | 18 | func edgeType(from: DoublyLinkedList>, to: DoublyLinkedList>) -> EdgeType { 19 | let fromIndex = from.value.sortedIndex 20 | let toIndex = to.value.sortedIndex 21 | 22 | if fromIndex == toIndex { 23 | return .cycle 24 | } else if abs(fromIndex - toIndex) == 1 { 25 | if fromIndex > toIndex { 26 | return .neighbor(direction: .left) 27 | } else { 28 | return .neighbor(direction: .right) 29 | } 30 | } else if fromIndex > toIndex { 31 | return .jump(direction: .left) 32 | } else { 33 | return .jump(direction: .right) 34 | } 35 | } 36 | 37 | func shiftPatchElement(node list: DoublyLinkedList>) { 38 | for node in list.makeNodeIterator() { 39 | var from = node.previous 40 | while let nextFrom = from, nextFrom.value.sourceIndex < node.value.sourceIndex { 41 | shiftPatchElement(from: nextFrom, to: node) 42 | from = nextFrom.previous 43 | } 44 | } 45 | } 46 | 47 | func shiftPatchElement(from: DoublyLinkedList>, to: DoublyLinkedList>) { 48 | let type = edgeType(from: from, to: to) 49 | switch type { 50 | case .cycle: 51 | fatalError() 52 | case let .neighbor(direction), let .jump(direction): 53 | if case .left = direction { 54 | switch (from.value.value, to.value.value) { 55 | case (.insertion, _): 56 | to.value = to.value.decremented() 57 | case (.deletion, _): 58 | to.value = to.value.incremented() 59 | } 60 | } 61 | } 62 | } 63 | 64 | extension SortedPatchElement { 65 | func incremented() -> SortedPatchElement { 66 | return SortedPatchElement( 67 | value: value.incremented(), 68 | sourceIndex: sourceIndex, 69 | sortedIndex: sortedIndex) 70 | } 71 | 72 | func decremented() -> SortedPatchElement { 73 | return SortedPatchElement( 74 | value: value.decremented(), 75 | sourceIndex: sourceIndex, 76 | sortedIndex: sortedIndex) 77 | } 78 | } 79 | 80 | extension Patch { 81 | 82 | func incremented() -> Patch { 83 | return shiftedIndex(by: 1) 84 | } 85 | 86 | func decremented() -> Patch { 87 | return shiftedIndex(by: -1) 88 | } 89 | 90 | func shiftedIndex(by n: Int) -> Patch { 91 | switch self { 92 | case let .insertion(index, element): 93 | return .insertion(index: index + n, element: element) 94 | case let .deletion(index): 95 | return .deletion(index: index + n) 96 | } 97 | } 98 | } 99 | 100 | func shiftedPatchElements(from sortedPatchElements: [SortedPatchElement]) -> [SortedPatchElement] { 101 | let linkedList = DoublyLinkedList(linkedList: LinkedList(array: sortedPatchElements)) 102 | if let secondElement = linkedList?.next { 103 | shiftPatchElement(node: secondElement) 104 | } 105 | 106 | guard let result = linkedList?.sorted(by: { (fst, second) -> Bool in 107 | fst.sortedIndex < second.sortedIndex 108 | }) else { 109 | return [] 110 | } 111 | return result 112 | } 113 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample/NestedTableViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct StringArray: Equatable, Collection { 4 | 5 | let elements: [String] 6 | let key: String 7 | 8 | typealias Index = Int 9 | 10 | var startIndex: Int { 11 | return elements.startIndex 12 | } 13 | 14 | var endIndex: Int { 15 | return elements.endIndex 16 | } 17 | 18 | subscript(i: Int) -> String { 19 | return elements[i] 20 | } 21 | 22 | public func index(after i: Int) -> Int { 23 | return elements.index(after: i) 24 | } 25 | 26 | static func ==(fst: StringArray, snd: StringArray) -> Bool { 27 | return fst.key == snd.key 28 | } 29 | } 30 | 31 | class NestedTableViewController: UITableViewController { 32 | 33 | let items = [ 34 | [ 35 | StringArray( 36 | elements: [ 37 | "🌞", 38 | "🐩", 39 | ], 40 | key: "First" 41 | ), 42 | StringArray( 43 | elements: [ 44 | "👋🏻", 45 | "🎁", 46 | ], 47 | key: "Second" 48 | ), 49 | ], 50 | [ 51 | StringArray( 52 | elements: [ 53 | "🎁", 54 | "👋🏻", 55 | ], 56 | key: "Second" 57 | ), 58 | StringArray( 59 | elements: [ 60 | "🌞", 61 | "🐩", 62 | ], 63 | key: "First" 64 | ), 65 | StringArray( 66 | elements: [ 67 | "😊", 68 | ], 69 | key: "Third" 70 | ), 71 | ], 72 | ] 73 | 74 | var currentConfiguration = 0 { 75 | didSet { 76 | tableView.animateRowAndSectionChanges( 77 | oldData: items[oldValue], 78 | newData: items[currentConfiguration] 79 | ) 80 | } 81 | } 82 | 83 | private let reuseIdentifier = "Cell" 84 | 85 | override func viewDidLoad() { 86 | super.viewDidLoad() 87 | 88 | let addButton = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refresh(_:))) 89 | navigationItem.rightBarButtonItem = addButton 90 | } 91 | 92 | @IBAction func refresh(_ sender: Any) { 93 | currentConfiguration = currentConfiguration == 0 ? 1 : 0 94 | } 95 | 96 | override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 97 | let view = UILabel() 98 | view.text = items[currentConfiguration][section].key 99 | view.sizeToFit() 100 | return view 101 | } 102 | 103 | override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 104 | return 30 105 | } 106 | 107 | override func numberOfSections(in tableView: UITableView) -> Int { 108 | return items[currentConfiguration].count 109 | } 110 | 111 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 112 | return items[currentConfiguration][section].elements.count 113 | } 114 | 115 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 116 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 117 | cell.textLabel?.text = items[currentConfiguration][indexPath.section].elements[indexPath.row] 118 | return cell 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample.xcodeproj/xcshareddata/xcschemes/TableViewExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Scripts/build-xcframework.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | OUTPUT_DIR=${OUTPUT_DIR?error: Please provide the build directory via the 'OUTPUT_DIR' environment variable} 7 | PROJECT_FILE=${PROJECT_FILE?error: Please provide the Xcode project file name via the 'PROJECT_FILE' environment variable} 8 | SCHEME_NAME=${SCHEME_NAME?error: Please provide the Xcode scheme to build via the 'SCHEME_NAME' environment variable} 9 | 10 | # Generate iOS framework 11 | xcodebuild -project "${PROJECT_FILE}" -configuration Release -archivePath "${OUTPUT_DIR}/${SCHEME_NAME}-iphoneos.xcarchive" -destination "generic/platform=iOS" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES -scheme "${SCHEME_NAME}" archive 12 | 13 | # Generate iOS Simulator framework 14 | xcodebuild -project "${PROJECT_FILE}" -configuration Release -archivePath "${OUTPUT_DIR}/${SCHEME_NAME}-iossimulator.xcarchive" -destination "generic/platform=iOS Simulator" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES -scheme "${SCHEME_NAME}" archive 15 | 16 | # Generate macOS framework 17 | xcodebuild -project "${PROJECT_FILE}" -configuration Release -archivePath "${OUTPUT_DIR}/${SCHEME_NAME}-macosx.xcarchive" -destination "generic/platform=macOS,name=Any Mac" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES -scheme "${SCHEME_NAME}" archive 18 | 19 | # Generate macOS Catalyst framework 20 | xcodebuild -project "${PROJECT_FILE}" -configuration Release -archivePath "${OUTPUT_DIR}/${SCHEME_NAME}-maccatalyst.xcarchive" -destination "generic/platform=macOS,variant=Mac Catalyst" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES SUPPORTS_MACCATALYST=YES -scheme "${SCHEME_NAME}" archive 21 | 22 | # Generate tvOS framework 23 | xcodebuild -project "${PROJECT_FILE}" -configuration Release -archivePath "${OUTPUT_DIR}/${SCHEME_NAME}-appletvos.xcarchive" -destination "generic/platform=tvOS" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES -scheme "${SCHEME_NAME}" archive 24 | 25 | # Generate tvOS Simulator framework 26 | xcodebuild -project "${PROJECT_FILE}" -configuration Release -archivePath "${OUTPUT_DIR}/${SCHEME_NAME}-appletvsimulator.xcarchive" -destination "generic/platform=tvOS Simulator" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES -scheme "${SCHEME_NAME}" archive 27 | 28 | # Generate watchOS framework 29 | xcodebuild -project "${PROJECT_FILE}" -configuration Release -archivePath "${OUTPUT_DIR}/${SCHEME_NAME}-watchos.xcarchive" -destination "generic/platform=watchOS" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES -scheme "${SCHEME_NAME}" archive 30 | 31 | # Generate watchOS Simulator framework 32 | xcodebuild -project "${PROJECT_FILE}" -configuration Release -archivePath "${OUTPUT_DIR}/${SCHEME_NAME}-watchsimulator.xcarchive" -destination "generic/platform=watchOS Simulator" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES -scheme "${SCHEME_NAME}" archive 33 | 34 | # Generate XCFramework 35 | xcodebuild -create-xcframework \ 36 | -archive "${OUTPUT_DIR}/${SCHEME_NAME}-appletvos.xcarchive" -framework "${SCHEME_NAME}.framework" \ 37 | -archive "${OUTPUT_DIR}/${SCHEME_NAME}-appletvsimulator.xcarchive" -framework "${SCHEME_NAME}.framework" \ 38 | -archive "${OUTPUT_DIR}/${SCHEME_NAME}-iphoneos.xcarchive" -framework "${SCHEME_NAME}.framework" \ 39 | -archive "${OUTPUT_DIR}/${SCHEME_NAME}-iossimulator.xcarchive" -framework "${SCHEME_NAME}.framework" \ 40 | -archive "${OUTPUT_DIR}/${SCHEME_NAME}-macosx.xcarchive" -framework "${SCHEME_NAME}.framework" \ 41 | -archive "${OUTPUT_DIR}/${SCHEME_NAME}-maccatalyst.xcarchive" -framework "${SCHEME_NAME}.framework" \ 42 | -archive "${OUTPUT_DIR}/${SCHEME_NAME}-watchos.xcarchive" -framework "${SCHEME_NAME}.framework" \ 43 | -archive "${OUTPUT_DIR}/${SCHEME_NAME}-watchsimulator.xcarchive" -framework "${SCHEME_NAME}.framework" \ 44 | -output "${OUTPUT_DIR}/${SCHEME_NAME}.xcframework" 45 | 46 | # Zip it! 47 | ditto -c -k --rsrc --keepParent "${OUTPUT_DIR}/${SCHEME_NAME}.xcframework" "${OUTPUT_DIR}/${SCHEME_NAME}.xcframework.zip" 48 | rm -rf "${OUTPUT_DIR}/${SCHEME_NAME}.xcframework" 49 | -------------------------------------------------------------------------------- /Differ.xcodeproj/xcshareddata/xcschemes/Differ.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 35 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Tests/DifferTests/ExtendedPatchSortTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Differ 3 | 4 | class ExtendedPatchSortTests: XCTestCase { 5 | 6 | func testDefaultOrder() { 7 | let expectations = [ 8 | ("gitten", "sitting", "M(0,5)I(0,s)D(4)I(4,i)"), 9 | ("Oh Hi", "Hi Oh", "M(0,4)M(0,4)M(0,2)"), 10 | ("12345", "12435", "M(2,3)"), 11 | ("1362", "31526", "M(0,2)M(1,3)I(2,5)"), 12 | ("221", "122", "M(2,0)") 13 | ] 14 | 15 | for expectation in expectations { 16 | XCTAssertEqual( 17 | _extendedTest(from: expectation.0, to: expectation.1), 18 | expectation.2) 19 | } 20 | } 21 | 22 | func testInsertionDeletionMove() { 23 | let expectations = [ 24 | ("gitten", "sitting", "I(5,i)I(1,s)D(5)M(0,6)"), 25 | ("1362", "31526", "I(3,5)M(0,2)M(1,4)") 26 | ] 27 | 28 | let sort: ExtendedSortingFunction = { fst, snd in 29 | switch (fst, snd) { 30 | case (.insert, _): 31 | return true 32 | case (.delete, .insert): 33 | return false 34 | case (.delete, _): 35 | return true 36 | case (.move, _): 37 | return false 38 | } 39 | } 40 | 41 | for expectation in expectations { 42 | XCTAssertEqual( 43 | _extendedTest( 44 | from: expectation.0, 45 | to: expectation.1, 46 | sortingFunction: sort), 47 | expectation.2) 48 | } 49 | } 50 | 51 | func testDeletionMoveInsertion() { 52 | let expectations = [ 53 | ("gitten", "sitting", "D(4)M(0,4)I(0,s)I(4,i)"), 54 | ("1362", "31526", "M(0,2)M(1,3)I(2,5)"), 55 | ("a1b2c3pq", "3sa1cz2rb", "D(7)D(6)M(5,0)M(3,5)M(3,4)I(1,s)I(5,z)I(7,r)") 56 | ] 57 | 58 | let sort: ExtendedSortingFunction = { fst, snd in 59 | switch (fst, snd) { 60 | case (.delete, _): 61 | return true 62 | case (.insert, _): 63 | return false 64 | case (.move, .insert): 65 | return true 66 | case (.move, _): 67 | return false 68 | } 69 | } 70 | 71 | for expectation in expectations { 72 | XCTAssertEqual( 73 | _extendedTest( 74 | from: expectation.0, 75 | to: expectation.1, 76 | sortingFunction: sort), 77 | expectation.2) 78 | } 79 | } 80 | 81 | func testRandomStringPermutationRandomPatchSort() { 82 | 83 | let sort: ExtendedSortingFunction = { _, _ in arc4random_uniform(2) == 0 84 | } 85 | for _ in 0 ..< 20 { 86 | let string1 = "eakjnrsignambmcbdcdhdkmhkolpdgfedcpgabtldjkaqkoobomuhpepirdcrdrgmrmaefesoiildmtnbronpmmbuuplnfnjgdhadkbmprensshiekknhskognpbknpbepmlakducnfktjeookncjpcnpklfedrebstisalskigsuojkookhbmkdafiaftrkrccupgjapqrigbanfbboapmicabeclhentlabourhtqmlboqctgorajirchesaorsgnigattkdrenquffcutffopbjrebegbfmkeikstqsut" 87 | let string2 = "mdjqtbchphncsjdkjtutagahmdtfcnjliipmqgrhgajsgotcdgidlghithdgrcmfuausmjnbtjghqblaiuldirulhllidbpcpglfbnfbkbddhdskdplsgjjsusractdplajrctgrcebhesbeneidsititlalsqkhliontgpesglkoorjqeniqaetatamneonhbhunqlfkbmfsjallnejhkcfaeapdnacqdtukcuiheiabqpudmgosssabisrrlmhcmpkgerhesqihdnfjmqgfnmulnfkmpqrsghutfsckurr" 88 | let patch = string1.extendedDiff(string2).patch( 89 | from: string1, 90 | to: string2, 91 | sort: sort) 92 | let result = string1.apply(patch) 93 | XCTAssertEqual(result, string2) 94 | } 95 | } 96 | } 97 | 98 | typealias ExtendedSortingFunction = (ExtendedDiff.Element, ExtendedDiff.Element) -> Bool 99 | 100 | func _extendedTest( 101 | from: String, 102 | to: String, 103 | sortingFunction: ExtendedSortingFunction? = nil) -> String { 104 | guard let sort = sortingFunction else { 105 | return extendedPatch( 106 | from: from, 107 | to: to) 108 | .reduce("") { $0 + $1.debugDescription } 109 | } 110 | return extendedPatch( 111 | from: from, 112 | to: to, 113 | sort: sort) 114 | .reduce("") { $0 + $1.debugDescription } 115 | } 116 | -------------------------------------------------------------------------------- /Tests/DifferTests/DiffTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Differ 3 | 4 | class DiffTests: XCTestCase { 5 | 6 | let expectations = [ 7 | ("kitten", "sitting", "D(0)I(0)D(4)I(4)I(6)"), 8 | ("🐩itt🍨ng", "kitten", "D(0)I(0)D(4)I(4)D(6)"), 9 | ("1234", "ABCD", "D(0)D(1)D(2)D(3)I(0)I(1)I(2)I(3)"), 10 | ("1234", "", "D(0)D(1)D(2)D(3)"), 11 | ("", "1234", "I(0)I(1)I(2)I(3)"), 12 | ("Hi", "Oh Hi", "I(0)I(1)I(2)"), 13 | ("Hi", "Hi O", "I(2)I(3)"), 14 | ("Oh Hi", "Hi", "D(0)D(1)D(2)"), 15 | ("Hi O", "Hi", "D(2)D(3)"), 16 | ("Wojtek", "Wojciech", "D(3)I(3)I(4)D(5)I(6)I(7)"), 17 | ("1234", "1234", ""), 18 | ("", "", ""), 19 | ("Oh Hi", "Hi Oh", "D(0)D(1)D(2)I(2)I(3)I(4)"), 20 | ("1362", "31526", "D(0)D(2)I(1)I(2)I(4)") 21 | ] 22 | 23 | let extendedExpectations = [ 24 | ("sitting", "kitten", "D(0)I(0)D(4)I(4)D(6)"), 25 | ("🐩itt🍨ng", "kitten", "D(0)I(0)D(4)I(4)D(6)"), 26 | ("1234", "ABCD", "D(0)D(1)D(2)D(3)I(0)I(1)I(2)I(3)"), 27 | ("1234", "", "D(0)D(1)D(2)D(3)"), 28 | ("", "1234", "I(0)I(1)I(2)I(3)"), 29 | ("Hi", "Oh Hi", "I(0)I(1)I(2)"), 30 | ("Hi", "Hi O", "I(2)I(3)"), 31 | ("Oh Hi", "Hi", "D(0)D(1)D(2)"), 32 | ("Hi O", "Hi", "D(2)D(3)"), 33 | ("Wojtek", "Wojciech", "D(3)I(3)I(4)D(5)I(6)I(7)"), 34 | ("1234", "1234", ""), 35 | ("", "", ""), 36 | ("gitten", "sitting", "M(0,6)I(0)D(4)I(4)"), 37 | ("Oh Hi", "Hi Oh", "M(0,3)M(1,4)M(2,2)"), 38 | ("Hi Oh", "Oh Hi", "M(0,3)M(1,4)M(2,2)"), 39 | ("12345", "12435", "M(2,3)"), 40 | ("1362", "31526", "M(0,1)M(2,4)I(2)") 41 | ] 42 | 43 | func testDiffOutputs() { 44 | for expectation in expectations { 45 | XCTAssertEqual( 46 | _test(from: expectation.0, to: expectation.1), 47 | expectation.2) 48 | } 49 | } 50 | 51 | func testExtendedDiffOutputs() { 52 | for expectation in extendedExpectations { 53 | XCTAssertEqual( 54 | _testExtended(from: expectation.0, to: expectation.1), 55 | expectation.2) 56 | } 57 | } 58 | 59 | func testDiffTracesForDeletion() { 60 | let traces = "test".diffTraces(to: "") 61 | 62 | XCTAssertEqual(4, traces.count) 63 | XCTAssertEqual(traces[0], Trace(from: Point(x: 0, y: 0), to: Point(x: 1, y: 0), D: 0)) 64 | XCTAssertEqual(traces[1], Trace(from: Point(x: 1, y: 0), to: Point(x: 2, y: 0), D: 0)) 65 | XCTAssertEqual(traces[2], Trace(from: Point(x: 2, y: 0), to: Point(x: 3, y: 0), D: 0)) 66 | XCTAssertEqual(traces[3], Trace(from: Point(x: 3, y: 0), to: Point(x: 4, y: 0), D: 0)) 67 | } 68 | 69 | func testDiffTracesForInsertion() { 70 | let traces = "".diffTraces(to: "test") 71 | 72 | XCTAssertEqual(4, traces.count) 73 | XCTAssertEqual(traces[0], Trace(from: Point(x: 0, y: 0), to: Point(x: 0, y: 1), D: 0)) 74 | XCTAssertEqual(traces[1], Trace(from: Point(x: 0, y: 1), to: Point(x: 0, y: 2), D: 0)) 75 | XCTAssertEqual(traces[2], Trace(from: Point(x: 0, y: 2), to: Point(x: 0, y: 3), D: 0)) 76 | XCTAssertEqual(traces[3], Trace(from: Point(x: 0, y: 3), to: Point(x: 0, y: 4), D: 0)) 77 | } 78 | 79 | // The tests below check efficiency of the algorithm 80 | 81 | func testDuplicateTraces() { 82 | for expectation in expectations { 83 | XCTAssertFalse(duplicateTraces(from: expectation.0, to: expectation.1)) 84 | } 85 | } 86 | 87 | func testTracesOutOfBounds() { 88 | for expectation in expectations { 89 | XCTAssertEqual(tracesOutOfBounds(from: expectation.0, to: expectation.1), [], "traces out of bounds for \(expectation.0) -> \(expectation.1)") 90 | } 91 | } 92 | 93 | func testSingleElementArray() { 94 | let changes = "a".diff("a") 95 | XCTAssertEqual(changes.elements.count, 0) 96 | } 97 | 98 | func duplicateTraces(from: String, to: String) -> Bool { 99 | let traces = from.diffTraces(to: to) 100 | let tracesSet = Set(traces) 101 | return !(traces.count == tracesSet.count) 102 | } 103 | 104 | func tracesOutOfBounds(from: String, to: String) -> [Trace] { 105 | let ac = from 106 | let bc = to 107 | return ac.diffTraces(to: bc) 108 | .filter { $0.to.y > bc.count || $0.to.x > ac.count } 109 | } 110 | 111 | func _test( 112 | from: String, 113 | to: String) -> String { 114 | return from 115 | .diff(to) 116 | .reduce("") { $0 + $1.debugDescription } 117 | } 118 | 119 | func _testExtended( 120 | from: String, 121 | to: String) -> String { 122 | return from 123 | .extendedDiff(to) 124 | .reduce("") { $0 + $1.debugDescription } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Examples/TableViewExample/Graph.playground/Sources/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Graph 4 | // 5 | // Created by Wojciech Czekalski on 21.03.2016. 6 | // Copyright © 2016 wczekalski. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | @testable import Differ 11 | 12 | public class GraphViewController: UIViewController { 13 | let dValueLabel = UILabel() 14 | let kValueLabel = UILabel() 15 | lazy var graphView: UIView = { 16 | let view = UIView() 17 | view.backgroundColor = self.backgroundColor() 18 | return view 19 | }() 20 | 21 | lazy var slider: UISlider = { 22 | let slider = UISlider() 23 | slider.addTarget(self, action: #selector(sliderDidChange), for: .valueChanged) 24 | return slider 25 | }() 26 | 27 | lazy var stackView: UIStackView = { 28 | let stackView = UIStackView(arrangedSubviews: [self.graphView, self.dValueLabel, self.kValueLabel, self.slider]) 29 | stackView.frame = self.view.bounds 30 | stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 31 | stackView.layoutMargins = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) 32 | stackView.isLayoutMarginsRelativeArrangement = true 33 | stackView.axis = .vertical 34 | return stackView 35 | }() 36 | 37 | var diffStrings = ("", "") { 38 | didSet { 39 | view.setNeedsLayout() 40 | } 41 | } 42 | 43 | var graph: Graph { 44 | let grid = Grid(x: diffStrings.0.length(), y: diffStrings.1.length()) 45 | return Graph(grid: grid, bounds: graphView.frame.insetBy(dx: 50, dy: 50)) 46 | } 47 | 48 | var arrows: [CAShapeLayer] = [] { 49 | didSet { 50 | oldValue.forEach { $0.removeFromSuperlayer() } 51 | arrows.forEach { graphView.layer.addSublayer($0) } 52 | } 53 | } 54 | 55 | var traces: [Trace] { 56 | return Array(diffStrings.0).diffTraces(to: Array(diffStrings.1)) 57 | } 58 | 59 | func display(range: Range? = nil) { 60 | graphView.layer.sublayers?.forEach { $0.removeFromSuperlayer() } 61 | var displayedTraces = traces 62 | if let range = range { 63 | displayedTraces = Array(displayedTraces[range]) 64 | slider.value = Float(range.upperBound - 1) / Float(traces.count - 1) 65 | } else { 66 | slider.value = 1 67 | } 68 | 69 | graph.gridLayers().forEach { graphView.layer.addSublayer($0) } 70 | arrows = displayedTraces.map { $0.shapeLayer(on: graph) } 71 | 72 | let labels1 = diffStrings.0.characterLabels(withFrames: graph.rects(row: -1)) 73 | let labels2 = diffStrings.1.characterLabels(withFrames: graph.rects(column: -1)) 74 | (labels1 + labels2).forEach { graphView.addSubview($0) } 75 | 76 | if let maxElement = displayedTraces.max(by: { $0.D < $1.D }) { 77 | dValueLabel.text = "Number of differences: \(maxElement.D)" 78 | kValueLabel.text = "k value \(maxElement.k())" 79 | } 80 | } 81 | 82 | @IBAction func sliderDidChange(sender: UISlider) { 83 | let maxIndex = Int(sender.value * Float(traces.count - 1)) 84 | let range: Range = 0 ..< maxIndex + 1 85 | display(range: range) 86 | } 87 | 88 | public override func viewDidLoad() { 89 | view.backgroundColor = backgroundColor() 90 | view.addSubview(stackView) 91 | } 92 | 93 | public override func viewDidLayoutSubviews() { 94 | display() 95 | } 96 | 97 | func backgroundColor() -> UIColor { 98 | return UIColor(red: 65 / 255, green: 153 / 255, blue: 1, alpha: 1) 99 | } 100 | 101 | public required init?(coder aDecoder: NSCoder) { 102 | fatalError("init(coder:) has not been implemented") 103 | } 104 | 105 | public init(string1: String, string2: String) { 106 | super.init(nibName: nil, bundle: nil) 107 | diffStrings = (string1, string2) 108 | } 109 | } 110 | 111 | extension Trace { 112 | func arrow(on graph: Graph) -> Arrow { 113 | let from = graph.coordinates(at: self.from) 114 | let to = graph.coordinates(at: self.to) 115 | 116 | let translatedCoordinates: (from: CGPoint, to: CGPoint) = { 117 | let yDelta = (to.y - from.y) / 20 118 | let xDelta = (to.x - from.x) / 20 119 | 120 | switch type() { 121 | case .deletion: 122 | return (CGPoint(x: from.x + xDelta, y: from.y), CGPoint(x: to.x - xDelta, y: to.y)) 123 | case .insertion: 124 | return (CGPoint(x: from.x, y: from.y + yDelta), CGPoint(x: to.x, y: to.y - yDelta)) 125 | case .matchPoint: 126 | return (CGPoint(x: from.x + xDelta, y: from.y + yDelta), CGPoint(x: to.x - xDelta, y: to.y - yDelta)) 127 | } 128 | }() 129 | return Arrow(from: translatedCoordinates.from, to: translatedCoordinates.to, tailWidth: 6, headWidth: 12, headLength: 10) 130 | } 131 | 132 | func shapeLayer(on graph: Graph) -> CAShapeLayer { 133 | let arrowLayer = UIBezierPath(arrow: arrow(on: graph)).shapeLayer() 134 | 135 | switch type() { 136 | case .deletion: 137 | arrowLayer.fillColor = UIColor.red.cgColor 138 | case .insertion: 139 | arrowLayer.fillColor = UIColor.green.cgColor 140 | case .matchPoint: 141 | arrowLayer.fillColor = UIColor.white.cgColor 142 | } 143 | return arrowLayer 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/Differ/NestedDiff.swift: -------------------------------------------------------------------------------- 1 | public struct NestedDiff: DiffProtocol { 2 | 3 | public typealias Index = Int 4 | 5 | public enum Element { 6 | case deleteSection(Int) 7 | case insertSection(Int) 8 | case deleteElement(Int, section: Int) 9 | case insertElement(Int, section: Int) 10 | } 11 | 12 | /// Returns the position immediately after the given index. 13 | /// 14 | /// - Parameters: 15 | /// - i: A valid index of the collection. `i` must be less than `endIndex`. 16 | /// - Returns: The index value immediately after `i`. 17 | public func index(after i: Int) -> Int { 18 | return i + 1 19 | } 20 | 21 | /// An array of particular diff operations 22 | public var elements: [Element] 23 | 24 | /// Initializes a new `NestedDiff` from a given array of diff operations. 25 | /// 26 | /// - Parameters: 27 | /// - elements: an array of particular diff operations 28 | public init(elements: [Element]) { 29 | self.elements = elements 30 | } 31 | } 32 | 33 | public extension Collection 34 | where Element: Collection { 35 | 36 | /// Creates a diff between the callee and `other` collection. It diffs elements two levels deep (therefore "nested") 37 | /// 38 | /// - Parameters: 39 | /// - other: a collection to compare the calee to 40 | /// - Returns: a `NestedDiff` between the calee and `other` collection 41 | func nestedDiff( 42 | to: Self, 43 | isEqualSection: EqualityChecker, 44 | isEqualElement: NestedElementEqualityChecker 45 | ) -> NestedDiff { 46 | let diffTraces = outputDiffPathTraces(to: to, isEqual: isEqualSection) 47 | 48 | // Diff sections 49 | let sectionDiff = Diff(traces: diffTraces).map { element -> NestedDiff.Element in 50 | switch element { 51 | case let .delete(at): 52 | return .deleteSection(at) 53 | case let .insert(at): 54 | return .insertSection(at) 55 | } 56 | } 57 | 58 | // Diff matching sections (moves, deletions, insertions) 59 | let filterMatchPoints = { (trace: Trace) -> Bool in 60 | if case .matchPoint = trace.type() { 61 | return true 62 | } 63 | return false 64 | } 65 | 66 | // offset & section 67 | 68 | let matchingSectionTraces = diffTraces 69 | .filter(filterMatchPoints) 70 | 71 | let fromSections = matchingSectionTraces.map { 72 | itemOnStartIndex(advancedBy: $0.from.x) 73 | } 74 | 75 | let toSections = matchingSectionTraces.map { 76 | to.itemOnStartIndex(advancedBy: $0.from.y) 77 | } 78 | 79 | let elementDiff = zip(zip(fromSections, toSections), matchingSectionTraces) 80 | .flatMap { (args) -> [NestedDiff.Element] in 81 | let (sections, trace) = args 82 | return sections.0.diff(sections.1, isEqual: isEqualElement).map { diffElement -> NestedDiff.Element in 83 | switch diffElement { 84 | case let .delete(at): 85 | return .deleteElement(at, section: trace.from.x) 86 | case let .insert(at): 87 | return .insertElement(at, section: trace.from.y) 88 | } 89 | } 90 | } 91 | 92 | return NestedDiff(elements: sectionDiff + elementDiff) 93 | } 94 | } 95 | 96 | public extension Collection 97 | where Element: Collection, Element.Element: Equatable { 98 | 99 | /// - SeeAlso: `nestedDiff(to:isEqualSection:isEqualElement:)` 100 | func nestedDiff( 101 | to: Self, 102 | isEqualSection: EqualityChecker 103 | ) -> NestedDiff { 104 | return nestedDiff( 105 | to: to, 106 | isEqualSection: isEqualSection, 107 | isEqualElement: { $0 == $1 } 108 | ) 109 | } 110 | } 111 | 112 | public extension Collection 113 | where Element: Collection, Element: Equatable { 114 | 115 | /// - SeeAlso: `nestedDiff(to:isEqualSection:isEqualElement:)` 116 | func nestedDiff( 117 | to: Self, 118 | isEqualElement: NestedElementEqualityChecker 119 | ) -> NestedDiff { 120 | return nestedDiff( 121 | to: to, 122 | isEqualSection: { $0 == $1 }, 123 | isEqualElement: isEqualElement 124 | ) 125 | } 126 | } 127 | 128 | public extension Collection 129 | where Element: Collection, Element: Equatable, 130 | Element.Element: Equatable { 131 | 132 | /// - SeeAlso: `nestedDiff(to:isEqualSection:isEqualElement:)` 133 | func nestedDiff(to: Self) -> NestedDiff { 134 | return nestedDiff( 135 | to: to, 136 | isEqualSection: { $0 == $1 }, 137 | isEqualElement: { $0 == $1 } 138 | ) 139 | } 140 | } 141 | 142 | extension NestedDiff.Element: CustomDebugStringConvertible { 143 | public var debugDescription: String { 144 | switch self { 145 | case let .deleteElement(row, section): 146 | return "DE(\(row),\(section))" 147 | case let .deleteSection(section): 148 | return "DS(\(section))" 149 | case let .insertElement(row, section): 150 | return "IE(\(row),\(section))" 151 | case let .insertSection(section): 152 | return "IS(\(section))" 153 | } 154 | } 155 | } 156 | 157 | extension NestedDiff: ExpressibleByArrayLiteral { 158 | 159 | public init(arrayLiteral elements: Element...) { 160 | self.elements = elements 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Tests/DifferTests/NestedExtendedDiffTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Differ 3 | 4 | class NestedExtendedDiffTests: XCTestCase { 5 | 6 | func testDiffOutputs() { 7 | let expectations = [ 8 | ( 9 | [ 10 | KeyedIntArray(elements: [], key: 1), 11 | KeyedIntArray(elements: [], key: 0) 12 | ], 13 | [ 14 | KeyedIntArray(elements: [], key: 0), 15 | KeyedIntArray(elements: [], key: 1) 16 | ], 17 | "MS(0,1)" 18 | ), 19 | ( 20 | [ 21 | KeyedIntArray(elements: [1], key: 1), 22 | KeyedIntArray(elements: [1, 2], key: 0) 23 | ], 24 | [ 25 | KeyedIntArray(elements: [1, 2], key: 0), 26 | KeyedIntArray(elements: [1, 2], key: 1) 27 | ], 28 | "MS(0,1)IE(1,1)" 29 | ), 30 | ( 31 | [ 32 | KeyedIntArray(elements: [1, 2], key: 1), 33 | KeyedIntArray(elements: [1, 2], key: 0) 34 | ], 35 | [ 36 | KeyedIntArray(elements: [1, 2], key: 0), 37 | KeyedIntArray(elements: [1], key: 1) 38 | ], 39 | "MS(0,1)DE(1,0)" 40 | ), 41 | ( 42 | [ 43 | KeyedIntArray(elements: [1], key: 1), 44 | KeyedIntArray(elements: [2, 1], key: 0) 45 | ], 46 | [ 47 | KeyedIntArray(elements: [1, 2], key: 0), 48 | KeyedIntArray(elements: [1], key: 1) 49 | ], 50 | "MS(0,1)ME((0,1),(1,0))" 51 | ), 52 | ( 53 | [ 54 | KeyedIntArray(elements: [1], key: 1), 55 | KeyedIntArray(elements: [2, 1], key: 0) 56 | ], 57 | [ 58 | KeyedIntArray(elements: [2, 1], key: 0) 59 | ], 60 | "DS(0)" 61 | ), 62 | ( 63 | [ 64 | KeyedIntArray(elements: [1], key: 1) 65 | ], 66 | [ 67 | KeyedIntArray(elements: [1], key: 0), 68 | KeyedIntArray(elements: [1], key: 1) 69 | ], 70 | "IS(0)" 71 | ), 72 | ( 73 | [ 74 | KeyedIntArray( 75 | elements: [ 76 | 0, 77 | 1 78 | ], 79 | key: 0 80 | ), 81 | KeyedIntArray( 82 | elements: [ 83 | 2, 84 | 3 85 | ], 86 | key: 1 87 | ) 88 | ], 89 | [ 90 | KeyedIntArray( 91 | elements: [ 92 | 3, 93 | 2 94 | ], 95 | key: 1 96 | ), 97 | KeyedIntArray( 98 | elements: [ 99 | 0, 100 | 1 101 | ], 102 | key: 0 103 | ), 104 | KeyedIntArray( 105 | elements: [ 106 | 12 107 | ], 108 | key: 2 109 | ) 110 | ], 111 | "MS(0,1)IS(2)ME((0,1),(1,0))" 112 | ), 113 | ( 114 | [ 115 | KeyedIntArray( 116 | elements: [ 117 | 3, 118 | 2 119 | ], 120 | key: 1 121 | ), 122 | KeyedIntArray( 123 | elements: [ 124 | 0, 125 | 1 126 | ], 127 | key: 0 128 | ), 129 | KeyedIntArray( 130 | elements: [ 131 | 12 132 | ], 133 | key: 2 134 | ) 135 | ], 136 | [ 137 | KeyedIntArray( 138 | elements: [ 139 | 0, 140 | 1 141 | ], 142 | key: 0 143 | ), 144 | KeyedIntArray( 145 | elements: [ 146 | 2, 147 | 3 148 | ], 149 | key: 1 150 | ) 151 | ], 152 | "MS(0,1)DS(2)ME((0,0),(1,1))" 153 | ) 154 | ] 155 | 156 | for expectation in expectations { 157 | XCTAssertEqual(_test(from: expectation.0, to: expectation.1), expectation.2) 158 | } 159 | } 160 | 161 | func _test(from: T, to: T) -> String 162 | where T.Element: Collection, 163 | T.Element: Equatable, 164 | T.Element.Element: Equatable { 165 | return from 166 | .nestedExtendedDiff(to: to) 167 | .reduce("") { $0 + $1.debugDescription } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Tests/DifferTests/NestedDiffTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Differ 3 | 4 | struct KeyedIntArray: Equatable { 5 | let elements: [Int] 6 | let key: Int 7 | 8 | public static func ==(fst: KeyedIntArray, snd: KeyedIntArray) -> Bool { 9 | return fst.key == snd.key 10 | } 11 | } 12 | 13 | extension KeyedIntArray: Collection { 14 | public func index(after i: Int) -> Int { 15 | return i + 1 16 | } 17 | 18 | public typealias IndexType = Array.Index 19 | 20 | public var startIndex: IndexType { 21 | return elements.startIndex 22 | } 23 | 24 | public var endIndex: IndexType { 25 | return elements.endIndex 26 | } 27 | 28 | public subscript(i: IndexType) -> Int { 29 | return elements[i] 30 | } 31 | } 32 | 33 | class NestedDiffTests: XCTestCase { 34 | 35 | func testDiffOutputs() { 36 | 37 | let keyedExpectations = [ 38 | ( 39 | [], 40 | [ 41 | KeyedIntArray(elements: [1, 2], key: 0), 42 | KeyedIntArray(elements: [1], key: 1) 43 | ], 44 | "IS(0)IS(1)" 45 | ), 46 | ( 47 | [ 48 | KeyedIntArray(elements: [2], key: 0), 49 | KeyedIntArray(elements: [1], key: 1) 50 | ], 51 | [ 52 | KeyedIntArray(elements: [1], key: 0), 53 | KeyedIntArray(elements: [], key: 1) 54 | ], 55 | "DE(0,0)IE(0,0)DE(0,1)" 56 | ), 57 | ( 58 | [ 59 | KeyedIntArray(elements: [2], key: 0), 60 | KeyedIntArray(elements: [0], key: 5), 61 | KeyedIntArray(elements: [1], key: 1) 62 | ], 63 | [ 64 | KeyedIntArray(elements: [1], key: 0), 65 | KeyedIntArray(elements: [], key: 1) 66 | ], 67 | "DS(1)DE(0,0)IE(0,0)DE(0,2)" 68 | ), 69 | ( 70 | [ 71 | KeyedIntArray(elements: [2], key: 0), 72 | KeyedIntArray(elements: [0], key: 5), 73 | KeyedIntArray(elements: [1], key: 1) 74 | ], 75 | [ 76 | KeyedIntArray(elements: [1], key: 0), 77 | KeyedIntArray(elements: [], key: 1) 78 | ], 79 | "DS(1)DE(0,0)IE(0,0)DE(0,2)" 80 | ), 81 | ( 82 | [ 83 | KeyedIntArray(elements: [2], key: 0), 84 | KeyedIntArray(elements: [1, 2, 3], key: -1), 85 | KeyedIntArray(elements: [1], key: 1) 86 | ], 87 | [ 88 | KeyedIntArray(elements: [2, 3], key: 0), 89 | KeyedIntArray(elements: [1, 2], key: 1) 90 | ], 91 | "DS(1)IE(1,0)IE(1,1)" 92 | ), 93 | ( 94 | [ 95 | KeyedIntArray(elements: [2], key: 0), 96 | KeyedIntArray(elements: [1], key: 1) 97 | ], 98 | [ 99 | KeyedIntArray(elements: [2, 1], key: 0), 100 | KeyedIntArray(elements: [], key: 1) 101 | ], 102 | "IE(1,0)DE(0,1)" 103 | ), 104 | ( 105 | [ 106 | KeyedIntArray(elements: [], key: 0), 107 | KeyedIntArray(elements: [1, 2], key: 1) 108 | ], 109 | [ 110 | KeyedIntArray(elements: [2], key: 0), 111 | KeyedIntArray(elements: [], key: 1) 112 | ], 113 | "IE(0,0)DE(0,1)DE(1,1)" 114 | ), 115 | ( 116 | [ 117 | KeyedIntArray(elements: [1, 2], key: 0), 118 | KeyedIntArray(elements: [], key: 1) 119 | ], 120 | [ 121 | KeyedIntArray(elements: [], key: 0), 122 | KeyedIntArray(elements: [1], key: 1) 123 | ], 124 | "DE(0,0)DE(1,0)IE(0,1)" 125 | ), 126 | ( 127 | [ 128 | KeyedIntArray(elements: [], key: 0), 129 | KeyedIntArray(elements: [1], key: 1), 130 | KeyedIntArray(elements: [2], key: 2) 131 | ], 132 | [ 133 | KeyedIntArray(elements: [1, 2], key: 0), 134 | KeyedIntArray(elements: [], key: 1), 135 | KeyedIntArray(elements: [], key: 2) 136 | ], 137 | "IE(0,0)IE(1,0)DE(0,1)DE(0,2)" 138 | ) 139 | ] 140 | 141 | let expectations: [([[Int]], [[Int]], String)] = [ 142 | ( 143 | [], 144 | [ 145 | [1, 2], 146 | [1] 147 | ], 148 | "IS(0)IS(1)" 149 | ), 150 | ( 151 | [ 152 | [1, 2], 153 | [] 154 | ], 155 | [], 156 | "DS(0)DS(1)" 157 | ), 158 | ( 159 | [[1, 2], [], [1]], 160 | [[1, 2], [], [1]], 161 | "" 162 | ), 163 | ( 164 | [[1, 2], [1, 4]], 165 | [[5, 2], [10, 4, 8]], 166 | "DS(0)DS(1)IS(0)IS(1)" 167 | ), 168 | ( 169 | [[1]], 170 | [[], [1, 2]], 171 | "DS(0)IS(0)IS(1)" 172 | ), 173 | ( 174 | [[1]], 175 | [[], [2]], 176 | "DS(0)IS(0)IS(1)" 177 | ) 178 | ] 179 | 180 | for expectation in expectations { 181 | XCTAssertEqual( 182 | _test(from: expectation.0, to: expectation.1), 183 | expectation.2) 184 | } 185 | 186 | for expectation in keyedExpectations { 187 | XCTAssertEqual( 188 | _test(from: expectation.0, to: expectation.1), 189 | expectation.2) 190 | } 191 | } 192 | 193 | func _test( 194 | from: [T], 195 | to: [T]) -> String 196 | where 197 | T: Equatable, 198 | T.Element: Equatable { 199 | return from 200 | .nestedDiff(to: to) 201 | .reduce("") { $0 + $1.debugDescription } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Sources/Differ/ExtendedPatch.swift: -------------------------------------------------------------------------------- 1 | enum BoxedDiffAndPatchElement { 2 | case move( 3 | diffElement: ExtendedDiff.Element, 4 | deletion: SortedPatchElement, 5 | insertion: SortedPatchElement 6 | ) 7 | case single( 8 | diffElement: ExtendedDiff.Element, 9 | patchElement: SortedPatchElement 10 | ) 11 | 12 | var diffElement: ExtendedDiff.Element { 13 | switch self { 14 | case let .move(de, _, _): 15 | return de 16 | case let .single(de, _): 17 | return de 18 | } 19 | } 20 | } 21 | 22 | /// Single step in a patch sequence. 23 | public enum ExtendedPatch { 24 | /// A single patch step containing the origin and target of a move 25 | case insertion(index: Int, element: Element) 26 | /// A single patch step containing a deletion index 27 | case deletion(index: Int) 28 | /// A single patch step containing the origin and target of a move 29 | case move(from: Int, to: Int) 30 | } 31 | 32 | /// Generates a patch sequence. It is a list of steps to be applied to obtain the `to` collection from the `from` one. The sorting function lets you sort the output e.g. you might want the output patch to have insertions first. 33 | /// 34 | /// - Complexity: O((N+M)*D) 35 | /// 36 | /// - Parameters: 37 | /// - from: The source collection 38 | /// - to: The target collection 39 | /// - sort: A sorting function 40 | /// - Returns: Arbitrarly sorted sequence of steps to obtain `to` collection from the `from` one. 41 | public func extendedPatch( 42 | from: T, 43 | to: T, 44 | sort: ExtendedDiff.OrderedBefore? = nil 45 | ) -> [ExtendedPatch] where T.Element: Equatable { 46 | return from.extendedDiff(to).patch(from: from, to: to, sort: sort) 47 | } 48 | 49 | extension ExtendedDiff { 50 | public typealias OrderedBefore = (_ fst: ExtendedDiff.Element, _ snd: ExtendedDiff.Element) -> Bool 51 | 52 | /// Generates a patch sequence based on the callee. It is a list of steps to be applied to obtain the `to` collection from the `from` one. The sorting function lets you sort the output e.g. you might want the output patch to have insertions first. 53 | /// 54 | /// - Complexity: O(D^2) 55 | /// 56 | /// - Parameters: 57 | /// - from: The source collection (usually the source collecetion of the callee) 58 | /// - to: The target collection (usually the target collecetion of the callee) 59 | /// - sort: A sorting function 60 | /// - Returns: Arbitrarly sorted sequence of steps to obtain `to` collection from the `from` one. 61 | public func patch( 62 | from: T, 63 | to: T, 64 | sort: OrderedBefore? = nil 65 | ) -> [ExtendedPatch] { 66 | 67 | let result: [SortedPatchElement] 68 | if let sort = sort { 69 | result = shiftedPatchElements(from: generateSortedPatchElements(from: from, to: to, sort: sort)) 70 | } else { 71 | result = shiftedPatchElements(from: generateSortedPatchElements(from: from, to: to)) 72 | } 73 | 74 | return result.indices.compactMap { i -> ExtendedPatch? in 75 | let patchElement = result[i] 76 | if moveIndices.contains(patchElement.sourceIndex) { 77 | let to = result[i + 1].value 78 | switch patchElement.value { 79 | case let .deletion(index): 80 | if case let .insertion(toIndex, _) = to { 81 | return .move(from: index, to: toIndex) 82 | } else { 83 | fatalError() 84 | } 85 | case let .insertion(index, _): 86 | if case let .deletion(fromIndex) = to { 87 | return .move(from: fromIndex, to: index) 88 | } else { 89 | fatalError() 90 | } 91 | } 92 | } else if !(i > 0 && moveIndices.contains(result[i - 1].sourceIndex)) { 93 | switch patchElement.value { 94 | case let .deletion(index): 95 | return .deletion(index: index) 96 | case let .insertion(index, element): 97 | return .insertion(index: index, element: element) 98 | } 99 | } 100 | return nil 101 | } 102 | } 103 | 104 | func generateSortedPatchElements( 105 | from: T, 106 | to: T, 107 | sort: @escaping OrderedBefore 108 | ) -> [SortedPatchElement] { 109 | let unboxed = boxDiffAndPatchElements( 110 | from: from, 111 | to: to 112 | ).sorted { from, to -> Bool in 113 | return sort(from.diffElement, to.diffElement) 114 | }.flatMap(unbox) 115 | 116 | return unboxed.indices.map { index -> SortedPatchElement in 117 | let old = unboxed[index] 118 | return SortedPatchElement( 119 | value: old.value, 120 | sourceIndex: old.sourceIndex, 121 | sortedIndex: index) 122 | }.sorted { (fst, snd) -> Bool in 123 | fst.sourceIndex < snd.sourceIndex 124 | } 125 | } 126 | 127 | func generateSortedPatchElements(from: T, to: T) -> [SortedPatchElement] { 128 | let patch = source.patch(to: to) 129 | return patch.indices.map { 130 | SortedPatchElement( 131 | value: patch[$0], 132 | sourceIndex: $0, 133 | sortedIndex: reorderedIndex[$0] 134 | ) 135 | } 136 | } 137 | 138 | func boxDiffAndPatchElements( 139 | from: T, 140 | to: T 141 | ) -> [BoxedDiffAndPatchElement] { 142 | let sourcePatch = generateSortedPatchElements(from: from, to: to) 143 | var indexDiff = 0 144 | return elements.indices.map { i in 145 | let diffElement = elements[i] 146 | switch diffElement { 147 | case .move: 148 | indexDiff += 1 149 | return .move( 150 | diffElement: diffElement, 151 | deletion: sourcePatch[sourceIndex[i + indexDiff - 1]], 152 | insertion: sourcePatch[sourceIndex[i + indexDiff]] 153 | ) 154 | default: 155 | return .single( 156 | diffElement: diffElement, 157 | patchElement: sourcePatch[sourceIndex[i + indexDiff]] 158 | ) 159 | } 160 | } 161 | } 162 | } 163 | 164 | func unbox(_ element: BoxedDiffAndPatchElement) -> [SortedPatchElement] { 165 | switch element { 166 | case let .move(_, deletion, insertion): 167 | return [deletion, insertion] 168 | case let .single(_, patchElement): 169 | return [patchElement] 170 | } 171 | } 172 | 173 | extension ExtendedPatch: CustomDebugStringConvertible { 174 | public var debugDescription: String { 175 | switch self { 176 | case let .deletion(at): 177 | return "D(\(at))" 178 | case let .insertion(at, element): 179 | return "I(\(at),\(element))" 180 | case let .move(from, to): 181 | return "M(\(from),\(to))" 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Sources/Differ/NestedExtendedDiff.swift: -------------------------------------------------------------------------------- 1 | public struct NestedExtendedDiff: DiffProtocol { 2 | 3 | public typealias Index = Int 4 | 5 | public enum Element { 6 | case deleteSection(Int) 7 | case insertSection(Int) 8 | case moveSection(from: Int, to: Int) 9 | case deleteElement(Int, section: Int) 10 | case insertElement(Int, section: Int) 11 | case moveElement(from: (item: Int, section: Int), to: (item: Int, section: Int)) 12 | } 13 | 14 | /// Returns the position immediately after the given index. 15 | /// 16 | /// - Parameters: 17 | /// - i: A valid index of the collection. `i` must be less than `endIndex`. 18 | /// - Returns: The index value immediately after `i`. 19 | public func index(after i: Int) -> Int { 20 | return i + 1 21 | } 22 | 23 | /// An array of particular diff operations 24 | public var elements: [Element] 25 | 26 | /// Initializes a new ``NestedExtendedDiff`` from a given array of diff operations. 27 | /// 28 | /// - Parameters: 29 | /// - elements: an array of particular diff operations 30 | public init(elements: [Element]) { 31 | self.elements = elements 32 | } 33 | } 34 | 35 | public typealias NestedElementEqualityChecker = (T.Element.Element, T.Element.Element) -> Bool where T.Element: Collection 36 | 37 | public extension Collection 38 | where Element: Collection { 39 | 40 | /// Creates a diff between the callee and `other` collection. It diffs elements two levels deep (therefore "nested") 41 | /// 42 | /// - Parameters: 43 | /// - other: a collection to compare the calee to 44 | /// - Returns: a ``NestedDiff`` between the calee and `other` collection 45 | func nestedExtendedDiff( 46 | to: Self, 47 | isEqualSection: EqualityChecker, 48 | isEqualElement: NestedElementEqualityChecker 49 | ) -> NestedExtendedDiff { 50 | 51 | // FIXME: This implementation is a copy paste of NestedDiff with some adjustments. 52 | 53 | let diffTraces = outputDiffPathTraces(to: to, isEqual: isEqualSection) 54 | 55 | let sectionDiff = 56 | extendedDiff( 57 | from: Diff(traces: diffTraces), 58 | other: to, 59 | isEqual: isEqualSection 60 | ).map { element -> NestedExtendedDiff.Element in 61 | switch element { 62 | case let .delete(at): 63 | return .deleteSection(at) 64 | case let .insert(at): 65 | return .insertSection(at) 66 | case let .move(from, to): 67 | return .moveSection(from: from, to: to) 68 | } 69 | } 70 | 71 | // Diff matching sections (moves, deletions, insertions) 72 | let filterMatchPoints = { (trace: Trace) -> Bool in 73 | if case .matchPoint = trace.type() { 74 | return true 75 | } 76 | return false 77 | } 78 | 79 | let sectionMoves = 80 | sectionDiff.compactMap { diffElement -> (Int, Int)? in 81 | if case let .moveSection(from, to) = diffElement { 82 | return (from, to) 83 | } 84 | return nil 85 | }.flatMap { move -> [NestedExtendedDiff.Element] in 86 | return itemOnStartIndex(advancedBy: move.0).extendedDiff(to.itemOnStartIndex(advancedBy: move.1), isEqual: isEqualElement) 87 | .map { diffElement -> NestedExtendedDiff.Element in 88 | switch diffElement { 89 | case let .insert(at): 90 | return .insertElement(at, section: move.1) 91 | case let .delete(at): 92 | return .deleteElement(at, section: move.0) 93 | case let .move(from, to): 94 | return .moveElement(from: (from, move.0), to: (to, move.1)) 95 | } 96 | } 97 | } 98 | 99 | // offset & section 100 | 101 | let matchingSectionTraces = diffTraces 102 | .filter(filterMatchPoints) 103 | 104 | let fromSections = matchingSectionTraces.map { 105 | itemOnStartIndex(advancedBy: $0.from.x) 106 | } 107 | 108 | let toSections = matchingSectionTraces.map { 109 | to.itemOnStartIndex(advancedBy: $0.from.y) 110 | } 111 | 112 | let elementDiff = zip(zip(fromSections, toSections), matchingSectionTraces) 113 | .flatMap { (args) -> [NestedExtendedDiff.Element] in 114 | let (sections, trace) = args 115 | return sections.0.extendedDiff(sections.1, isEqual: isEqualElement).map { diffElement -> NestedExtendedDiff.Element in 116 | switch diffElement { 117 | case let .delete(at): 118 | return .deleteElement(at, section: trace.from.x) 119 | case let .insert(at): 120 | return .insertElement(at, section: trace.from.y) 121 | case let .move(from, to): 122 | return .moveElement(from: (from, trace.from.x), to: (to, trace.from.y)) 123 | } 124 | } 125 | } 126 | 127 | return NestedExtendedDiff(elements: sectionDiff + sectionMoves + elementDiff) 128 | } 129 | } 130 | 131 | public extension Collection 132 | where Element: Collection, 133 | Element.Element: Equatable { 134 | 135 | /// - SeeAlso: `nestedDiff(to:isEqualSection:isEqualElement:)` 136 | func nestedExtendedDiff( 137 | to: Self, 138 | isEqualSection: EqualityChecker 139 | ) -> NestedExtendedDiff { 140 | return nestedExtendedDiff( 141 | to: to, 142 | isEqualSection: isEqualSection, 143 | isEqualElement: { $0 == $1 } 144 | ) 145 | } 146 | } 147 | 148 | public extension Collection 149 | where Element: Collection, Element: Equatable { 150 | 151 | /// - SeeAlso: `nestedDiff(to:isEqualSection:isEqualElement:)` 152 | func nestedExtendedDiff( 153 | to: Self, 154 | isEqualElement: NestedElementEqualityChecker 155 | ) -> NestedExtendedDiff { 156 | return nestedExtendedDiff( 157 | to: to, 158 | isEqualSection: { $0 == $1 }, 159 | isEqualElement: isEqualElement 160 | ) 161 | } 162 | } 163 | 164 | public extension Collection 165 | where Element: Collection, Element: Equatable, 166 | Element.Element: Equatable { 167 | 168 | /// - SeeAlso: `nestedDiff(to:isEqualSection:isEqualElement:)` 169 | func nestedExtendedDiff(to: Self) -> NestedExtendedDiff { 170 | return nestedExtendedDiff( 171 | to: to, 172 | isEqualSection: { $0 == $1 }, 173 | isEqualElement: { $0 == $1 } 174 | ) 175 | } 176 | } 177 | 178 | extension NestedExtendedDiff.Element: CustomDebugStringConvertible { 179 | public var debugDescription: String { 180 | switch self { 181 | case let .deleteElement(row, section): 182 | return "DE(\(row),\(section))" 183 | case let .deleteSection(section): 184 | return "DS(\(section))" 185 | case let .insertElement(row, section): 186 | return "IE(\(row),\(section))" 187 | case let .insertSection(section): 188 | return "IS(\(section))" 189 | case let .moveElement(from, to): 190 | return "ME((\(from.item),\(from.section)),(\(to.item),\(to.section)))" 191 | case let .moveSection(from, to): 192 | return "MS(\(from),\(to))" 193 | } 194 | } 195 | } 196 | 197 | extension NestedExtendedDiff: ExpressibleByArrayLiteral { 198 | 199 | public init(arrayLiteral elements: NestedExtendedDiff.Element...) { 200 | self.elements = elements 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/Differ/ExtendedDiff.swift: -------------------------------------------------------------------------------- 1 | /// A sequence of deletions, insertions, and moves where deletions point to locations in the source and insertions point to locations in the output. 2 | /// Examples: 3 | /// ``` 4 | /// "12" -> "": D(0)D(1) 5 | /// "" -> "12": I(0)I(1) 6 | /// ``` 7 | /// - SeeAlso: Diff 8 | public struct ExtendedDiff: DiffProtocol { 9 | 10 | public typealias Index = Int 11 | 12 | public enum Element { 13 | case insert(at: Int) 14 | case delete(at: Int) 15 | case move(from: Int, to: Int) 16 | } 17 | 18 | /// Returns the position immediately after the given index. 19 | /// 20 | /// - Parameters: 21 | /// - i: A valid index of the collection. `i` must be less than 22 | /// `endIndex`. 23 | /// - Returns: The index value immediately after `i`. 24 | public func index(after i: Int) -> Int { 25 | return i + 1 26 | } 27 | 28 | /// Diff used to compute an instance 29 | public let source: Diff 30 | /// An array which holds indices of diff elements in the source diff (i.e. diff without moves). 31 | let sourceIndex: [Int] 32 | /// An array which holds indices of diff elements in a diff where move's subelements (deletion and insertion) are ordered accordingly 33 | let reorderedIndex: [Int] 34 | 35 | /// An array of particular diff operations 36 | public let elements: [ExtendedDiff.Element] 37 | let moveIndices: Set 38 | } 39 | 40 | extension ExtendedDiff.Element { 41 | init(_ diffElement: Diff.Element) { 42 | switch diffElement { 43 | case let .delete(at): 44 | self = .delete(at: at) 45 | case let .insert(at): 46 | self = .insert(at: at) 47 | } 48 | } 49 | } 50 | 51 | public extension Collection { 52 | 53 | /// Creates an extended diff between the callee and `other` collection 54 | /// 55 | /// - Complexity: O((N+M)*D). There's additional cost of O(D^2) to compute the moves. 56 | /// 57 | /// - Parameters: 58 | /// - other: a collection to compare the callee to 59 | /// - isEqual: instance comparator closure 60 | /// - Returns: ExtendedDiff between the callee and `other` collection 61 | func extendedDiff(_ other: Self, isEqual: EqualityChecker) -> ExtendedDiff { 62 | return extendedDiff(from: diff(other, isEqual: isEqual), other: other, isEqual: isEqual) 63 | } 64 | 65 | /// Creates an extended diff between the callee and `other` collection 66 | /// 67 | /// - Complexity: O(D^2). where D is number of elements in diff. 68 | /// 69 | /// - Parameters: 70 | /// - diff: source diff 71 | /// - other: a collection to compare the callee to 72 | /// - isEqual: instance comparator closure 73 | /// - Returns: ExtendedDiff between the callee and `other` collection 74 | func extendedDiff(from diff: Diff, other: Self, isEqual: EqualityChecker) -> ExtendedDiff { 75 | 76 | var elements: [ExtendedDiff.Element] = [] 77 | var moveOriginIndices = Set() 78 | var moveTargetIndices = Set() 79 | // It maps indices after reordering (e.g. bringing move origin and target next to each other in the output) to their positions in the source Diff 80 | var sourceIndex = [Int]() 81 | 82 | // Complexity O(d^2) where d is the length of the diff 83 | 84 | /* 85 | * 1. Iterate all objects 86 | * 2. For every iteration find the next matching element 87 | a) if it's not found insert the element as is to the output array 88 | b) if it's found calculate move as in 3 89 | * 3. Calculating the move. 90 | We call the first element a *candidate* and the second element a *match* 91 | 1. The position of the candidate never changes 92 | 2. The position of the match is equal to its initial position + m where m is equal to -d + i where d = deletions between candidate and match and i = insertions between candidate and match 93 | * 4. Remove the candidate and match and insert the move in the place of the candidate 94 | * 95 | */ 96 | 97 | for candidateIndex in diff.indices { 98 | if !moveTargetIndices.contains(candidateIndex) && !moveOriginIndices.contains(candidateIndex) { 99 | let candidate = diff[candidateIndex] 100 | let match = firstMatch(diff, dirtyIndices: moveTargetIndices.union(moveOriginIndices), candidate: candidate, candidateIndex: candidateIndex, other: other, isEqual: isEqual) 101 | if let match = match { 102 | switch match.0 { 103 | case let .move(from, _): 104 | if from == candidate.at() { 105 | sourceIndex.append(candidateIndex) 106 | sourceIndex.append(match.1) 107 | moveOriginIndices.insert(candidateIndex) 108 | moveTargetIndices.insert(match.1) 109 | } else { 110 | sourceIndex.append(match.1) 111 | sourceIndex.append(candidateIndex) 112 | moveOriginIndices.insert(match.1) 113 | moveTargetIndices.insert(candidateIndex) 114 | } 115 | default: fatalError() 116 | } 117 | elements.append(match.0) 118 | } else { 119 | sourceIndex.append(candidateIndex) 120 | elements.append(ExtendedDiff.Element(candidate)) 121 | } 122 | } 123 | } 124 | 125 | let reorderedIndices = flip(array: sourceIndex) 126 | 127 | return ExtendedDiff( 128 | source: diff, 129 | sourceIndex: sourceIndex, 130 | reorderedIndex: reorderedIndices, 131 | elements: elements, 132 | moveIndices: moveOriginIndices 133 | ) 134 | } 135 | 136 | func firstMatch( 137 | _ diff: Diff, 138 | dirtyIndices: Set, 139 | candidate: Diff.Element, 140 | candidateIndex: Diff.Index, 141 | other: Self, 142 | isEqual: EqualityChecker 143 | ) -> (ExtendedDiff.Element, Diff.Index)? { 144 | for matchIndex in (candidateIndex + 1) ..< diff.endIndex { 145 | if !dirtyIndices.contains(matchIndex) { 146 | let match = diff[matchIndex] 147 | if let move = createMatch(candidate, match: match, other: other, isEqual: isEqual) { 148 | return (move, matchIndex) 149 | } 150 | } 151 | } 152 | return nil 153 | } 154 | 155 | func createMatch(_ candidate: Diff.Element, match: Diff.Element, other: Self, isEqual: EqualityChecker) -> ExtendedDiff.Element? { 156 | switch (candidate, match) { 157 | case (.delete, .insert): 158 | if isEqual(itemOnStartIndex(advancedBy: candidate.at()), other.itemOnStartIndex(advancedBy: match.at())) { 159 | return .move(from: candidate.at(), to: match.at()) 160 | } 161 | case (.insert, .delete): 162 | if isEqual(itemOnStartIndex(advancedBy: match.at()), other.itemOnStartIndex(advancedBy: candidate.at())) { 163 | return .move(from: match.at(), to: candidate.at()) 164 | } 165 | default: return nil 166 | } 167 | return nil 168 | } 169 | } 170 | 171 | public extension Collection where Element: Equatable { 172 | 173 | /// - SeeAlso: `extendedDiff(_:isEqual:)` 174 | func extendedDiff(_ other: Self) -> ExtendedDiff { 175 | return extendedDiff(other, isEqual: { $0 == $1 }) 176 | } 177 | } 178 | 179 | extension Collection { 180 | func itemOnStartIndex(advancedBy n: Int) -> Element { 181 | return self[self.index(startIndex, offsetBy: n)] 182 | } 183 | } 184 | 185 | func flip(array: [Int]) -> [Int] { 186 | return zip(array, array.indices) 187 | .sorted { $0.0 < $1.0 } 188 | .map { $0.1 } 189 | } 190 | 191 | extension ExtendedDiff.Element: CustomDebugStringConvertible { 192 | public var debugDescription: String { 193 | switch self { 194 | case let .delete(at): 195 | return "D(\(at))" 196 | case let .insert(at): 197 | return "I(\(at))" 198 | case let .move(from, to): 199 | return "M(\(from),\(to))" 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Differ 2 | 3 | [![Continuous Integration](https://github.com/tonyarnold/Differ/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/tonyarnold/Differ/actions/workflows/continuous-integration.yml) 4 | 5 | Differ generates the differences between `Collection` instances (this includes Strings!). 6 | 7 | It uses a [fast algorithm](http://www.xmailserver.org/diff2.pdf) `(O((N+M)*D))` to do this. 8 | 9 | ## Features 10 | 11 | - ⚡️ [It is fast](#performance-notes) 12 | - Differ supports three types of operations: 13 | - Insertions 14 | - Deletions 15 | - Moves (when using `ExtendedDiff`) 16 | - Arbitrary sorting of patches (`Patch`) 17 | - Utilities for updating `UITableView` and `UICollectionView` in UIKit, and `NSTableView` and `NSCollectionView` in AppKit 18 | - Calculating differences between collections containing collections (use `NestedDiff`) 19 | 20 | ## Why do I need it? 21 | 22 | There's a lot more to calculating diffs than performing table view animations easily! 23 | 24 | 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. 25 | 26 | 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. 27 | 28 | ## Diffs, patches and sorting 29 | 30 | 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: 31 | 32 | Change | Result 33 | :--------------------------------|:------------- 34 | Delete the item at index 0 | `""` 35 | Insert `b` at index 0 | `"b"` 36 | 37 | If we want to perform these operations in different order, simple reordering of the existing patches won't work: 38 | 39 | Change | Result 40 | :---------------------------------|:------- 41 | Insert `b` at index 0 | `"ba"` 42 | Delete the item at index 0 | `"a"` 43 | 44 | ...whoops! 45 | 46 | To get to the correct outcome, we need to shift the order of insertions and deletions so that we get this: 47 | 48 | Change | Result 49 | :---------------------------------|:------ 50 | Insert `b` at index 1 | `"ab"` 51 | Delete the item at index 0 | `"b"` 52 | 53 | ### Solution 54 | 55 | In order to mitigate this issue, there are two types of output: 56 | 57 | - *Diff* 58 | - 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`. 59 | - *Patch* 60 | - 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. 61 | 62 | ### Practical sorting 63 | 64 | In practice, this means that a diff to transform the string `1234` into `1` could be described as the following set of steps: 65 | 66 | DELETE 1 67 | DELETE 2 68 | DELETE 3 69 | 70 | The patch to describe the same change would be: 71 | 72 | DELETE 1 73 | DELETE 1 74 | DELETE 1 75 | 76 | However, if we decided to sort it so that deletions and higher indices are processed first, we get this patch: 77 | 78 | DELETE 3 79 | DELETE 2 80 | DELETE 1 81 | 82 | ## How to use 83 | 84 | ### Table and Collection Views 85 | 86 | 87 | The following will automatically animate deletions, insertions, and moves: 88 | 89 | ```swift 90 | tableView.animateRowChanges(oldData: old, newData: new) 91 | 92 | collectionView.animateItemChanges(oldData: old, newData: new, updateData: { self.dataSource = new }) 93 | ``` 94 | 95 | It can work with sections, too! 96 | ```swift 97 | tableView.animateRowAndSectionChanges(oldData: old, newData: new) 98 | 99 | collectionView.animateItemAndSectionChanges(oldData: old, newData: new, updateData: { self.dataSource = new }) 100 | ``` 101 | 102 | You can also calculate `diff` separately and use it later: 103 | ```swift 104 | // Generate the difference first 105 | let diff = dataSource.extendedDiff(newDataSource) 106 | 107 | // This will apply changes to dataSource. 108 | let dataSourceUpdate = { self.dataSource = newDataSource } 109 | 110 | // ... 111 | 112 | tableView.apply(diff) 113 | 114 | collectionView.apply(diff, updateData: dataSourceUpdate) 115 | ``` 116 | 117 | Please see the [included examples](/Examples/) for a working sample. 118 | 119 | #### Note about `updateData` 120 | 121 | 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](https://developer.apple.com/documentation/uikit/uicollectionview/1618045-performbatchupdates): 122 | 123 | > 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:)`. 124 | 125 | Thus, it is **recommended** to update your `dataSource` inside `updateData` closure to avoid potential crashes during animations. 126 | 127 | ### Using Patch and Diff 128 | 129 | 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: 130 | 131 | ```swift 132 | let from: T 133 | let to: T 134 | 135 | // patch() only includes insertions and deletions 136 | let patch: [Patch] = patch(from: from, to: to) 137 | 138 | // extendedPatch() includes insertions, deletions and moves 139 | let patch: [ExtendedPatch] = extendedPatch(from: from, to: to) 140 | ``` 141 | 142 | When you need additional control over ordering, you could use the following: 143 | 144 | ```swift 145 | let insertionsFirst = { element1, element2 -> Bool in 146 | switch (element1, element2) { 147 | case (.insert(let at1), .insert(let at2)): 148 | return at1 < at2 149 | case (.insert, .delete): 150 | return true 151 | case (.delete, .insert): 152 | return false 153 | case (.delete(let at1), .delete(let at2)): 154 | return at1 < at2 155 | default: fatalError() // Unreachable 156 | } 157 | } 158 | 159 | // Results in a list of patches with insertions preceding deletions 160 | let patch = patch(from: from, to: to, sort: insertionsFirst) 161 | ``` 162 | 163 | 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. 164 | 165 | `D` is the length of a diff: 166 | 167 | - Generating a sorted patch takes `O(D^2)` time. 168 | - The default order takes `O(D)` to generate. 169 | 170 | ```swift 171 | // Generate the difference first 172 | let diff = from.diff(to) 173 | 174 | // Now generate the list of patches utilising the diff we've just calculated 175 | let patch = diff.patch(from: from, to: to) 176 | ``` 177 | 178 | If you'd like to learn more about how this library works, `Graph.playground` is a great place to start. 179 | 180 | ## Performance notes 181 | 182 | 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. 183 | 184 | In the following benchmarks, you should see an order of magnitude difference in calculation time between the two algorithms. 185 | 186 | Each measurement is the mean time in seconds it takes to calculate a diff, over 10 runs on an iPhone 6. 187 | 188 | | | Diff | Dwifft | 189 | |---------|:----------|:--------| 190 | | same | 0.0213 | 52.3642 | 191 | | created | 0.0188 | 0.0033 | 192 | | deleted | 0.0184 | 0.0050 | 193 | | diff | 0.1320 | 63.4084 | 194 | 195 | You can run these benchmarks yourself by [checking out the Diff Performance Suite](https://github.com/tonyarnold/DiffPerformanceSuite). 196 | 197 | 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](http://par.cse.nsysu.edu.tw/~lcs/Hunt-Szymanski%20Algorithm.php) and/or [Hirschberg's algorithm](https://en.wikipedia.org/wiki/Hirschberg%27s_algorithm). 198 | 199 | ## Requirements 200 | 201 | Differ requires at least Swift 5.4 or Xcode 12.5 to compile. 202 | 203 | ## Installation 204 | 205 | You can add Differ to your project using Carthage, CocoaPods, Swift Package Manager, or as an Xcode subproject. 206 | 207 | ### Carthage 208 | 209 | ```ruby 210 | github "tonyarnold/Differ" 211 | ``` 212 | 213 | ### CocoaPods 214 | 215 | ```ruby 216 | pod 'Differ' 217 | ``` 218 | 219 | ## Acknowledgements 220 | 221 | Differ is a modified fork of [Wojtek Czekalski's](https://github.com/wokalski) [Diff.swift](https://github.com/wokalski/Diff.swift) - Wojtek deserves all the credit for the original implementation, I am merely its present custodian. 222 | 223 | Please, [file issues with this fork here in this repository](https://github.com/tonyarnold/Diff/issues/new), not in Wojtek's original repository. 224 | -------------------------------------------------------------------------------- /Tests/DifferTests/PatchSortTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Differ 3 | 4 | class PatchTests: XCTestCase { 5 | 6 | func testDefaultOrder() { 7 | 8 | let defaultOrder = [ 9 | ("kitten", "sitting", "D(0)I(0,s)D(4)I(4,i)I(6,g)"), 10 | ("🐩itt🍨ng", "kitten", "D(0)I(0,k)D(4)I(4,e)D(6)"), 11 | ("1234", "ABCD", "D(0)D(0)D(0)D(0)I(0,A)I(1,B)I(2,C)I(3,D)"), 12 | ("1234", "", "D(0)D(0)D(0)D(0)"), 13 | ("", "1234", "I(0,1)I(1,2)I(2,3)I(3,4)"), 14 | ("Hi", "Oh Hi", "I(0,O)I(1,h)I(2, )"), 15 | ("Hi", "Hi O", "I(2, )I(3,O)"), 16 | ("Hi O", "Hi", "D(2)D(2)"), 17 | ("Wojtek", "Wojciech", "D(3)I(3,c)I(4,i)D(6)I(6,c)I(7,h)"), 18 | ("1234", "1234", ""), 19 | ("", "", ""), 20 | ("Oh Hi", "Hi Oh", "D(0)D(0)D(0)I(2, )I(3,O)I(4,h)"), 21 | ("1362", "31526", "D(0)D(1)I(1,1)I(2,5)I(4,6)"), 22 | ("1234b2", "ab", "D(0)D(0)D(0)D(0)I(0,a)D(2)") 23 | ] 24 | 25 | for expectation in defaultOrder { 26 | XCTAssertEqual( 27 | _test(from: expectation.0, to: expectation.1), 28 | expectation.2) 29 | } 30 | } 31 | 32 | func testInsertionsFirst() { 33 | 34 | let insertionsFirst = [ 35 | ("kitten", "sitting", "I(1,s)I(6,i)I(8,g)D(0)D(4)"), 36 | ("🐩itt🍨ng", "kitten", "I(1,k)I(6,e)D(0)D(4)D(6)"), 37 | ("1234", "ABCD", "I(4,A)I(5,B)I(6,C)I(7,D)D(0)D(0)D(0)D(0)"), 38 | ("1234", "", "D(0)D(0)D(0)D(0)"), 39 | ("", "1234", "I(0,1)I(1,2)I(2,3)I(3,4)"), 40 | ("Hi", "Oh Hi", "I(0,O)I(1,h)I(2, )"), 41 | ("Hi", "Hi O", "I(2, )I(3,O)"), 42 | ("Hi O", "Hi", "D(2)D(2)"), 43 | ("Wojtek", "Wojciech", "I(4,c)I(5,i)I(8,c)I(9,h)D(3)D(6)"), 44 | ("1234", "1234", ""), 45 | ("", "", ""), 46 | ("Oh Hi", "Hi Oh", "I(5, )I(6,O)I(7,h)D(0)D(0)D(0)"), 47 | ("1362", "31526", "I(3,1)I(4,5)I(6,6)D(0)D(1)") 48 | ] 49 | 50 | let insertionsFirstSort = { (element1: Diff.Element, element2: Diff.Element) -> Bool in 51 | switch (element1, element2) { 52 | case let (.insert(at1), .insert(at2)): 53 | return at1 < at2 54 | case (.insert(_), .delete(_)): 55 | return true 56 | case (.delete(_), .insert(_)): 57 | return false 58 | case let (.delete(at1), .delete(at2)): 59 | return at1 < at2 60 | } 61 | } 62 | 63 | for expectation in insertionsFirst { 64 | XCTAssertEqual( 65 | _test( 66 | from: expectation.0, 67 | to: expectation.1, 68 | sortingFunction: insertionsFirstSort), 69 | expectation.2) 70 | } 71 | } 72 | 73 | func testDeletionsFirst() { 74 | 75 | let deletionsFirst = [ 76 | ("kitten", "sitting", "D(0)D(3)I(0,s)I(4,i)I(6,g)"), 77 | ("🐩itt🍨ng", "kitten", "D(0)D(3)D(4)I(0,k)I(4,e)"), 78 | ("1234", "ABCD", "D(0)D(0)D(0)D(0)I(0,A)I(1,B)I(2,C)I(3,D)"), 79 | ("1234", "", "D(0)D(0)D(0)D(0)"), 80 | ("", "1234", "I(0,1)I(1,2)I(2,3)I(3,4)"), 81 | ("Hi", "Oh Hi", "I(0,O)I(1,h)I(2, )"), 82 | ("Hi", "Hi O", "I(2, )I(3,O)"), 83 | ("Hi O", "Hi", "D(2)D(2)"), 84 | ("Wojtek", "Wojciech", "D(3)D(4)I(3,c)I(4,i)I(6,c)I(7,h)"), 85 | ("1234", "1234", ""), 86 | ("", "", ""), 87 | ("Oh Hi", "Hi Oh", "D(0)D(0)D(0)I(2, )I(3,O)I(4,h)"), 88 | ("1362", "31526", "D(0)D(1)I(1,1)I(2,5)I(4,6)") 89 | ] 90 | 91 | let deletionsFirstSort = { (element1: Diff.Element, element2: Diff.Element) -> Bool in 92 | switch (element1, element2) { 93 | case let (.insert(at1), .insert(at2)): 94 | return at1 < at2 95 | case (.insert(_), .delete(_)): 96 | return false 97 | case (.delete(_), .insert(_)): 98 | return true 99 | case let (.delete(at1), .delete(at2)): 100 | return at1 < at2 101 | } 102 | } 103 | 104 | for expectation in deletionsFirst { 105 | XCTAssertEqual( 106 | _test( 107 | from: expectation.0, 108 | to: expectation.1, 109 | sortingFunction: deletionsFirstSort), 110 | expectation.2) 111 | } 112 | } 113 | 114 | func testRandomStringPermutationRandomPatchSort() { 115 | 116 | let sort = { (_: Diff.Element, _: Diff.Element) -> Bool in 117 | arc4random_uniform(2) == 0 118 | } 119 | for _ in 0 ..< 200 { 120 | let randomString = randomAlphaNumericString(length: 30) 121 | let permutation = randomAlphaNumericString(length: 30) 122 | let patch = randomString.diff(permutation).patch(from: randomString, to: permutation, sort: sort) 123 | let result = randomString.apply(patch) 124 | XCTAssertEqual(result, permutation) 125 | } 126 | } 127 | 128 | // See https://github.com/tonyarnold/Differ/issues/63 129 | func testLargeCollectionOnBackgroundThread() { 130 | let FIRST_STRING = """ 131 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vel turpis nunc eget lorem dolor sed viverra ipsum. Pharetra pharetra massa massa ultricies mi quis hendrerit. Sit amet porttitor eget dolor morbi non arcu risus quis. Cursus risus at ultrices mi tempus imperdiet nulla malesuada. Odio ut enim blandit volutpat maecenas volutpat blandit aliquam. Eu ultrices vitae auctor eu augue ut. Urna condimentum mattis pellentesque id nibh. Vestibulum lorem sed risus ultricies tristique. Tempor orci eu lobortis elementum. Purus faucibus ornare suspendisse sed nisi lacus. Fames ac turpis egestas maecenas pharetra convallis posuere morbi leo. 132 | Neque volutpat ac tincidunt vitae semper quis lectus nulla. Eu turpis egestas pretium aenean pharetra magna ac placerat. Vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet. Mauris a diam maecenas sed enim ut sem. Pellentesque massa placerat duis ultricies lacus. Nullam non nisi est sit amet facilisis magna etiam. Eget mauris pharetra et ultrices neque ornare aenean euismod elementum. Ipsum dolor sit amet consectetur adipiscing elit duis tristique. Tellus rutrum tellus pellentesque eu tincidunt tortor. Id volutpat lacus laoreet non curabitur gravida arcu. Tellus at urna condimentum mattis pellentesque id nibh tortor id. Viverra maecenas accumsan lacus vel facilisis volutpat est velit. Ut ornare lectus sit amet est placerat in egestas. Vestibulum sed arcu non odio euismod lacinia. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Felis donec et odio pellentesque diam volutpat commodo sed. 133 | Diam ut venenatis tellus in metus. Ultrices tincidunt arcu non sodales. Id velit ut tortor pretium viverra suspendisse potenti. Amet commodo nulla facilisi nullam vehicula. Blandit massa enim nec dui nunc mattis enim ut. Massa tempor nec feugiat nisl. Sed odio morbi quis commodo odio aenean. Dui ut ornare lectus sit amet est placerat in egestas. Varius vel pharetra vel turpis nunc eget lorem dolor sed. In cursus turpis massa tincidunt dui ut ornare lectus sit. 134 | """ 135 | let SECOND_STRING = """ 136 | Proin sagittis nisl rhoncus mattis rhoncus urna neque viverra. Auctor eu augue ut lectus arcu. Condimentum lacinia quis vel eros donec. Aliquam purus sit amet luctus venenatis lectus magna fringilla urna. Tellus mauris a diam maecenas sed enim ut sem. Pharetra et ultrices neque ornare aenean. Pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada. Nunc faucibus a pellentesque sit amet porttitor eget. Neque convallis a cras semper auctor. Faucibus vitae aliquet nec ullamcorper. Lectus mauris ultrices eros in cursus turpis. Elit sed vulputate mi sit amet. Dolor morbi non arcu risus quis varius quam quisque. Et malesuada fames ac turpis. Libero id faucibus nisl tincidunt eget nullam non nisi est. Eget dolor morbi non arcu risus quis. Id porta nibh venenatis cras sed felis. Quis imperdiet massa tincidunt nunc pulvinar. Tincidunt dui ut ornare lectus sit amet est placerat. Ultrices tincidunt arcu non sodales neque sodales ut. 137 | Erat nam at lectus urna. Tellus at urna condimentum mattis. Aliquam vestibulum morbi blandit cursus risus at ultrices. Tristique senectus et netus et malesuada fames ac turpis. Arcu odio ut sem nulla pharetra diam sit amet nisl. Lorem sed risus ultricies tristique nulla aliquet. Ac turpis egestas maecenas pharetra convallis posuere morbi. Dolor sed viverra ipsum nunc. Nunc mattis enim ut tellus elementum sagittis vitae. Congue mauris rhoncus aenean vel elit scelerisque mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra vitae. Amet luctus venenatis lectus magna fringilla urna porttitor rhoncus. Suspendisse sed nisi lacus sed. 138 | Non quam lacus suspendisse faucibus. Urna porttitor rhoncus dolor purus non enim praesent. Ultrices sagittis orci a scelerisque purus semper. Ultricies lacus sed turpis tincidunt. Pharetra vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae purus. Sit amet massa vitae tortor. Laoreet non curabitur gravida arcu ac tortor dignissim convallis aenean. Eu tincidunt tortor aliquam nulla facilisi cras fermentum. Cras ornare arcu dui vivamus arcu felis bibendum ut. Convallis tellus id interdum velit laoreet id. Ac turpis egestas sed tempus urna et. Facilisis sed odio morbi quis commodo odio aenean. Felis eget nunc lobortis mattis. Neque gravida in fermentum et sollicitudin ac orci. Id diam maecenas ultricies mi eget. Sit amet aliquam id diam maecenas. Blandit libero volutpat sed cras ornare arcu dui vivamus arcu. Mauris ultrices eros in cursus turpis massa tincidunt dui. 139 | """ 140 | let source = Array(FIRST_STRING).map(String.init) 141 | let target = Array(SECOND_STRING).map(String.init) 142 | let predicate: (Diff.Element, Diff.Element) -> Bool = { _, _ in false } 143 | 144 | let expectation = self.expectation(description: "Patch") 145 | 146 | DispatchQueue.global().async(execute: { 147 | let patch = Differ.patch(from: source, to: target, sort: predicate) 148 | XCTAssertEqual(source.apply(patch), target) 149 | expectation.fulfill() 150 | }) 151 | 152 | waitForExpectations(timeout: 30, handler: nil) 153 | } 154 | } 155 | 156 | func randomAlphaNumericString(length: Int) -> String { 157 | 158 | let allowedChars = "abcdefghijklmnopqrstu" 159 | let allowedCharsCount = UInt32(allowedChars.count) 160 | var randomString = "" 161 | 162 | for _ in 0 ..< length { 163 | let randomNum = Int(arc4random_uniform(allowedCharsCount)) 164 | let randomIndex = allowedChars.index(allowedChars.startIndex, offsetBy: randomNum) 165 | let newCharacter = allowedChars[randomIndex] 166 | randomString += String(newCharacter) 167 | } 168 | 169 | return randomString 170 | } 171 | 172 | typealias SortingFunction = (Diff.Element, Diff.Element) -> Bool 173 | 174 | func _test( 175 | from: String, 176 | to: String, 177 | sortingFunction: SortingFunction? = nil) -> String { 178 | if let sort = sortingFunction { 179 | return patch( 180 | from: from, 181 | to: to, 182 | sort: sort) 183 | .reduce("") { $0 + $1.debugDescription } 184 | } 185 | return patch( 186 | from: from, 187 | to: to) 188 | .reduce("") { $0 + $1.debugDescription } 189 | } 190 | -------------------------------------------------------------------------------- /Sources/Differ/Diff.swift: -------------------------------------------------------------------------------- 1 | public protocol DiffProtocol: Collection { 2 | 3 | associatedtype DiffElementType 4 | 5 | var elements: [DiffElementType] { get } 6 | } 7 | 8 | /// A sequence of deletions and insertions where deletions point to locations in the source and insertions point to locations in the output. 9 | /// 10 | /// For example: 11 | /// ``` 12 | /// "12" -> "": D(0)D(1) 13 | /// "" -> "12": I(0)I(1) 14 | /// ``` 15 | public struct Diff: DiffProtocol { 16 | 17 | public enum Element { 18 | case insert(at: Int) 19 | case delete(at: Int) 20 | } 21 | 22 | /// Returns the position immediately after the given index. 23 | /// 24 | /// - Parameters: 25 | /// - i: A valid index of the collection. `i` must be less than `endIndex`. 26 | /// - Returns: The index value immediately after `i`. 27 | public func index(after i: Int) -> Int { 28 | return i + 1 29 | } 30 | 31 | /// An array of particular diff operations 32 | public var elements: [Diff.Element] 33 | 34 | /// Initializes a new `Diff` from a given array of diff operations. 35 | /// 36 | /// - Parameters: 37 | /// - elements: an array of particular diff operations 38 | public init(elements: [Diff.Element]) { 39 | self.elements = elements 40 | } 41 | } 42 | 43 | extension Diff.Element { 44 | public init?(trace: Trace) { 45 | switch trace.type() { 46 | case .insertion: 47 | self = .insert(at: trace.from.y) 48 | case .deletion: 49 | self = .delete(at: trace.from.x) 50 | case .matchPoint: 51 | return nil 52 | } 53 | } 54 | 55 | func at() -> Int { 56 | switch self { 57 | case let .delete(at): 58 | return at 59 | case let .insert(at): 60 | return at 61 | } 62 | } 63 | } 64 | 65 | public struct Point: Hashable { 66 | public let x: Int 67 | public let y: Int 68 | } 69 | 70 | /// A data structure representing single trace produced by the diff algorithm. 71 | /// 72 | /// See the [paper](http://www.xmailserver.org/diff2.pdf) for more information on traces. 73 | public struct Trace: Hashable { 74 | public let from: Point 75 | public let to: Point 76 | public let D: Int 77 | } 78 | 79 | enum TraceType { 80 | case insertion 81 | case deletion 82 | case matchPoint 83 | } 84 | 85 | extension Trace { 86 | func type() -> TraceType { 87 | if from.x + 1 == to.x && from.y + 1 == to.y { 88 | return .matchPoint 89 | } else if from.y < to.y { 90 | return .insertion 91 | } else { 92 | return .deletion 93 | } 94 | } 95 | 96 | func k() -> Int { 97 | return from.x - from.y 98 | } 99 | } 100 | 101 | extension Array { 102 | func value(at index: Index) -> Element? { 103 | if index < 0 || index >= count { 104 | return nil 105 | } 106 | return self[index] 107 | } 108 | } 109 | 110 | struct TraceStep { 111 | let D: Int 112 | let k: Int 113 | let previousX: Int? 114 | let nextX: Int? 115 | } 116 | 117 | public typealias EqualityChecker = (T.Element, T.Element) -> Bool 118 | 119 | public extension Collection { 120 | 121 | /// Creates a diff between the calee and `other` collection 122 | /// 123 | /// - Complexity: O((N+M)*D) 124 | /// 125 | /// - Parameters: 126 | /// - other: a collection to compare the calee to 127 | /// - Returns: a Diff between the calee and `other` collection 128 | func diff( 129 | _ other: Self, 130 | isEqual: EqualityChecker 131 | ) -> Diff { 132 | let diffPath = outputDiffPathTraces( 133 | to: other, 134 | isEqual: isEqual 135 | ) 136 | return Diff(elements: 137 | diffPath 138 | .compactMap { Diff.Element(trace: $0) } 139 | ) 140 | } 141 | 142 | /// Generates all traces required to create an output diff. See the [paper](http://www.xmailserver.org/diff2.pdf) for more information on traces. 143 | /// 144 | /// - Parameters: 145 | /// - to: other collection 146 | /// - Returns: all traces required to create an output diff 147 | func diffTraces( 148 | to: Self, 149 | isEqual: EqualityChecker 150 | ) -> [Trace] { 151 | if count == 0 && to.count == 0 { 152 | return [] 153 | } else if count == 0 { 154 | return tracesForInsertions(to: to) 155 | } else if to.count == 0 { 156 | return tracesForDeletions() 157 | } else { 158 | return myersDiffTraces(to: to, isEqual: isEqual) 159 | } 160 | } 161 | 162 | /// Returns the traces which mark the shortest diff path. 163 | func outputDiffPathTraces( 164 | to: Self, 165 | isEqual: EqualityChecker 166 | ) -> [Trace] { 167 | return findPath( 168 | diffTraces(to: to, isEqual: isEqual), 169 | n: Int(count), 170 | m: Int(to.count) 171 | ) 172 | } 173 | 174 | fileprivate func tracesForDeletions() -> [Trace] { 175 | return (0 ..< count) 176 | .map({ 177 | Trace(from: Point(x: $0, y: 0), to: Point(x: $0 + 1, y: 0), D: 0) 178 | }) 179 | } 180 | 181 | fileprivate func tracesForInsertions(to: Self) -> [Trace] { 182 | return (0 ..< to.count) 183 | .map({ 184 | Trace(from: Point(x: 0, y: $0), to: Point(x: 0, y: $0 + 1), D: 0) 185 | }) 186 | } 187 | 188 | fileprivate func myersDiffTraces( 189 | to: Self, 190 | isEqual: (Element, Element) -> Bool 191 | ) -> [Trace] { 192 | 193 | // fromCount is N, N is the number of from array 194 | let fromCount = Int(count) 195 | // toCount is M, M is the number of to array 196 | let toCount = Int(to.count) 197 | var traces = Array() 198 | 199 | let max = fromCount + toCount // this is arbitrary, maximum difference between from and to. N+M assures that this algorithm always finds from diff 200 | 201 | var vertices = Array(repeating: -1, count: max + 1) // from [0...N+M], it is -M...N in the whitepaper 202 | vertices[toCount + 1] = 0 203 | 204 | // D-patch: numberOfDifferences is D 205 | for numberOfDifferences in 0 ... max { 206 | for k in stride(from: (-numberOfDifferences), through: numberOfDifferences, by: 2) { 207 | 208 | guard k >= -toCount && k <= fromCount else { 209 | continue 210 | } 211 | 212 | let index = k + toCount 213 | let traceStep = TraceStep(D: numberOfDifferences, k: k, previousX: vertices.value(at: index - 1), nextX: vertices.value(at: index + 1)) 214 | if let trace = bound(trace: nextTrace(traceStep), maxX: fromCount, maxY: toCount) { 215 | var x = trace.to.x 216 | var y = trace.to.y 217 | 218 | traces.append(trace) 219 | 220 | // keep going as long as they match on diagonal k 221 | while x >= 0 && y >= 0 && x < fromCount && y < toCount { 222 | let targetItem = to.itemOnStartIndex(advancedBy: y) 223 | let baseItem = itemOnStartIndex(advancedBy: x) 224 | if isEqual(baseItem, targetItem) { 225 | x += 1 226 | y += 1 227 | traces.append(Trace(from: Point(x: x - 1, y: y - 1), to: Point(x: x, y: y), D: numberOfDifferences)) 228 | } else { 229 | break 230 | } 231 | } 232 | 233 | vertices[index] = x 234 | 235 | if x >= fromCount && y >= toCount { 236 | return traces 237 | } 238 | } 239 | } 240 | } 241 | return [] 242 | } 243 | 244 | fileprivate func bound(trace: Trace, maxX: Int, maxY: Int) -> Trace? { 245 | guard trace.to.x <= maxX && trace.to.y <= maxY else { 246 | return nil 247 | } 248 | return trace 249 | } 250 | 251 | fileprivate func nextTrace(_ traceStep: TraceStep) -> Trace { 252 | let traceType = nextTraceType(traceStep) 253 | let k = traceStep.k 254 | let D = traceStep.D 255 | 256 | if traceType == .insertion { 257 | let x = traceStep.nextX ?? -1 258 | return Trace(from: Point(x: x, y: x - k - 1), to: Point(x: x, y: x - k), D: D) 259 | } else { 260 | let x = (traceStep.previousX ?? 0) + 1 261 | return Trace(from: Point(x: x - 1, y: x - k), to: Point(x: x, y: x - k), D: D) 262 | } 263 | } 264 | 265 | fileprivate func nextTraceType(_ traceStep: TraceStep) -> TraceType { 266 | let D = traceStep.D 267 | let k = traceStep.k 268 | let previousX = traceStep.previousX 269 | let nextX = traceStep.nextX 270 | 271 | if k == -D { 272 | return .insertion 273 | } else if k != D { 274 | if let previousX = previousX, let nextX = nextX, previousX < nextX { 275 | return .insertion 276 | } 277 | return .deletion 278 | } else { 279 | return .deletion 280 | } 281 | } 282 | 283 | fileprivate func findPath(_ traces: [Trace], n: Int, m: Int) -> [Trace] { 284 | 285 | guard traces.count > 0 else { 286 | return [] 287 | } 288 | 289 | var array = [Trace]() 290 | var item = traces.last! 291 | array.append(item) 292 | 293 | if item.from != Point(x: 0, y: 0) { 294 | for trace in traces.reversed() { 295 | if trace.to.x == item.from.x && trace.to.y == item.from.y { 296 | array.insert(trace, at: 0) 297 | item = trace 298 | 299 | if trace.from == Point(x: 0, y: 0) { 300 | break 301 | } 302 | } 303 | } 304 | } 305 | return array 306 | } 307 | } 308 | 309 | public extension Collection where Element: Equatable { 310 | 311 | /// - SeeAlso: `diff(_:isEqual:)` 312 | func diff( 313 | _ other: Self 314 | ) -> Diff { 315 | return diff(other, isEqual: { $0 == $1 }) 316 | } 317 | 318 | /// - SeeAlso: `diffTraces(to:isEqual:)` 319 | func diffTraces( 320 | to: Self 321 | ) -> [Trace] { 322 | return diffTraces(to: to, isEqual: { $0 == $1 }) 323 | } 324 | 325 | /// - SeeAlso: `outputDiffPathTraces(to:isEqual:)` 326 | func outputDiffPathTraces( 327 | to: Self 328 | ) -> [Trace] { 329 | return outputDiffPathTraces(to: to, isEqual: { $0 == $1 }) 330 | } 331 | } 332 | 333 | extension DiffProtocol { 334 | 335 | public typealias IndexType = Array.Index 336 | 337 | public var startIndex: IndexType { 338 | return elements.startIndex 339 | } 340 | 341 | public var endIndex: IndexType { 342 | return elements.endIndex 343 | } 344 | 345 | public subscript(i: IndexType) -> DiffElementType { 346 | return elements[i] 347 | } 348 | } 349 | 350 | public extension Diff { 351 | init(traces: [Trace]) { 352 | elements = traces.compactMap { Diff.Element(trace: $0) } 353 | } 354 | } 355 | 356 | extension Diff.Element: CustomDebugStringConvertible { 357 | public var debugDescription: String { 358 | switch self { 359 | case let .delete(at): 360 | return "D(\(at))" 361 | case let .insert(at): 362 | return "I(\(at))" 363 | } 364 | } 365 | } 366 | 367 | extension Diff: ExpressibleByArrayLiteral { 368 | 369 | public init(arrayLiteral elements: Diff.Element...) { 370 | self.elements = elements 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /Sources/Differ/Diff+AppKit.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 2 | import AppKit 3 | 4 | // MARK: - NSTableView 5 | 6 | extension NSTableView { 7 | 8 | /// Animates rows which changed between oldData and newData using custom `isEqual`. 9 | /// 10 | /// - Parameters: 11 | /// - oldData: Data which reflects the previous state of `NSTableView` 12 | /// - newData: Data which reflects the current state of `NSTableView` 13 | /// - isEqual: A function comparing two elements of `T` 14 | /// - deletionAnimation: Animation type for deletions 15 | /// - insertionAnimation: Animation type for insertions 16 | /// - rowIndexTransform: Closure which transforms a zero-based row to the desired index 17 | public func animateRowChanges( 18 | oldData: T, 19 | newData: T, 20 | isEqual: EqualityChecker, 21 | deletionAnimation: NSTableView.AnimationOptions = [], 22 | insertionAnimation: NSTableView.AnimationOptions = [], 23 | rowIndexTransform: (Int) -> Int = { $0 } 24 | ) { 25 | apply( 26 | oldData.extendedDiff(newData, isEqual: isEqual).patch(from: oldData, to: newData), 27 | deletionAnimation: deletionAnimation, 28 | insertionAnimation: insertionAnimation, 29 | rowIndexTransform: rowIndexTransform 30 | ) 31 | } 32 | 33 | /// Animates rows which changed between oldData and newData. 34 | /// 35 | /// - Parameters: 36 | /// - oldData: Data which reflects the previous state of `NSTableView` 37 | /// - newData: Data which reflects the current state of `NSTableView` 38 | /// - deletionAnimation: Animation type for deletions 39 | /// - insertionAnimation: Animation type for insertions 40 | /// - rowIndexTransform: Closure which transforms a zero-based row to the desired index 41 | public func animateRowChanges( 42 | oldData: T, 43 | newData: T, 44 | deletionAnimation: NSTableView.AnimationOptions = [], 45 | insertionAnimation: NSTableView.AnimationOptions = [], 46 | rowIndexTransform: (Int) -> Int = { $0 } 47 | ) where T.Element: Equatable { 48 | apply( 49 | extendedPatch(from: oldData, to: newData), 50 | deletionAnimation: deletionAnimation, 51 | insertionAnimation: insertionAnimation, 52 | rowIndexTransform: rowIndexTransform 53 | ) 54 | } 55 | 56 | /// Animates a series of patches in a single beginUpdates/endUpdates batch. 57 | /// - Parameters: 58 | /// - patches: A series of patches to apply 59 | /// - deletionAnimation: Animation type for deletions 60 | /// - insertionAnimation: Animation type for insertions 61 | /// - rowIndexTransform: Closure which transforms a zero-based row to the desired index 62 | public func apply( 63 | _ patches: [ExtendedPatch], 64 | deletionAnimation: NSTableView.AnimationOptions = [], 65 | insertionAnimation: NSTableView.AnimationOptions = [], 66 | rowIndexTransform: (Int) -> Int = { $0 } 67 | ) { 68 | beginUpdates() 69 | for patch in patches { 70 | switch patch { 71 | case .insertion(index: let index, element: _): 72 | insertRows(at: .init(integer: rowIndexTransform(index)), withAnimation: insertionAnimation) 73 | case .deletion(index: let index): 74 | removeRows(at: .init(integer: rowIndexTransform(index)), withAnimation: deletionAnimation) 75 | case .move(from: let from, to: let to): 76 | moveRow(at: rowIndexTransform(from), to: rowIndexTransform(to)) 77 | } 78 | } 79 | endUpdates() 80 | } 81 | 82 | // MARK: Deprecated 83 | 84 | /// Animates rows which changed between oldData and newData. 85 | /// 86 | /// - Parameters: 87 | /// - oldData: Data which reflects the previous state of `NSTableView` 88 | /// - newData: Data which reflects the current state of `NSTableView` 89 | /// - isEqual: A function comparing two elements of `T` 90 | /// - deletionAnimation: Animation type for deletions 91 | /// - insertionAnimation: Animation type for insertions 92 | /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath`. Only the `.item` is used, not the `.section`. 93 | @available(*, deprecated, message: "Use `animateRowChanges(…rowIndexTransform:)`instead. Deprecated because indexPaths are not used in `NSTableView` rows, just Integer row indices.") 94 | public func animateRowChanges( 95 | oldData: T, 96 | newData: T, 97 | isEqual: EqualityChecker, 98 | deletionAnimation: NSTableView.AnimationOptions = [], 99 | insertionAnimation: NSTableView.AnimationOptions = [], 100 | indexPathTransform: (IndexPath) -> IndexPath 101 | ) { 102 | animateRowChanges( 103 | oldData: oldData, 104 | newData: newData, 105 | isEqual: isEqual, 106 | deletionAnimation: deletionAnimation, 107 | insertionAnimation: insertionAnimation, 108 | rowIndexTransform: { indexPathTransform(.init(item: $0, section: 0)).item } 109 | ) 110 | } 111 | 112 | /// Animates rows which changed between oldData and newData. 113 | /// 114 | /// - Parameters: 115 | /// - oldData: Data which reflects the previous state of `NSTableView` 116 | /// - newData: Data which reflects the current state of `NSTableView` 117 | /// - deletionAnimation: Animation type for deletions 118 | /// - insertionAnimation: Animation type for insertions 119 | /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath`. Only the `.item` is used, not the `.section`. 120 | @available(*, deprecated, message: "Use `animateRowChanges(…rowIndexTransform:)`instead. Deprecated because indexPaths are not used in `NSTableView` rows, just Integer row indices.") 121 | public func animateRowChanges( 122 | oldData: T, 123 | newData: T, 124 | deletionAnimation: NSTableView.AnimationOptions = [], 125 | insertionAnimation: NSTableView.AnimationOptions = [], 126 | indexPathTransform: (IndexPath) -> IndexPath 127 | ) where T.Element: Equatable { 128 | animateRowChanges( 129 | oldData: oldData, 130 | newData: newData, 131 | deletionAnimation: deletionAnimation, 132 | insertionAnimation: insertionAnimation, 133 | rowIndexTransform: { indexPathTransform(.init(item: $0, section: 0)).item } 134 | ) 135 | } 136 | 137 | @available(*, deprecated, message: "Use `apply(_ patches: …)` based on `ExtendedPatch` instead. Deprecated because it has errors animating multiple moves.") 138 | public func apply( 139 | _ diff: ExtendedDiff, 140 | deletionAnimation: NSTableView.AnimationOptions = [], 141 | insertionAnimation: NSTableView.AnimationOptions = [], 142 | indexPathTransform: (IndexPath) -> IndexPath = { $0 } 143 | ) { 144 | let update = BatchUpdate(diff: diff, indexPathTransform: indexPathTransform) 145 | 146 | beginUpdates() 147 | removeRows(at: IndexSet(update.deletions.map { $0.item }), withAnimation: deletionAnimation) 148 | insertRows(at: IndexSet(update.insertions.map { $0.item }), withAnimation: insertionAnimation) 149 | update.moves.forEach { moveRow(at: $0.from.item, to: $0.to.item) } 150 | endUpdates() 151 | } 152 | } 153 | 154 | // MARK: - NSCollectionView 155 | 156 | @available(macOS 10.11, *) 157 | public extension NSCollectionView { 158 | /// Animates items which changed between oldData and newData. 159 | /// 160 | /// - Parameters: 161 | /// - oldData: Data which reflects the previous state of `UICollectionView` 162 | /// - newData: Data which reflects the current state of `UICollectionView` 163 | /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` 164 | /// - completion: Closure to be executed when the animation completes 165 | func animateItemChanges( 166 | oldData: T, 167 | newData: T, 168 | indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, 169 | completion: ((Bool) -> Void)? = nil 170 | ) where T.Element: Equatable { 171 | let diff = oldData.extendedDiff(newData) 172 | apply(diff, completion: completion, indexPathTransform: indexPathTransform) 173 | } 174 | 175 | /// Animates items which changed between oldData and newData. 176 | /// 177 | /// - Parameters: 178 | /// - oldData: Data which reflects the previous state of `UICollectionView` 179 | /// - newData: Data which reflects the current state of `UICollectionView` 180 | /// - isEqual: A function comparing two elements of `T` 181 | /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` 182 | /// - completion: Closure to be executed when the animation completes 183 | func animateItemChanges( 184 | oldData: T, 185 | newData: T, 186 | isEqual: EqualityChecker, 187 | indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, 188 | completion: ((Bool) -> Swift.Void)? = nil 189 | ) { 190 | let diff = oldData.extendedDiff(newData, isEqual: isEqual) 191 | apply(diff, completion: completion, indexPathTransform: indexPathTransform) 192 | } 193 | 194 | func apply( 195 | _ diff: ExtendedDiff, 196 | completion: ((Bool) -> Swift.Void)? = nil, 197 | indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 } 198 | ) { 199 | self.animator() 200 | .performBatchUpdates({ 201 | let update = BatchUpdate(diff: diff, indexPathTransform: indexPathTransform) 202 | self.deleteItems(at: Set(update.deletions)) 203 | self.insertItems(at: Set(update.insertions)) 204 | update.moves.forEach { self.moveItem(at: $0.from, to: $0.to) } 205 | }, completionHandler: completion) 206 | } 207 | 208 | /// Animates items and sections which changed between oldData and newData. 209 | /// 210 | /// - Parameters: 211 | /// - oldData: Data which reflects the previous state of `UICollectionView` 212 | /// - newData: Data which reflects the current state of `UICollectionView` 213 | /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` 214 | /// - sectionTransform: Closure which transforms zero-based section(`Int`) into desired section(`Int`) 215 | /// - completion: Closure to be executed when the animation completes 216 | func animateItemAndSectionChanges( 217 | oldData: T, 218 | newData: T, 219 | indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, 220 | sectionTransform: @escaping (Int) -> Int = { $0 }, 221 | completion: ((Bool) -> Swift.Void)? = nil 222 | ) 223 | where T.Element: Collection, 224 | T.Element: Equatable, 225 | T.Element.Element: Equatable { 226 | self.apply( 227 | oldData.nestedExtendedDiff(to: newData), 228 | indexPathTransform: indexPathTransform, 229 | sectionTransform: sectionTransform, 230 | completion: completion 231 | ) 232 | } 233 | 234 | /// Animates items and sections which changed between oldData and newData. 235 | /// 236 | /// - Parameters: 237 | /// - oldData: Data which reflects the previous state of `UICollectionView` 238 | /// - newData: Data which reflects the current state of `UICollectionView` 239 | /// - isEqualElement: A function comparing two items (elements of `T.Element`) 240 | /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` 241 | /// - sectionTransform: Closure which transforms zero-based section(`Int`) into desired section(`Int`) 242 | /// - completion: Closure to be executed when the animation completes 243 | func animateItemAndSectionChanges( 244 | oldData: T, 245 | newData: T, 246 | isEqualElement: NestedElementEqualityChecker, 247 | indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, 248 | sectionTransform: @escaping (Int) -> Int = { $0 }, 249 | completion: ((Bool) -> Swift.Void)? = nil 250 | ) 251 | where T.Element: Collection, 252 | T.Element: Equatable { 253 | self.apply( 254 | oldData.nestedExtendedDiff( 255 | to: newData, 256 | isEqualElement: isEqualElement 257 | ), 258 | indexPathTransform: indexPathTransform, 259 | sectionTransform: sectionTransform, 260 | completion: completion 261 | ) 262 | } 263 | 264 | /// Animates items and sections which changed between oldData and newData. 265 | /// 266 | /// - Parameters: 267 | /// - oldData: Data which reflects the previous state of `UICollectionView` 268 | /// - newData: Data which reflects the current state of `UICollectionView` 269 | /// - isEqualSection: A function comparing two sections (elements of `T`) 270 | /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` 271 | /// - sectionTransform: Closure which transforms zero-based section(`Int`) into desired section(`Int`) 272 | /// - completion: Closure to be executed when the animation completes 273 | func animateItemAndSectionChanges( 274 | oldData: T, 275 | newData: T, 276 | isEqualSection: EqualityChecker, 277 | indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, 278 | sectionTransform: @escaping (Int) -> Int = { $0 }, 279 | completion: ((Bool) -> Swift.Void)? = nil 280 | ) 281 | where T.Element: Collection, 282 | T.Element.Element: Equatable { 283 | self.apply( 284 | oldData.nestedExtendedDiff( 285 | to: newData, 286 | isEqualSection: isEqualSection 287 | ), 288 | indexPathTransform: indexPathTransform, 289 | sectionTransform: sectionTransform, 290 | completion: completion 291 | ) 292 | } 293 | 294 | /// Animates items and sections which changed between oldData and newData. 295 | /// 296 | /// - Parameters: 297 | /// - oldData: Data which reflects the previous state of `UICollectionView` 298 | /// - newData: Data which reflects the current state of `UICollectionView` 299 | /// - isEqualSection: A function comparing two sections (elements of `T`) 300 | /// - isEqualElement: A function comparing two items (elements of `T.Element`) 301 | /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` 302 | /// - sectionTransform: Closure which transforms zero-based section(`Int`) into desired section(`Int`) 303 | /// - completion: Closure to be executed when the animation completes 304 | func animateItemAndSectionChanges( 305 | oldData: T, 306 | newData: T, 307 | isEqualSection: EqualityChecker, 308 | isEqualElement: NestedElementEqualityChecker, 309 | indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, 310 | sectionTransform: @escaping (Int) -> Int = { $0 }, 311 | completion: ((Bool) -> Swift.Void)? = nil 312 | ) 313 | where T.Element: Collection { 314 | self.apply( 315 | oldData.nestedExtendedDiff( 316 | to: newData, 317 | isEqualSection: isEqualSection, 318 | isEqualElement: isEqualElement 319 | ), 320 | indexPathTransform: indexPathTransform, 321 | sectionTransform: sectionTransform, 322 | completion: completion 323 | ) 324 | } 325 | 326 | func apply( 327 | _ diff: NestedExtendedDiff, 328 | indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, 329 | sectionTransform: @escaping (Int) -> Int = { $0 }, 330 | completion: ((Bool) -> Void)? = nil 331 | ) { 332 | self.animator() 333 | .performBatchUpdates({ 334 | let update = NestedBatchUpdate(diff: diff, indexPathTransform: indexPathTransform, sectionTransform: sectionTransform) 335 | self.insertSections(update.sectionInsertions) 336 | self.deleteSections(update.sectionDeletions) 337 | update.sectionMoves.forEach { self.moveSection($0.from, toSection: $0.to) } 338 | self.deleteItems(at: Set(update.itemDeletions)) 339 | self.insertItems(at: Set(update.itemInsertions)) 340 | update.itemMoves.forEach { self.moveItem(at: $0.from, to: $0.to) } 341 | }, completionHandler: completion) 342 | } 343 | } 344 | 345 | #endif 346 | -------------------------------------------------------------------------------- /Examples/TableViewExample/TableViewExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9098C1781F6FE89700026404 /* Differ.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9098C1721F6FE88500026404 /* Differ.framework */; }; 11 | 9098C1791F6FE89700026404 /* Differ.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 9098C1721F6FE88500026404 /* Differ.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 12 | C9278AB61DE31362009CE846 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9278AB51DE31362009CE846 /* AppDelegate.swift */; }; 13 | C9278AB81DE31362009CE846 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9278AB71DE31362009CE846 /* TableViewController.swift */; }; 14 | C9278ABD1DE31362009CE846 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C9278ABB1DE31362009CE846 /* Main.storyboard */; }; 15 | C9278ABF1DE31362009CE846 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9278ABE1DE31362009CE846 /* Assets.xcassets */; }; 16 | C9278AC21DE31362009CE846 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C9278AC01DE31362009CE846 /* LaunchScreen.storyboard */; }; 17 | C93448FF1E195C570035E956 /* NestedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93448FE1E195C570035E956 /* NestedTableViewController.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 9098C1711F6FE88500026404 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = C9278AC91DE323F6009CE846 /* Differ.xcodeproj */; 24 | proxyType = 2; 25 | remoteGlobalIDString = C9EE87161CCFCA83006BD90E; 26 | remoteInfo = Differ; 27 | }; 28 | 9098C1731F6FE88500026404 /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = C9278AC91DE323F6009CE846 /* Differ.xcodeproj */; 31 | proxyType = 2; 32 | remoteGlobalIDString = C9838FF11D29571000691BE8; 33 | remoteInfo = DifferTests; 34 | }; 35 | 9098C17A1F6FE89700026404 /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = C9278AC91DE323F6009CE846 /* Differ.xcodeproj */; 38 | proxyType = 1; 39 | remoteGlobalIDString = C9EE87151CCFCA83006BD90E; 40 | remoteInfo = Differ; 41 | }; 42 | /* End PBXContainerItemProxy section */ 43 | 44 | /* Begin PBXCopyFilesBuildPhase section */ 45 | C9278AD71DE32424009CE846 /* CopyFiles */ = { 46 | isa = PBXCopyFilesBuildPhase; 47 | buildActionMask = 2147483647; 48 | dstPath = ""; 49 | dstSubfolderSpec = 10; 50 | files = ( 51 | 9098C1791F6FE89700026404 /* Differ.framework in CopyFiles */, 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXCopyFilesBuildPhase section */ 56 | 57 | /* Begin PBXFileReference section */ 58 | 83A52110201BC80200514B1C /* Graph.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Graph.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 59 | C9278AB21DE31362009CE846 /* TableViewExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TableViewExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | C9278AB51DE31362009CE846 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 61 | C9278AB71DE31362009CE846 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 62 | C9278ABC1DE31362009CE846 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 63 | C9278ABE1DE31362009CE846 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 64 | C9278AC11DE31362009CE846 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 65 | C9278AC31DE31362009CE846 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | C9278AC91DE323F6009CE846 /* Differ.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Differ.xcodeproj; path = ../../Differ.xcodeproj; sourceTree = ""; }; 67 | C93448FE1E195C570035E956 /* NestedTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NestedTableViewController.swift; sourceTree = ""; }; 68 | /* End PBXFileReference section */ 69 | 70 | /* Begin PBXFrameworksBuildPhase section */ 71 | C9278AAF1DE31362009CE846 /* Frameworks */ = { 72 | isa = PBXFrameworksBuildPhase; 73 | buildActionMask = 2147483647; 74 | files = ( 75 | 9098C1781F6FE89700026404 /* Differ.framework in Frameworks */, 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXFrameworksBuildPhase section */ 80 | 81 | /* Begin PBXGroup section */ 82 | 9098C16D1F6FE88500026404 /* Products */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 9098C1721F6FE88500026404 /* Differ.framework */, 86 | 9098C1741F6FE88500026404 /* DifferTests.xctest */, 87 | ); 88 | name = Products; 89 | sourceTree = ""; 90 | }; 91 | C9278AA91DE31361009CE846 = { 92 | isa = PBXGroup; 93 | children = ( 94 | 83A52110201BC80200514B1C /* Graph.playground */, 95 | C9278AC91DE323F6009CE846 /* Differ.xcodeproj */, 96 | C9278AB41DE31362009CE846 /* TableViewExample */, 97 | C9278AB31DE31362009CE846 /* Products */, 98 | ); 99 | sourceTree = ""; 100 | }; 101 | C9278AB31DE31362009CE846 /* Products */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | C9278AB21DE31362009CE846 /* TableViewExample.app */, 105 | ); 106 | name = Products; 107 | sourceTree = ""; 108 | }; 109 | C9278AB41DE31362009CE846 /* TableViewExample */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | C9278AB51DE31362009CE846 /* AppDelegate.swift */, 113 | C9278AB71DE31362009CE846 /* TableViewController.swift */, 114 | C93448FE1E195C570035E956 /* NestedTableViewController.swift */, 115 | C9278ABB1DE31362009CE846 /* Main.storyboard */, 116 | C9278ABE1DE31362009CE846 /* Assets.xcassets */, 117 | C9278AC01DE31362009CE846 /* LaunchScreen.storyboard */, 118 | C9278AC31DE31362009CE846 /* Info.plist */, 119 | ); 120 | path = TableViewExample; 121 | sourceTree = ""; 122 | }; 123 | /* End PBXGroup section */ 124 | 125 | /* Begin PBXNativeTarget section */ 126 | C9278AB11DE31362009CE846 /* TableViewExample */ = { 127 | isa = PBXNativeTarget; 128 | buildConfigurationList = C9278AC61DE31362009CE846 /* Build configuration list for PBXNativeTarget "TableViewExample" */; 129 | buildPhases = ( 130 | C9278AAE1DE31362009CE846 /* Sources */, 131 | C9278AAF1DE31362009CE846 /* Frameworks */, 132 | C9278AB01DE31362009CE846 /* Resources */, 133 | C9278AD71DE32424009CE846 /* CopyFiles */, 134 | ); 135 | buildRules = ( 136 | ); 137 | dependencies = ( 138 | 9098C17B1F6FE89700026404 /* PBXTargetDependency */, 139 | ); 140 | name = TableViewExample; 141 | productName = TableViewExample; 142 | productReference = C9278AB21DE31362009CE846 /* TableViewExample.app */; 143 | productType = "com.apple.product-type.application"; 144 | }; 145 | /* End PBXNativeTarget section */ 146 | 147 | /* Begin PBXProject section */ 148 | C9278AAA1DE31361009CE846 /* Project object */ = { 149 | isa = PBXProject; 150 | attributes = { 151 | LastSwiftUpdateCheck = 0800; 152 | LastUpgradeCheck = 0900; 153 | ORGANIZATIONNAME = wokalski; 154 | TargetAttributes = { 155 | C9278AB11DE31362009CE846 = { 156 | CreatedOnToolsVersion = 8.0; 157 | ProvisioningStyle = Automatic; 158 | }; 159 | }; 160 | }; 161 | buildConfigurationList = C9278AAD1DE31361009CE846 /* Build configuration list for PBXProject "TableViewExample" */; 162 | compatibilityVersion = "Xcode 3.2"; 163 | developmentRegion = English; 164 | hasScannedForEncodings = 0; 165 | knownRegions = ( 166 | English, 167 | en, 168 | Base, 169 | ); 170 | mainGroup = C9278AA91DE31361009CE846; 171 | productRefGroup = C9278AB31DE31362009CE846 /* Products */; 172 | projectDirPath = ""; 173 | projectReferences = ( 174 | { 175 | ProductGroup = 9098C16D1F6FE88500026404 /* Products */; 176 | ProjectRef = C9278AC91DE323F6009CE846 /* Differ.xcodeproj */; 177 | }, 178 | ); 179 | projectRoot = ""; 180 | targets = ( 181 | C9278AB11DE31362009CE846 /* TableViewExample */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXReferenceProxy section */ 187 | 9098C1721F6FE88500026404 /* Differ.framework */ = { 188 | isa = PBXReferenceProxy; 189 | fileType = wrapper.framework; 190 | path = Differ.framework; 191 | remoteRef = 9098C1711F6FE88500026404 /* PBXContainerItemProxy */; 192 | sourceTree = BUILT_PRODUCTS_DIR; 193 | }; 194 | 9098C1741F6FE88500026404 /* DifferTests.xctest */ = { 195 | isa = PBXReferenceProxy; 196 | fileType = wrapper.cfbundle; 197 | path = DifferTests.xctest; 198 | remoteRef = 9098C1731F6FE88500026404 /* PBXContainerItemProxy */; 199 | sourceTree = BUILT_PRODUCTS_DIR; 200 | }; 201 | /* End PBXReferenceProxy section */ 202 | 203 | /* Begin PBXResourcesBuildPhase section */ 204 | C9278AB01DE31362009CE846 /* Resources */ = { 205 | isa = PBXResourcesBuildPhase; 206 | buildActionMask = 2147483647; 207 | files = ( 208 | C9278AC21DE31362009CE846 /* LaunchScreen.storyboard in Resources */, 209 | C9278ABF1DE31362009CE846 /* Assets.xcassets in Resources */, 210 | C9278ABD1DE31362009CE846 /* Main.storyboard in Resources */, 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | }; 214 | /* End PBXResourcesBuildPhase section */ 215 | 216 | /* Begin PBXSourcesBuildPhase section */ 217 | C9278AAE1DE31362009CE846 /* Sources */ = { 218 | isa = PBXSourcesBuildPhase; 219 | buildActionMask = 2147483647; 220 | files = ( 221 | C93448FF1E195C570035E956 /* NestedTableViewController.swift in Sources */, 222 | C9278AB81DE31362009CE846 /* TableViewController.swift in Sources */, 223 | C9278AB61DE31362009CE846 /* AppDelegate.swift in Sources */, 224 | ); 225 | runOnlyForDeploymentPostprocessing = 0; 226 | }; 227 | /* End PBXSourcesBuildPhase section */ 228 | 229 | /* Begin PBXTargetDependency section */ 230 | 9098C17B1F6FE89700026404 /* PBXTargetDependency */ = { 231 | isa = PBXTargetDependency; 232 | name = Differ; 233 | targetProxy = 9098C17A1F6FE89700026404 /* PBXContainerItemProxy */; 234 | }; 235 | /* End PBXTargetDependency section */ 236 | 237 | /* Begin PBXVariantGroup section */ 238 | C9278ABB1DE31362009CE846 /* Main.storyboard */ = { 239 | isa = PBXVariantGroup; 240 | children = ( 241 | C9278ABC1DE31362009CE846 /* Base */, 242 | ); 243 | name = Main.storyboard; 244 | sourceTree = ""; 245 | }; 246 | C9278AC01DE31362009CE846 /* LaunchScreen.storyboard */ = { 247 | isa = PBXVariantGroup; 248 | children = ( 249 | C9278AC11DE31362009CE846 /* Base */, 250 | ); 251 | name = LaunchScreen.storyboard; 252 | sourceTree = ""; 253 | }; 254 | /* End PBXVariantGroup section */ 255 | 256 | /* Begin XCBuildConfiguration section */ 257 | C9278AC41DE31362009CE846 /* Debug */ = { 258 | isa = XCBuildConfiguration; 259 | buildSettings = { 260 | ALWAYS_SEARCH_USER_PATHS = NO; 261 | CLANG_ANALYZER_NONNULL = YES; 262 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 263 | CLANG_CXX_LIBRARY = "libc++"; 264 | CLANG_ENABLE_MODULES = YES; 265 | CLANG_ENABLE_OBJC_ARC = YES; 266 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 267 | CLANG_WARN_BOOL_CONVERSION = YES; 268 | CLANG_WARN_COMMA = YES; 269 | CLANG_WARN_CONSTANT_CONVERSION = YES; 270 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 271 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 272 | CLANG_WARN_EMPTY_BODY = YES; 273 | CLANG_WARN_ENUM_CONVERSION = YES; 274 | CLANG_WARN_INFINITE_RECURSION = YES; 275 | CLANG_WARN_INT_CONVERSION = YES; 276 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 277 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 280 | CLANG_WARN_STRICT_PROTOTYPES = YES; 281 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 282 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 283 | CLANG_WARN_UNREACHABLE_CODE = YES; 284 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 285 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 286 | COPY_PHASE_STRIP = NO; 287 | DEBUG_INFORMATION_FORMAT = dwarf; 288 | ENABLE_STRICT_OBJC_MSGSEND = YES; 289 | ENABLE_TESTABILITY = YES; 290 | GCC_C_LANGUAGE_STANDARD = gnu99; 291 | GCC_DYNAMIC_NO_PIC = NO; 292 | GCC_NO_COMMON_BLOCKS = YES; 293 | GCC_OPTIMIZATION_LEVEL = 0; 294 | GCC_PREPROCESSOR_DEFINITIONS = ( 295 | "DEBUG=1", 296 | "$(inherited)", 297 | ); 298 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 299 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 300 | GCC_WARN_UNDECLARED_SELECTOR = YES; 301 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 302 | GCC_WARN_UNUSED_FUNCTION = YES; 303 | GCC_WARN_UNUSED_VARIABLE = YES; 304 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 305 | ONLY_ACTIVE_ARCH = YES; 306 | PRODUCT_BUNDLE_IDENTIFIER = "com.tonyarnold.$(TARGET_NAME:c99-identifier)"; 307 | PRODUCT_NAME = "$(TARGET_NAME)"; 308 | SDKROOT = iphoneos; 309 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 310 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 311 | SWIFT_VERSION = 4.0; 312 | }; 313 | name = Debug; 314 | }; 315 | C9278AC51DE31362009CE846 /* Release */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ALWAYS_SEARCH_USER_PATHS = NO; 319 | CLANG_ANALYZER_NONNULL = YES; 320 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 321 | CLANG_CXX_LIBRARY = "libc++"; 322 | CLANG_ENABLE_MODULES = YES; 323 | CLANG_ENABLE_OBJC_ARC = YES; 324 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 325 | CLANG_WARN_BOOL_CONVERSION = YES; 326 | CLANG_WARN_COMMA = YES; 327 | CLANG_WARN_CONSTANT_CONVERSION = YES; 328 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 329 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 330 | CLANG_WARN_EMPTY_BODY = YES; 331 | CLANG_WARN_ENUM_CONVERSION = YES; 332 | CLANG_WARN_INFINITE_RECURSION = YES; 333 | CLANG_WARN_INT_CONVERSION = YES; 334 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 335 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 336 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 337 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 338 | CLANG_WARN_STRICT_PROTOTYPES = YES; 339 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 340 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 341 | CLANG_WARN_UNREACHABLE_CODE = YES; 342 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 343 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 344 | COPY_PHASE_STRIP = NO; 345 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 346 | ENABLE_NS_ASSERTIONS = NO; 347 | ENABLE_STRICT_OBJC_MSGSEND = YES; 348 | GCC_C_LANGUAGE_STANDARD = gnu99; 349 | GCC_NO_COMMON_BLOCKS = YES; 350 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 351 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 352 | GCC_WARN_UNDECLARED_SELECTOR = YES; 353 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 354 | GCC_WARN_UNUSED_FUNCTION = YES; 355 | GCC_WARN_UNUSED_VARIABLE = YES; 356 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 357 | PRODUCT_BUNDLE_IDENTIFIER = "com.tonyarnold.$(TARGET_NAME:c99-identifier)"; 358 | PRODUCT_NAME = "$(TARGET_NAME)"; 359 | SDKROOT = iphoneos; 360 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 361 | SWIFT_VERSION = 4.0; 362 | VALIDATE_PRODUCT = YES; 363 | }; 364 | name = Release; 365 | }; 366 | C9278AC71DE31362009CE846 /* Debug */ = { 367 | isa = XCBuildConfiguration; 368 | buildSettings = { 369 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 370 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 371 | DEVELOPMENT_TEAM = ""; 372 | INFOPLIST_FILE = TableViewExample/Info.plist; 373 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 374 | }; 375 | name = Debug; 376 | }; 377 | C9278AC81DE31362009CE846 /* Release */ = { 378 | isa = XCBuildConfiguration; 379 | buildSettings = { 380 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 381 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 382 | DEVELOPMENT_TEAM = ""; 383 | INFOPLIST_FILE = TableViewExample/Info.plist; 384 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 385 | }; 386 | name = Release; 387 | }; 388 | /* End XCBuildConfiguration section */ 389 | 390 | /* Begin XCConfigurationList section */ 391 | C9278AAD1DE31361009CE846 /* Build configuration list for PBXProject "TableViewExample" */ = { 392 | isa = XCConfigurationList; 393 | buildConfigurations = ( 394 | C9278AC41DE31362009CE846 /* Debug */, 395 | C9278AC51DE31362009CE846 /* Release */, 396 | ); 397 | defaultConfigurationIsVisible = 0; 398 | defaultConfigurationName = Release; 399 | }; 400 | C9278AC61DE31362009CE846 /* Build configuration list for PBXNativeTarget "TableViewExample" */ = { 401 | isa = XCConfigurationList; 402 | buildConfigurations = ( 403 | C9278AC71DE31362009CE846 /* Debug */, 404 | C9278AC81DE31362009CE846 /* Release */, 405 | ); 406 | defaultConfigurationIsVisible = 0; 407 | defaultConfigurationName = Release; 408 | }; 409 | /* End XCConfigurationList section */ 410 | }; 411 | rootObject = C9278AAA1DE31361009CE846 /* Project object */; 412 | } 413 | --------------------------------------------------------------------------------