├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Examples └── HeckelDiffExample │ ├── AppDelegate.swift │ ├── HeckelDiffExample.xcodeproj │ └── project.pbxproj │ ├── HeckelDiffExample │ └── Info.plist │ ├── HeckelDiffExample_tvOS │ └── Info.plist │ └── ViewController.swift ├── HeckelDiff.podspec ├── HeckelDiff.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── HeckelDiff.xcscheme ├── HeckelDiff.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Source ├── Diff.swift ├── HeckelDiff.h ├── Info.plist ├── ListUpdate.swift ├── UICollectionView+Diff.swift └── UITableView+Diff.swift └── Tests ├── DiffTests.swift ├── HeckelDiffTests.swift └── Info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/HeckelDiffExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // HeckelDiffExample 4 | // 5 | // Created by Matias Cudich on 11/23/16. 6 | // Copyright © 2016 Matias Cudich. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? = { 15 | let window = UIWindow(frame: UIScreen.main.bounds) 16 | window.backgroundColor = .white 17 | return window 18 | }() 19 | 20 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 21 | window?.rootViewController = ViewController() 22 | window?.makeKeyAndVisible() 23 | return true 24 | } 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Examples/HeckelDiffExample/HeckelDiffExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3A16935520EDCF78005E2034 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D869F5D51DE622CD00CDAFAF /* AppDelegate.swift */; }; 11 | 3A16935620EDCF7F005E2034 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D869F5D71DE622CD00CDAFAF /* ViewController.swift */; }; 12 | D869F5D61DE622CD00CDAFAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D869F5D51DE622CD00CDAFAF /* AppDelegate.swift */; }; 13 | D869F5D81DE622CD00CDAFAF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D869F5D71DE622CD00CDAFAF /* ViewController.swift */; }; 14 | D869F5EA1DE6235400CDAFAF /* HeckelDiff.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D869F5E91DE6235400CDAFAF /* HeckelDiff.framework */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 3A16934620EDCF28005E2034 /* HeckelDiffExample_tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HeckelDiffExample_tvOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 3A16935120EDCF2B005E2034 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 20 | D869F5D21DE622CD00CDAFAF /* HeckelDiffExample_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HeckelDiffExample_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | D869F5D51DE622CD00CDAFAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | D869F5D71DE622CD00CDAFAF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | D869F5E11DE622CD00CDAFAF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | D869F5E91DE6235400CDAFAF /* HeckelDiff.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HeckelDiff.framework; path = "../../../../Library/Developer/Xcode/DerivedData/HeckelDiff-erjcepmqpsqxtmfwwwgzprtwbpvk/Build/Products/Debug-iphonesimulator/HeckelDiff.framework"; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | 3A16934320EDCF28005E2034 /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | ); 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | D869F5CF1DE622CD00CDAFAF /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | D869F5EA1DE6235400CDAFAF /* HeckelDiff.framework in Frameworks */, 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXFrameworksBuildPhase section */ 44 | 45 | /* Begin PBXGroup section */ 46 | 3A16934720EDCF29005E2034 /* HeckelDiffExample_tvOS */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | 3A16935120EDCF2B005E2034 /* Info.plist */, 50 | ); 51 | path = HeckelDiffExample_tvOS; 52 | sourceTree = ""; 53 | }; 54 | D869F5C91DE622CD00CDAFAF = { 55 | isa = PBXGroup; 56 | children = ( 57 | D869F5D51DE622CD00CDAFAF /* AppDelegate.swift */, 58 | D869F5D71DE622CD00CDAFAF /* ViewController.swift */, 59 | D869F5D41DE622CD00CDAFAF /* HeckelDiffExample */, 60 | 3A16934720EDCF29005E2034 /* HeckelDiffExample_tvOS */, 61 | D869F5D31DE622CD00CDAFAF /* Products */, 62 | D869F5E81DE6235400CDAFAF /* Frameworks */, 63 | ); 64 | sourceTree = ""; 65 | }; 66 | D869F5D31DE622CD00CDAFAF /* Products */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | D869F5D21DE622CD00CDAFAF /* HeckelDiffExample_iOS.app */, 70 | 3A16934620EDCF28005E2034 /* HeckelDiffExample_tvOS.app */, 71 | ); 72 | name = Products; 73 | sourceTree = ""; 74 | }; 75 | D869F5D41DE622CD00CDAFAF /* HeckelDiffExample */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | D869F5E11DE622CD00CDAFAF /* Info.plist */, 79 | ); 80 | path = HeckelDiffExample; 81 | sourceTree = ""; 82 | }; 83 | D869F5E81DE6235400CDAFAF /* Frameworks */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | D869F5E91DE6235400CDAFAF /* HeckelDiff.framework */, 87 | ); 88 | name = Frameworks; 89 | sourceTree = ""; 90 | }; 91 | /* End PBXGroup section */ 92 | 93 | /* Begin PBXNativeTarget section */ 94 | 3A16934520EDCF28005E2034 /* HeckelDiffExample_tvOS */ = { 95 | isa = PBXNativeTarget; 96 | buildConfigurationList = 3A16935220EDCF2B005E2034 /* Build configuration list for PBXNativeTarget "HeckelDiffExample_tvOS" */; 97 | buildPhases = ( 98 | 3A16934220EDCF28005E2034 /* Sources */, 99 | 3A16934320EDCF28005E2034 /* Frameworks */, 100 | 3A16934420EDCF28005E2034 /* Resources */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = HeckelDiffExample_tvOS; 107 | productName = HeckelDiffExample_tvOS; 108 | productReference = 3A16934620EDCF28005E2034 /* HeckelDiffExample_tvOS.app */; 109 | productType = "com.apple.product-type.application"; 110 | }; 111 | D869F5D11DE622CD00CDAFAF /* HeckelDiffExample_iOS */ = { 112 | isa = PBXNativeTarget; 113 | buildConfigurationList = D869F5E41DE622CD00CDAFAF /* Build configuration list for PBXNativeTarget "HeckelDiffExample_iOS" */; 114 | buildPhases = ( 115 | D869F5CE1DE622CD00CDAFAF /* Sources */, 116 | D869F5CF1DE622CD00CDAFAF /* Frameworks */, 117 | D869F5D01DE622CD00CDAFAF /* Resources */, 118 | ); 119 | buildRules = ( 120 | ); 121 | dependencies = ( 122 | ); 123 | name = HeckelDiffExample_iOS; 124 | productName = HeckelDiffExample; 125 | productReference = D869F5D21DE622CD00CDAFAF /* HeckelDiffExample_iOS.app */; 126 | productType = "com.apple.product-type.application"; 127 | }; 128 | /* End PBXNativeTarget section */ 129 | 130 | /* Begin PBXProject section */ 131 | D869F5CA1DE622CD00CDAFAF /* Project object */ = { 132 | isa = PBXProject; 133 | attributes = { 134 | LastSwiftUpdateCheck = 0940; 135 | LastUpgradeCheck = 1010; 136 | ORGANIZATIONNAME = "Matias Cudich"; 137 | TargetAttributes = { 138 | 3A16934520EDCF28005E2034 = { 139 | CreatedOnToolsVersion = 9.4.1; 140 | ProvisioningStyle = Automatic; 141 | }; 142 | D869F5D11DE622CD00CDAFAF = { 143 | CreatedOnToolsVersion = 8.1; 144 | DevelopmentTeam = 46UKZ786J3; 145 | LastSwiftMigration = 1010; 146 | ProvisioningStyle = Automatic; 147 | }; 148 | }; 149 | }; 150 | buildConfigurationList = D869F5CD1DE622CD00CDAFAF /* Build configuration list for PBXProject "HeckelDiffExample" */; 151 | compatibilityVersion = "Xcode 3.2"; 152 | developmentRegion = English; 153 | hasScannedForEncodings = 0; 154 | knownRegions = ( 155 | en, 156 | Base, 157 | ); 158 | mainGroup = D869F5C91DE622CD00CDAFAF; 159 | productRefGroup = D869F5D31DE622CD00CDAFAF /* Products */; 160 | projectDirPath = ""; 161 | projectRoot = ""; 162 | targets = ( 163 | D869F5D11DE622CD00CDAFAF /* HeckelDiffExample_iOS */, 164 | 3A16934520EDCF28005E2034 /* HeckelDiffExample_tvOS */, 165 | ); 166 | }; 167 | /* End PBXProject section */ 168 | 169 | /* Begin PBXResourcesBuildPhase section */ 170 | 3A16934420EDCF28005E2034 /* Resources */ = { 171 | isa = PBXResourcesBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | D869F5D01DE622CD00CDAFAF /* Resources */ = { 178 | isa = PBXResourcesBuildPhase; 179 | buildActionMask = 2147483647; 180 | files = ( 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXResourcesBuildPhase section */ 185 | 186 | /* Begin PBXSourcesBuildPhase section */ 187 | 3A16934220EDCF28005E2034 /* Sources */ = { 188 | isa = PBXSourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 3A16935620EDCF7F005E2034 /* ViewController.swift in Sources */, 192 | 3A16935520EDCF78005E2034 /* AppDelegate.swift in Sources */, 193 | ); 194 | runOnlyForDeploymentPostprocessing = 0; 195 | }; 196 | D869F5CE1DE622CD00CDAFAF /* Sources */ = { 197 | isa = PBXSourcesBuildPhase; 198 | buildActionMask = 2147483647; 199 | files = ( 200 | D869F5D81DE622CD00CDAFAF /* ViewController.swift in Sources */, 201 | D869F5D61DE622CD00CDAFAF /* AppDelegate.swift in Sources */, 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | /* End PBXSourcesBuildPhase section */ 206 | 207 | /* Begin XCBuildConfiguration section */ 208 | 3A16935320EDCF2B005E2034 /* Debug */ = { 209 | isa = XCBuildConfiguration; 210 | buildSettings = { 211 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; 212 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 213 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 214 | CLANG_ENABLE_OBJC_WEAK = YES; 215 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 216 | CLANG_WARN_COMMA = YES; 217 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 218 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 220 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 221 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 222 | CLANG_WARN_STRICT_PROTOTYPES = YES; 223 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 224 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 225 | CODE_SIGN_STYLE = Automatic; 226 | GCC_C_LANGUAGE_STANDARD = gnu11; 227 | INFOPLIST_FILE = HeckelDiffExample_tvOS/Info.plist; 228 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 229 | PRODUCT_BUNDLE_IDENTIFIER = "com.codeisjoy.HeckelDiffExample-tvOS"; 230 | PRODUCT_NAME = "$(TARGET_NAME)"; 231 | SDKROOT = appletvos; 232 | TARGETED_DEVICE_FAMILY = 3; 233 | TVOS_DEPLOYMENT_TARGET = 11.4; 234 | }; 235 | name = Debug; 236 | }; 237 | 3A16935420EDCF2B005E2034 /* Release */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; 241 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 243 | CLANG_ENABLE_OBJC_WEAK = YES; 244 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 247 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 249 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 251 | CLANG_WARN_STRICT_PROTOTYPES = YES; 252 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 253 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 254 | CODE_SIGN_STYLE = Automatic; 255 | GCC_C_LANGUAGE_STANDARD = gnu11; 256 | INFOPLIST_FILE = HeckelDiffExample_tvOS/Info.plist; 257 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 258 | PRODUCT_BUNDLE_IDENTIFIER = "com.codeisjoy.HeckelDiffExample-tvOS"; 259 | PRODUCT_NAME = "$(TARGET_NAME)"; 260 | SDKROOT = appletvos; 261 | TARGETED_DEVICE_FAMILY = 3; 262 | TVOS_DEPLOYMENT_TARGET = 11.4; 263 | }; 264 | name = Release; 265 | }; 266 | D869F5E21DE622CD00CDAFAF /* Debug */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ALWAYS_SEARCH_USER_PATHS = NO; 270 | CLANG_ANALYZER_NONNULL = YES; 271 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 272 | CLANG_CXX_LIBRARY = "libc++"; 273 | CLANG_ENABLE_MODULES = YES; 274 | CLANG_ENABLE_OBJC_ARC = YES; 275 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 276 | CLANG_WARN_BOOL_CONVERSION = YES; 277 | CLANG_WARN_COMMA = YES; 278 | CLANG_WARN_CONSTANT_CONVERSION = YES; 279 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 281 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 282 | CLANG_WARN_EMPTY_BODY = YES; 283 | CLANG_WARN_ENUM_CONVERSION = YES; 284 | CLANG_WARN_INFINITE_RECURSION = YES; 285 | CLANG_WARN_INT_CONVERSION = YES; 286 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 288 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 289 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 290 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 291 | CLANG_WARN_STRICT_PROTOTYPES = YES; 292 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 293 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 294 | CLANG_WARN_UNREACHABLE_CODE = YES; 295 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 296 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 297 | COPY_PHASE_STRIP = NO; 298 | DEBUG_INFORMATION_FORMAT = dwarf; 299 | ENABLE_STRICT_OBJC_MSGSEND = YES; 300 | ENABLE_TESTABILITY = YES; 301 | GCC_C_LANGUAGE_STANDARD = gnu99; 302 | GCC_DYNAMIC_NO_PIC = NO; 303 | GCC_NO_COMMON_BLOCKS = YES; 304 | GCC_OPTIMIZATION_LEVEL = 0; 305 | GCC_PREPROCESSOR_DEFINITIONS = ( 306 | "DEBUG=1", 307 | "$(inherited)", 308 | ); 309 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 310 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 311 | GCC_WARN_UNDECLARED_SELECTOR = YES; 312 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 313 | GCC_WARN_UNUSED_FUNCTION = YES; 314 | GCC_WARN_UNUSED_VARIABLE = YES; 315 | IPHONEOS_DEPLOYMENT_TARGET = 10.1; 316 | MTL_ENABLE_DEBUG_INFO = YES; 317 | ONLY_ACTIVE_ARCH = YES; 318 | SDKROOT = iphoneos; 319 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 320 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 321 | SWIFT_VERSION = 4.2; 322 | TARGETED_DEVICE_FAMILY = "1,2"; 323 | }; 324 | name = Debug; 325 | }; 326 | D869F5E31DE622CD00CDAFAF /* Release */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ALWAYS_SEARCH_USER_PATHS = NO; 330 | CLANG_ANALYZER_NONNULL = YES; 331 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 332 | CLANG_CXX_LIBRARY = "libc++"; 333 | CLANG_ENABLE_MODULES = YES; 334 | CLANG_ENABLE_OBJC_ARC = YES; 335 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 336 | CLANG_WARN_BOOL_CONVERSION = YES; 337 | CLANG_WARN_COMMA = YES; 338 | CLANG_WARN_CONSTANT_CONVERSION = YES; 339 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 340 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 341 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 342 | CLANG_WARN_EMPTY_BODY = YES; 343 | CLANG_WARN_ENUM_CONVERSION = YES; 344 | CLANG_WARN_INFINITE_RECURSION = YES; 345 | CLANG_WARN_INT_CONVERSION = YES; 346 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 347 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 348 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 349 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 350 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 351 | CLANG_WARN_STRICT_PROTOTYPES = YES; 352 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 353 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 354 | CLANG_WARN_UNREACHABLE_CODE = YES; 355 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 356 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 357 | COPY_PHASE_STRIP = NO; 358 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 359 | ENABLE_NS_ASSERTIONS = NO; 360 | ENABLE_STRICT_OBJC_MSGSEND = YES; 361 | GCC_C_LANGUAGE_STANDARD = gnu99; 362 | GCC_NO_COMMON_BLOCKS = YES; 363 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 364 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 365 | GCC_WARN_UNDECLARED_SELECTOR = YES; 366 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 367 | GCC_WARN_UNUSED_FUNCTION = YES; 368 | GCC_WARN_UNUSED_VARIABLE = YES; 369 | IPHONEOS_DEPLOYMENT_TARGET = 10.1; 370 | MTL_ENABLE_DEBUG_INFO = NO; 371 | SDKROOT = iphoneos; 372 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 373 | SWIFT_VERSION = 4.2; 374 | TARGETED_DEVICE_FAMILY = "1,2"; 375 | VALIDATE_PRODUCT = YES; 376 | }; 377 | name = Release; 378 | }; 379 | D869F5E51DE622CD00CDAFAF /* Debug */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 383 | DEVELOPMENT_TEAM = 46UKZ786J3; 384 | INFOPLIST_FILE = HeckelDiffExample/Info.plist; 385 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 386 | PRODUCT_BUNDLE_IDENTIFIER = "com.matiascudich.HeckelDiffExample-iOS"; 387 | PRODUCT_NAME = "$(TARGET_NAME)"; 388 | }; 389 | name = Debug; 390 | }; 391 | D869F5E61DE622CD00CDAFAF /* Release */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 395 | DEVELOPMENT_TEAM = 46UKZ786J3; 396 | INFOPLIST_FILE = HeckelDiffExample/Info.plist; 397 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 398 | PRODUCT_BUNDLE_IDENTIFIER = "com.matiascudich.HeckelDiffExample-iOS"; 399 | PRODUCT_NAME = "$(TARGET_NAME)"; 400 | }; 401 | name = Release; 402 | }; 403 | /* End XCBuildConfiguration section */ 404 | 405 | /* Begin XCConfigurationList section */ 406 | 3A16935220EDCF2B005E2034 /* Build configuration list for PBXNativeTarget "HeckelDiffExample_tvOS" */ = { 407 | isa = XCConfigurationList; 408 | buildConfigurations = ( 409 | 3A16935320EDCF2B005E2034 /* Debug */, 410 | 3A16935420EDCF2B005E2034 /* Release */, 411 | ); 412 | defaultConfigurationIsVisible = 0; 413 | defaultConfigurationName = Release; 414 | }; 415 | D869F5CD1DE622CD00CDAFAF /* Build configuration list for PBXProject "HeckelDiffExample" */ = { 416 | isa = XCConfigurationList; 417 | buildConfigurations = ( 418 | D869F5E21DE622CD00CDAFAF /* Debug */, 419 | D869F5E31DE622CD00CDAFAF /* Release */, 420 | ); 421 | defaultConfigurationIsVisible = 0; 422 | defaultConfigurationName = Release; 423 | }; 424 | D869F5E41DE622CD00CDAFAF /* Build configuration list for PBXNativeTarget "HeckelDiffExample_iOS" */ = { 425 | isa = XCConfigurationList; 426 | buildConfigurations = ( 427 | D869F5E51DE622CD00CDAFAF /* Debug */, 428 | D869F5E61DE622CD00CDAFAF /* Release */, 429 | ); 430 | defaultConfigurationIsVisible = 0; 431 | defaultConfigurationName = Release; 432 | }; 433 | /* End XCConfigurationList section */ 434 | }; 435 | rootObject = D869F5CA1DE622CD00CDAFAF /* Project object */; 436 | } 437 | -------------------------------------------------------------------------------- /Examples/HeckelDiffExample/HeckelDiffExample/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 | UIRequiredDeviceCapabilities 24 | 25 | armv7 26 | 27 | UISupportedInterfaceOrientations 28 | 29 | UIInterfaceOrientationPortrait 30 | UIInterfaceOrientationLandscapeLeft 31 | UIInterfaceOrientationLandscapeRight 32 | 33 | UISupportedInterfaceOrientations~ipad 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationPortraitUpsideDown 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Examples/HeckelDiffExample/HeckelDiffExample_tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | UIUserInterfaceStyle 28 | Automatic 29 | 30 | 31 | -------------------------------------------------------------------------------- /Examples/HeckelDiffExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // HeckelDiffExample 4 | // 5 | // Created by Matias Cudich on 11/23/16. 6 | // Copyright © 2016 Matias Cudich. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import HeckelDiff 11 | 12 | class ViewController: UIViewController, UITableViewDataSource { 13 | lazy var tableView: UITableView = { 14 | let tableView = UITableView(frame: CGRect.zero, style: .plain) 15 | tableView.dataSource = self 16 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 17 | return tableView 18 | }() 19 | 20 | var items = ["1", "2", "3", "4", "5"] 21 | 22 | override func loadView() { 23 | view = tableView 24 | } 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | refresh() 30 | } 31 | 32 | func refresh() { 33 | let previousItems = items 34 | let last = items.removeLast() 35 | items.insert(last, at: 0) 36 | 37 | tableView.applyDiff(previousItems, items, inSection: 0, withAnimation: .right) 38 | 39 | let time = DispatchTime.now() + DispatchTimeInterval.seconds(1) 40 | DispatchQueue.main.asyncAfter(deadline: time) { 41 | self.refresh() 42 | } 43 | } 44 | 45 | func numberOfSections(in tableView: UITableView) -> Int { 46 | return 1 47 | } 48 | 49 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 50 | return items.count 51 | } 52 | 53 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 54 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 55 | cell.textLabel?.text = items[indexPath.row] 56 | return cell 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /HeckelDiff.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "HeckelDiff" 3 | s.version = "0.2.4" 4 | s.summary = "Pure Swift implementation of Paul Heckel's \"A Technique for Isolating Differences Between Files\"" 5 | s.description = "Given two collections, provides a very efficient set of steps to transform one into the other. Adds support for UITableView and UICollectionView batched updates." 6 | 7 | s.homepage = "https://github.com/mcudich/HeckelDiff" 8 | s.license = "MIT" 9 | s.author = { "Matias Cudich" => "mcudich@gmail.com" } 10 | s.source = { :git => "https://github.com/mcudich/HeckelDiff.git", :tag => s.version.to_s } 11 | s.social_media_url = "https://twitter.com/mcudich" 12 | 13 | s.ios.deployment_target = "8.0" 14 | s.tvos.deployment_target = "9.0" 15 | 16 | s.swift_version = '4.2' 17 | 18 | s.source_files = "Source/**/*.{h,m,swift}" 19 | end 20 | -------------------------------------------------------------------------------- /HeckelDiff.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D829E61D1DE5039500560BD4 /* HeckelDiff.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D829E6131DE5039500560BD4 /* HeckelDiff.framework */; }; 11 | D829E6241DE5039500560BD4 /* HeckelDiff.h in Headers */ = {isa = PBXBuildFile; fileRef = D829E6161DE5039500560BD4 /* HeckelDiff.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | D829E62E1DE5047600560BD4 /* Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = D829E62D1DE5047600560BD4 /* Diff.swift */; }; 13 | D829E6301DE504A400560BD4 /* DiffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D829E62F1DE504A400560BD4 /* DiffTests.swift */; }; 14 | D869F5C31DE61F4400CDAFAF /* UICollectionView+Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = D869F5C21DE61F4400CDAFAF /* UICollectionView+Diff.swift */; }; 15 | D869F5C51DE61FDC00CDAFAF /* ListUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D869F5C41DE61FDC00CDAFAF /* ListUpdate.swift */; }; 16 | D8C71BDD1DE61A2200EB6B20 /* UITableView+Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C71BDC1DE61A2200EB6B20 /* UITableView+Diff.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXContainerItemProxy section */ 20 | D829E61E1DE5039500560BD4 /* PBXContainerItemProxy */ = { 21 | isa = PBXContainerItemProxy; 22 | containerPortal = D829E60A1DE5039500560BD4 /* Project object */; 23 | proxyType = 1; 24 | remoteGlobalIDString = D829E6121DE5039500560BD4; 25 | remoteInfo = HeckelDiff; 26 | }; 27 | /* End PBXContainerItemProxy section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | D829E6131DE5039500560BD4 /* HeckelDiff.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = HeckelDiff.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | D829E6161DE5039500560BD4 /* HeckelDiff.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HeckelDiff.h; sourceTree = ""; }; 32 | D829E6171DE5039500560BD4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 33 | D829E61C1DE5039500560BD4 /* HeckelDiffTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HeckelDiffTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | D829E6231DE5039500560BD4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | D829E62D1DE5047600560BD4 /* Diff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Diff.swift; sourceTree = ""; }; 36 | D829E62F1DE504A400560BD4 /* DiffTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffTests.swift; sourceTree = ""; }; 37 | D869F5C21DE61F4400CDAFAF /* UICollectionView+Diff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Diff.swift"; sourceTree = ""; }; 38 | D869F5C41DE61FDC00CDAFAF /* ListUpdate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListUpdate.swift; sourceTree = ""; }; 39 | D8C71BDC1DE61A2200EB6B20 /* UITableView+Diff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+Diff.swift"; sourceTree = ""; }; 40 | /* End PBXFileReference section */ 41 | 42 | /* Begin PBXFrameworksBuildPhase section */ 43 | D829E60F1DE5039500560BD4 /* Frameworks */ = { 44 | isa = PBXFrameworksBuildPhase; 45 | buildActionMask = 2147483647; 46 | files = ( 47 | ); 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | D829E6191DE5039500560BD4 /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | D829E61D1DE5039500560BD4 /* HeckelDiff.framework in Frameworks */, 55 | ); 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXFrameworksBuildPhase section */ 59 | 60 | /* Begin PBXGroup section */ 61 | D829E6091DE5039500560BD4 = { 62 | isa = PBXGroup; 63 | children = ( 64 | D829E6151DE5039500560BD4 /* Source */, 65 | D829E6201DE5039500560BD4 /* Tests */, 66 | D829E6141DE5039500560BD4 /* Products */, 67 | ); 68 | sourceTree = ""; 69 | }; 70 | D829E6141DE5039500560BD4 /* Products */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | D829E6131DE5039500560BD4 /* HeckelDiff.framework */, 74 | D829E61C1DE5039500560BD4 /* HeckelDiffTests.xctest */, 75 | ); 76 | name = Products; 77 | sourceTree = ""; 78 | }; 79 | D829E6151DE5039500560BD4 /* Source */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | D829E6161DE5039500560BD4 /* HeckelDiff.h */, 83 | D829E6171DE5039500560BD4 /* Info.plist */, 84 | D829E62D1DE5047600560BD4 /* Diff.swift */, 85 | D8C71BDC1DE61A2200EB6B20 /* UITableView+Diff.swift */, 86 | D869F5C21DE61F4400CDAFAF /* UICollectionView+Diff.swift */, 87 | D869F5C41DE61FDC00CDAFAF /* ListUpdate.swift */, 88 | ); 89 | path = Source; 90 | sourceTree = ""; 91 | }; 92 | D829E6201DE5039500560BD4 /* Tests */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | D829E6231DE5039500560BD4 /* Info.plist */, 96 | D829E62F1DE504A400560BD4 /* DiffTests.swift */, 97 | ); 98 | path = Tests; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXHeadersBuildPhase section */ 104 | D829E6101DE5039500560BD4 /* Headers */ = { 105 | isa = PBXHeadersBuildPhase; 106 | buildActionMask = 2147483647; 107 | files = ( 108 | D829E6241DE5039500560BD4 /* HeckelDiff.h in Headers */, 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | /* End PBXHeadersBuildPhase section */ 113 | 114 | /* Begin PBXNativeTarget section */ 115 | D829E6121DE5039500560BD4 /* HeckelDiff */ = { 116 | isa = PBXNativeTarget; 117 | buildConfigurationList = D829E6271DE5039500560BD4 /* Build configuration list for PBXNativeTarget "HeckelDiff" */; 118 | buildPhases = ( 119 | D829E60E1DE5039500560BD4 /* Sources */, 120 | D829E60F1DE5039500560BD4 /* Frameworks */, 121 | D829E6101DE5039500560BD4 /* Headers */, 122 | D829E6111DE5039500560BD4 /* Resources */, 123 | ); 124 | buildRules = ( 125 | ); 126 | dependencies = ( 127 | ); 128 | name = HeckelDiff; 129 | productName = HeckelDiff; 130 | productReference = D829E6131DE5039500560BD4 /* HeckelDiff.framework */; 131 | productType = "com.apple.product-type.framework"; 132 | }; 133 | D829E61B1DE5039500560BD4 /* HeckelDiffTests */ = { 134 | isa = PBXNativeTarget; 135 | buildConfigurationList = D829E62A1DE5039500560BD4 /* Build configuration list for PBXNativeTarget "HeckelDiffTests" */; 136 | buildPhases = ( 137 | D829E6181DE5039500560BD4 /* Sources */, 138 | D829E6191DE5039500560BD4 /* Frameworks */, 139 | D829E61A1DE5039500560BD4 /* Resources */, 140 | ); 141 | buildRules = ( 142 | ); 143 | dependencies = ( 144 | D829E61F1DE5039500560BD4 /* PBXTargetDependency */, 145 | ); 146 | name = HeckelDiffTests; 147 | productName = HeckelDiffTests; 148 | productReference = D829E61C1DE5039500560BD4 /* HeckelDiffTests.xctest */; 149 | productType = "com.apple.product-type.bundle.unit-test"; 150 | }; 151 | /* End PBXNativeTarget section */ 152 | 153 | /* Begin PBXProject section */ 154 | D829E60A1DE5039500560BD4 /* Project object */ = { 155 | isa = PBXProject; 156 | attributes = { 157 | LastSwiftUpdateCheck = 0810; 158 | LastUpgradeCheck = 1010; 159 | ORGANIZATIONNAME = "Matias Cudich"; 160 | TargetAttributes = { 161 | D829E6121DE5039500560BD4 = { 162 | CreatedOnToolsVersion = 8.1; 163 | LastSwiftMigration = 0810; 164 | ProvisioningStyle = Manual; 165 | }; 166 | D829E61B1DE5039500560BD4 = { 167 | CreatedOnToolsVersion = 8.1; 168 | LastSwiftMigration = 0810; 169 | ProvisioningStyle = Manual; 170 | }; 171 | }; 172 | }; 173 | buildConfigurationList = D829E60D1DE5039500560BD4 /* Build configuration list for PBXProject "HeckelDiff" */; 174 | compatibilityVersion = "Xcode 3.2"; 175 | developmentRegion = English; 176 | hasScannedForEncodings = 0; 177 | knownRegions = ( 178 | en, 179 | ); 180 | mainGroup = D829E6091DE5039500560BD4; 181 | productRefGroup = D829E6141DE5039500560BD4 /* Products */; 182 | projectDirPath = ""; 183 | projectRoot = ""; 184 | targets = ( 185 | D829E6121DE5039500560BD4 /* HeckelDiff */, 186 | D829E61B1DE5039500560BD4 /* HeckelDiffTests */, 187 | ); 188 | }; 189 | /* End PBXProject section */ 190 | 191 | /* Begin PBXResourcesBuildPhase section */ 192 | D829E6111DE5039500560BD4 /* Resources */ = { 193 | isa = PBXResourcesBuildPhase; 194 | buildActionMask = 2147483647; 195 | files = ( 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | D829E61A1DE5039500560BD4 /* Resources */ = { 200 | isa = PBXResourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | /* End PBXResourcesBuildPhase section */ 207 | 208 | /* Begin PBXSourcesBuildPhase section */ 209 | D829E60E1DE5039500560BD4 /* Sources */ = { 210 | isa = PBXSourcesBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | D8C71BDD1DE61A2200EB6B20 /* UITableView+Diff.swift in Sources */, 214 | D869F5C31DE61F4400CDAFAF /* UICollectionView+Diff.swift in Sources */, 215 | D869F5C51DE61FDC00CDAFAF /* ListUpdate.swift in Sources */, 216 | D829E62E1DE5047600560BD4 /* Diff.swift in Sources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | D829E6181DE5039500560BD4 /* Sources */ = { 221 | isa = PBXSourcesBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | D829E6301DE504A400560BD4 /* DiffTests.swift in Sources */, 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | }; 228 | /* End PBXSourcesBuildPhase section */ 229 | 230 | /* Begin PBXTargetDependency section */ 231 | D829E61F1DE5039500560BD4 /* PBXTargetDependency */ = { 232 | isa = PBXTargetDependency; 233 | target = D829E6121DE5039500560BD4 /* HeckelDiff */; 234 | targetProxy = D829E61E1DE5039500560BD4 /* PBXContainerItemProxy */; 235 | }; 236 | /* End PBXTargetDependency section */ 237 | 238 | /* Begin XCBuildConfiguration section */ 239 | D829E6251DE5039500560BD4 /* Debug */ = { 240 | isa = XCBuildConfiguration; 241 | buildSettings = { 242 | ALWAYS_SEARCH_USER_PATHS = NO; 243 | CLANG_ANALYZER_NONNULL = YES; 244 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 245 | CLANG_CXX_LIBRARY = "libc++"; 246 | CLANG_ENABLE_MODULES = YES; 247 | CLANG_ENABLE_OBJC_ARC = YES; 248 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 249 | CLANG_WARN_BOOL_CONVERSION = YES; 250 | CLANG_WARN_COMMA = YES; 251 | CLANG_WARN_CONSTANT_CONVERSION = YES; 252 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 270 | COPY_PHASE_STRIP = NO; 271 | CURRENT_PROJECT_VERSION = 1; 272 | DEBUG_INFORMATION_FORMAT = dwarf; 273 | ENABLE_STRICT_OBJC_MSGSEND = YES; 274 | ENABLE_TESTABILITY = YES; 275 | GCC_C_LANGUAGE_STANDARD = gnu99; 276 | GCC_DYNAMIC_NO_PIC = NO; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_OPTIMIZATION_LEVEL = 0; 279 | GCC_PREPROCESSOR_DEFINITIONS = ( 280 | "DEBUG=1", 281 | "$(inherited)", 282 | ); 283 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 284 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 285 | GCC_WARN_UNDECLARED_SELECTOR = YES; 286 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 287 | GCC_WARN_UNUSED_FUNCTION = YES; 288 | GCC_WARN_UNUSED_VARIABLE = YES; 289 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 290 | MACOSX_DEPLOYMENT_TARGET = 10.10; 291 | MTL_ENABLE_DEBUG_INFO = YES; 292 | ONLY_ACTIVE_ARCH = YES; 293 | SDKROOT = iphoneos; 294 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos watchsimulator watchos appletvsimulator appletvos macosx"; 295 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 296 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 297 | TARGETED_DEVICE_FAMILY = "1,2,3,4"; 298 | TVOS_DEPLOYMENT_TARGET = 9.0; 299 | VERSIONING_SYSTEM = "apple-generic"; 300 | VERSION_INFO_PREFIX = ""; 301 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 302 | }; 303 | name = Debug; 304 | }; 305 | D829E6261DE5039500560BD4 /* Release */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ALWAYS_SEARCH_USER_PATHS = NO; 309 | CLANG_ANALYZER_NONNULL = YES; 310 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 311 | CLANG_CXX_LIBRARY = "libc++"; 312 | CLANG_ENABLE_MODULES = YES; 313 | CLANG_ENABLE_OBJC_ARC = YES; 314 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 315 | CLANG_WARN_BOOL_CONVERSION = YES; 316 | CLANG_WARN_COMMA = YES; 317 | CLANG_WARN_CONSTANT_CONVERSION = YES; 318 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 319 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 320 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 321 | CLANG_WARN_EMPTY_BODY = YES; 322 | CLANG_WARN_ENUM_CONVERSION = YES; 323 | CLANG_WARN_INFINITE_RECURSION = YES; 324 | CLANG_WARN_INT_CONVERSION = YES; 325 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 326 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 327 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 328 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 329 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 330 | CLANG_WARN_STRICT_PROTOTYPES = YES; 331 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 332 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 333 | CLANG_WARN_UNREACHABLE_CODE = YES; 334 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 335 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 336 | COPY_PHASE_STRIP = NO; 337 | CURRENT_PROJECT_VERSION = 1; 338 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 339 | ENABLE_NS_ASSERTIONS = NO; 340 | ENABLE_STRICT_OBJC_MSGSEND = YES; 341 | GCC_C_LANGUAGE_STANDARD = gnu99; 342 | GCC_NO_COMMON_BLOCKS = YES; 343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 345 | GCC_WARN_UNDECLARED_SELECTOR = YES; 346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 347 | GCC_WARN_UNUSED_FUNCTION = YES; 348 | GCC_WARN_UNUSED_VARIABLE = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 350 | MACOSX_DEPLOYMENT_TARGET = 10.10; 351 | MTL_ENABLE_DEBUG_INFO = NO; 352 | SDKROOT = iphoneos; 353 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos watchsimulator watchos appletvsimulator appletvos macosx"; 354 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 355 | TARGETED_DEVICE_FAMILY = "1,2,3,4"; 356 | TVOS_DEPLOYMENT_TARGET = 9.0; 357 | VALIDATE_PRODUCT = YES; 358 | VERSIONING_SYSTEM = "apple-generic"; 359 | VERSION_INFO_PREFIX = ""; 360 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 361 | }; 362 | name = Release; 363 | }; 364 | D829E6281DE5039500560BD4 /* Debug */ = { 365 | isa = XCBuildConfiguration; 366 | buildSettings = { 367 | CLANG_ENABLE_MODULES = YES; 368 | CODE_SIGN_IDENTITY = ""; 369 | CODE_SIGN_STYLE = Manual; 370 | DEFINES_MODULE = YES; 371 | DEVELOPMENT_TEAM = ""; 372 | DYLIB_COMPATIBILITY_VERSION = 1; 373 | DYLIB_CURRENT_VERSION = 1; 374 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 375 | INFOPLIST_FILE = Source/Info.plist; 376 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 377 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 378 | PRODUCT_BUNDLE_IDENTIFIER = com.matiascudich.HeckelDiff; 379 | PRODUCT_NAME = "$(TARGET_NAME)"; 380 | PROVISIONING_PROFILE_SPECIFIER = ""; 381 | SKIP_INSTALL = YES; 382 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 383 | SWIFT_VERSION = 4.2; 384 | }; 385 | name = Debug; 386 | }; 387 | D829E6291DE5039500560BD4 /* Release */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | CLANG_ENABLE_MODULES = YES; 391 | CODE_SIGN_IDENTITY = ""; 392 | CODE_SIGN_STYLE = Manual; 393 | DEFINES_MODULE = YES; 394 | DEVELOPMENT_TEAM = ""; 395 | DYLIB_COMPATIBILITY_VERSION = 1; 396 | DYLIB_CURRENT_VERSION = 1; 397 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 398 | INFOPLIST_FILE = Source/Info.plist; 399 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 400 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 401 | PRODUCT_BUNDLE_IDENTIFIER = com.matiascudich.HeckelDiff; 402 | PRODUCT_NAME = "$(TARGET_NAME)"; 403 | PROVISIONING_PROFILE_SPECIFIER = ""; 404 | SKIP_INSTALL = YES; 405 | SWIFT_VERSION = 4.2; 406 | }; 407 | name = Release; 408 | }; 409 | D829E62B1DE5039500560BD4 /* Debug */ = { 410 | isa = XCBuildConfiguration; 411 | buildSettings = { 412 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 413 | CLANG_ENABLE_MODULES = YES; 414 | CODE_SIGN_STYLE = Manual; 415 | DEVELOPMENT_TEAM = ""; 416 | INFOPLIST_FILE = Tests/Info.plist; 417 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 418 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 419 | PRODUCT_BUNDLE_IDENTIFIER = com.matiascudich.HeckelDiffTests; 420 | PRODUCT_NAME = "$(TARGET_NAME)"; 421 | PROVISIONING_PROFILE_SPECIFIER = ""; 422 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 423 | SWIFT_VERSION = 4.2; 424 | }; 425 | name = Debug; 426 | }; 427 | D829E62C1DE5039500560BD4 /* Release */ = { 428 | isa = XCBuildConfiguration; 429 | buildSettings = { 430 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 431 | CLANG_ENABLE_MODULES = YES; 432 | CODE_SIGN_STYLE = Manual; 433 | DEVELOPMENT_TEAM = ""; 434 | INFOPLIST_FILE = Tests/Info.plist; 435 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 436 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 437 | PRODUCT_BUNDLE_IDENTIFIER = com.matiascudich.HeckelDiffTests; 438 | PRODUCT_NAME = "$(TARGET_NAME)"; 439 | PROVISIONING_PROFILE_SPECIFIER = ""; 440 | SWIFT_VERSION = 4.2; 441 | }; 442 | name = Release; 443 | }; 444 | /* End XCBuildConfiguration section */ 445 | 446 | /* Begin XCConfigurationList section */ 447 | D829E60D1DE5039500560BD4 /* Build configuration list for PBXProject "HeckelDiff" */ = { 448 | isa = XCConfigurationList; 449 | buildConfigurations = ( 450 | D829E6251DE5039500560BD4 /* Debug */, 451 | D829E6261DE5039500560BD4 /* Release */, 452 | ); 453 | defaultConfigurationIsVisible = 0; 454 | defaultConfigurationName = Release; 455 | }; 456 | D829E6271DE5039500560BD4 /* Build configuration list for PBXNativeTarget "HeckelDiff" */ = { 457 | isa = XCConfigurationList; 458 | buildConfigurations = ( 459 | D829E6281DE5039500560BD4 /* Debug */, 460 | D829E6291DE5039500560BD4 /* Release */, 461 | ); 462 | defaultConfigurationIsVisible = 0; 463 | defaultConfigurationName = Release; 464 | }; 465 | D829E62A1DE5039500560BD4 /* Build configuration list for PBXNativeTarget "HeckelDiffTests" */ = { 466 | isa = XCConfigurationList; 467 | buildConfigurations = ( 468 | D829E62B1DE5039500560BD4 /* Debug */, 469 | D829E62C1DE5039500560BD4 /* Release */, 470 | ); 471 | defaultConfigurationIsVisible = 0; 472 | defaultConfigurationName = Release; 473 | }; 474 | /* End XCConfigurationList section */ 475 | }; 476 | rootObject = D829E60A1DE5039500560BD4 /* Project object */; 477 | } 478 | -------------------------------------------------------------------------------- /HeckelDiff.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /HeckelDiff.xcodeproj/xcshareddata/xcschemes/HeckelDiff.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /HeckelDiff.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /HeckelDiff.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Matias Cudich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "HeckelDiff", 8 | products: [ 9 | .library(name: "HeckelDiff", targets: ["HeckelDiff"]) 10 | ], 11 | targets: [ 12 | .target(name: "HeckelDiff", path: "Source") 13 | ], 14 | swiftLanguageVersions: [.v5, .v4_2] 15 | ) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HeckelDiff 2 | [![Swift](https://img.shields.io/badge/swift-3-orange.svg?style=flat)](#) 3 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 4 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/HeckelDiff.svg)](https://img.shields.io/cocoapods/v/HeckelDiff) 5 | [![Platform](https://img.shields.io/cocoapods/p/HeckelDiff.svg?style=flat)](http://cocoadocs.org/docsets/HeckelDiff) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT) 7 | 8 | Pure Swift implementation of Paul Heckel's *A Technique for Isolating Differences Between Files* 9 | 10 | ## Features 11 | 12 | This is a simple diff algorithm that provides the minimum set of steps to transform one collection into another. Transformations are listed as discrete operations: 13 | * **Insertion** - what items should be inserted into the array, and at what index. 14 | * **Deletion** - what items should be removed from the array, and at what index. 15 | * **Move** - what items should be moved, and their origin and destination indices. 16 | * **Update** - what items should be updated/replaced with new context, and at what index. 17 | 18 | These operations are calculated in **linear** time, using the algorithm described in [this paper](http://dl.acm.org/citation.cfm?id=359467). 19 | 20 | Knowing this set of operations is especially handy for efficiently updating **UITableViews** and **UICollectionViews**. 21 | 22 | ## Example 23 | 24 | Consider a simple example that compares lists of integers: 25 | ```swift 26 | let o = [1, 2, 3, 3, 4] 27 | let n = [2, 3, 1, 3, 4] 28 | let result = diff(o, n) 29 | // [.move(1, 0), .move(2, 1), .move(0, 2)] 30 | 31 | let o = [0, 1, 2, 3, 4, 5, 6, 7, 8] 32 | let n = [0, 2, 3, 4, 7, 6, 9, 5, 10] 33 | let result = diff(o, n) 34 | // [.delete(1), .delete(8), .move(7, 4), .insert(6), .move(5, 7), .insert(8)] 35 | ``` 36 | 37 | `orderedDiff` is also available, which provides a set of operations that are friendly for batched updates in UIKit contexts (note how `move` is replaced by pairs of `insert` and `delete` operations): 38 | ```swift 39 | let o = [1, 2, 3, 3, 4] 40 | let n = [2, 3, 1, 3, 4] 41 | let result = orderedDiff(o, n) 42 | // [.delete(2), .delete(1), .delete(0), .insert(0), .insert(1), .insert(2)] 43 | ``` 44 | 45 | ## UITableView/UICollectionView Support 46 | 47 | HeckelDiff has built-in support for generating efficient batched updates for `UITableView` and `UICollectionView`. Methods are made available on both that allow informing the corresponding table or collection view that their data model has changed. 48 | 49 | For example: 50 | ```swift 51 | tableView.applyDiff(previousItems, newItems, withAnimation: .fade) 52 | ``` 53 | or 54 | ```swift 55 | collectionView.applyDiff(previousItems, newItems) 56 | ``` 57 | 58 | ## Update Support 59 | 60 | Elements in collections passed into `diff` must conform to `Hashable`. HeckelDiff uses elements' `hashValues` to determine whether they should be **inserted**, **deleted** or **moved**. In some cases, elements are instead marked for **update**. This is because even though the `hashValues` of two elements might be equivalent, the elements may not be **equal**. You can take advantage of this by implementing the `Hashable` protocol in such a way that your elements get updated when appropriate. 61 | 62 | For example, you may have two records that refer to the same person (perhaps you use a record ID as a hash value). You may want to support a case where a person's phone number may change, but the record itself remains in the same position in the array. Your `Equatable` implementation may take the phone number value into account, whereas your `hashValue` may only reflect the underlying record ID value. 63 | 64 | In the context of a `UITableView` or `UICollectionView`, you would most efficiently handle this by reloading the given row that needs updating (rather than deleting it and re-inserting it). Use the supplied `applyDiff` functions to have HeckelDiff perform this for you. 65 | 66 | ## Installation 67 | 68 | #### Carthage 69 | 70 | You can install Carthage with [Homebrew](http://brew.sh/) using the following command: 71 | 72 | ```bash 73 | $ brew update 74 | $ brew install carthage 75 | ``` 76 | 77 | Add the following line to your [Cartfile](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#cartfile): 78 | 79 | ```ogdl 80 | github "mcudich/HeckelDiff" 81 | ``` 82 | 83 | Run `carthage update`, then make sure to add `HeckelDiff.framework` to "Linked Frameworks and Libraries" and "copy-frameworks" Build Phases. 84 | 85 | #### CocoaPods 86 | 87 | [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: 88 | 89 | ```bash 90 | $ gem install cocoapods 91 | ``` 92 | 93 | To integrate TemplateKit into your Xcode project using CocoaPods, specify it in your `Podfile`: 94 | 95 | ```ruby 96 | source 'https://github.com/CocoaPods/Specs.git' 97 | platform :ios, '10.0' 98 | use_frameworks! 99 | 100 | target '' do 101 | pod 'HeckelDiff', '~> 0.1.0' 102 | end 103 | ``` 104 | 105 | Then, run the following command: 106 | 107 | ```bash 108 | $ pod install 109 | ``` 110 | 111 | ## Requirements 112 | 113 | - iOS 9.0+ 114 | - Xcode 8.0+ 115 | - Swift 3.0+ 116 | -------------------------------------------------------------------------------- /Source/Diff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Diff.swift 3 | // HeckelDiff 4 | // 5 | // Created by Matias Cudich on 11/22/16. 6 | // Copyright © 2016 Matias Cudich. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Used to represent the operation to perform on the source array. Indices indicate the position at 12 | /// which to perform the given operation. 13 | /// 14 | /// - insert: Insert a new value at the given index. 15 | /// - delete: Delete a value at the given index. 16 | /// - move: Move a value from the given origin index, to the given destination index. 17 | /// - update: Update the value at the given index. 18 | public enum Operation: Equatable { 19 | case insert(Int) 20 | case delete(Int) 21 | case move(Int, Int) 22 | case update(Int) 23 | 24 | public static func ==(lhs: Operation, rhs: Operation) -> Bool { 25 | switch (lhs, rhs) { 26 | case let (.insert(l), .insert(r)), 27 | let (.delete(l), .delete(r)), 28 | let (.update(l), .update(r)): return l == r 29 | case let (.move(l1,l2), .move(r1,r2)): return l1 == r1 && l2 == r2 30 | default: return false 31 | } 32 | } 33 | } 34 | 35 | enum Counter { 36 | case zero 37 | case one 38 | case many 39 | 40 | mutating func increment() { 41 | switch self { 42 | case .zero: 43 | self = .one 44 | case .one: 45 | self = .many 46 | case .many: 47 | break 48 | } 49 | } 50 | } 51 | 52 | class SymbolEntry { 53 | var oc: Counter = .zero 54 | var nc: Counter = .zero 55 | var olno = [Int]() 56 | 57 | var occursInBoth: Bool { 58 | return oc != .zero && nc != .zero 59 | } 60 | } 61 | 62 | enum Entry { 63 | case symbol(SymbolEntry) 64 | case index(Int) 65 | } 66 | 67 | /// Returns a diff, given an old and a new representation of a given collection (such as an `Array`). 68 | /// The return value is a list of `Operation` values, each which instructs how to transform the old 69 | /// collection into the new collection. 70 | /// 71 | /// - parameter old: The old collection. 72 | /// - parameter new: The new collection. 73 | /// - returns: A list of `Operation` values, representing the diff. 74 | /// 75 | /// Based on http://dl.acm.org/citation.cfm?id=359467. 76 | /// 77 | /// And other similar implementations at: 78 | /// * https://github.com/Instagram/IGListKit 79 | /// * https://github.com/andre-alves/PHDiff 80 | public func diff(_ old: T, _ new: T) -> [Operation] where T.Iterator.Element: Hashable, T.Index == Int { 81 | var table = [Int: SymbolEntry]() 82 | var oa = [Entry]() 83 | var na = [Entry]() 84 | 85 | // Pass 1 comprises the following: (a) each line i of file N is read in sequence; (b) a symbol 86 | // table entry for each line i is created if it does not already exist; (c) NC for the line's 87 | // symbol table entry is incremented; and (d) NA [i] is set to point to the symbol table entry of 88 | // line i. 89 | for item in new { 90 | let entry = table[item.hashValue] ?? SymbolEntry() 91 | table[item.hashValue] = entry 92 | entry.nc.increment() 93 | na.append(.symbol(entry)) 94 | } 95 | 96 | // Pass 2 is identical to pass 1 except that it acts on file O, array OA, and counter OC, 97 | // and OLNO for the symbol table entry is set to the line's number. 98 | for (index, item) in old.enumerated() { 99 | let entry = table[item.hashValue] ?? SymbolEntry() 100 | table[item.hashValue] = entry 101 | entry.oc.increment() 102 | entry.olno.append(index) 103 | oa.append(.symbol(entry)) 104 | } 105 | 106 | // In pass 3 we use observation 1 and process only those lines having NC = OC = 1. Since each 107 | // represents (we assume) the same unmodified line, for each we replace the symbol table pointers 108 | // in NA and OA by the number of the line in the other file. For example, if NA[i] corresponds to 109 | // such a line, we look NA[i] up in the symbol table and set NA[i] to OLNO and OA[OLNO] to i. 110 | // In pass 3 we also "find" unique virtual lines immediately before the first and immediately 111 | // after the last lines of the files. 112 | for (index, item) in na.enumerated() { 113 | if case let .symbol(entry) = item, entry.occursInBoth, !entry.olno.isEmpty { 114 | 115 | let oldIndex = entry.olno.removeFirst() 116 | na[index] = .index(oldIndex) 117 | oa[oldIndex] = .index(index) 118 | } 119 | } 120 | 121 | // In pass 4, we apply observation 2 and process each line in NA in ascending order: If NA[i] 122 | // points to OA[j] and NA[i + 1] and OA[j + 1] contain identical symbol table entry pointers, then 123 | // OA[j + 1] is set to line i + 1 and NA[i + 1] is set to line j + 1. 124 | var i = 1 125 | while i < na.count - 1 { 126 | if case let .index(j) = na[i], j + 1 < oa.count, 127 | case let .symbol(newEntry) = na[i + 1], 128 | case let .symbol(oldEntry) = oa[j + 1], newEntry === oldEntry { 129 | na[i + 1] = .index(j + 1) 130 | oa[j + 1] = .index(i + 1) 131 | } 132 | 133 | i += 1 134 | } 135 | 136 | // In pass 5, we also apply observation 2 and process each entry in descending order: if NA[i] 137 | // points to OA[j] and NA[i - 1] and OA[j - 1] contain identical symbol table pointers, then 138 | // NA[i - 1] is replaced by j - 1 and OA[j - 1] is replaced by i - 1. 139 | i = na.count - 1 140 | while i > 0 { 141 | if case let .index(j) = na[i], j - 1 >= 0, 142 | case let .symbol(newEntry) = na[i - 1], 143 | case let .symbol(oldEntry) = oa[j - 1], newEntry === oldEntry { 144 | na[i - 1] = .index(j - 1) 145 | oa[j - 1] = .index(i - 1) 146 | } 147 | 148 | i -= 1 149 | } 150 | 151 | var steps = [Operation]() 152 | 153 | var deleteOffsets = Array(repeating: 0, count: old.count) 154 | var runningOffset = 0 155 | for (index, item) in oa.enumerated() { 156 | deleteOffsets[index] = runningOffset 157 | if case .symbol = item { 158 | steps.append(.delete(index)) 159 | runningOffset += 1 160 | } 161 | } 162 | 163 | runningOffset = 0 164 | 165 | for (index, item) in na.enumerated() { 166 | switch item { 167 | case .symbol: 168 | steps.append(.insert(index)) 169 | runningOffset += 1 170 | case let .index(oldIndex): 171 | // The object has changed, so it should be updated. 172 | if old[oldIndex] != new[index] { 173 | steps.append(.update(index)) 174 | } 175 | 176 | let deleteOffset = deleteOffsets[oldIndex] 177 | // The object is not at the expected position, so move it. 178 | if (oldIndex - deleteOffset + runningOffset) != index { 179 | steps.append(.move(oldIndex, index)) 180 | } 181 | } 182 | } 183 | 184 | return steps 185 | } 186 | 187 | /// Similar to to `diff`, except that this returns the same set of operations but in an order that 188 | /// can be applied step-wise to transform the old array into the new one. 189 | /// 190 | /// - parameter old: The old collection. 191 | /// - parameter new: The new collection. 192 | /// - returns: A list of `Operation` values, representing the diff. 193 | public func orderedDiff(_ old: T, _ new: T) -> [Operation] where T.Iterator.Element: Hashable, T.Index == Int { 194 | let steps = diff(old, new) 195 | 196 | var insertions = [Operation]() 197 | var updates = [Operation]() 198 | var possibleDeletions: [Operation?] = Array(repeating: nil, count: old.count) 199 | 200 | let trackDeletion = { (fromIndex: Int, step: Operation) in 201 | if possibleDeletions[fromIndex] == nil { 202 | possibleDeletions[fromIndex] = step 203 | } 204 | } 205 | 206 | for step in steps { 207 | switch step { 208 | case .insert: 209 | insertions.append(step) 210 | case let .delete(fromIndex): 211 | trackDeletion(fromIndex, step) 212 | case let .move(fromIndex, toIndex): 213 | insertions.append(.insert(toIndex)) 214 | trackDeletion(fromIndex, .delete(fromIndex)) 215 | case .update: 216 | updates.append(step) 217 | } 218 | } 219 | 220 | let deletions = possibleDeletions.compactMap { $0 }.reversed() 221 | 222 | return deletions + insertions + updates 223 | } 224 | -------------------------------------------------------------------------------- /Source/HeckelDiff.h: -------------------------------------------------------------------------------- 1 | // 2 | // HeckelDiff.h 3 | // HeckelDiff 4 | // 5 | // Created by Matias Cudich on 11/22/16. 6 | // Copyright © 2016 Matias Cudich. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for HeckelDiff. 12 | FOUNDATION_EXPORT double HeckelDiffVersionNumber; 13 | 14 | //! Project version string for HeckelDiff. 15 | FOUNDATION_EXPORT const unsigned char HeckelDiffVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Source/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 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Source/ListUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListUpdate.swift 3 | // HeckelDiff 4 | // 5 | // Created by Matias Cudich on 11/23/16. 6 | // Copyright © 2016 Matias Cudich. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | #if canImport(UIKit) 11 | import UIKit 12 | #elseif canImport(AppKit) 13 | import AppKit 14 | #endif 15 | 16 | public struct ListUpdate { 17 | public var deletions = [IndexPath]() 18 | public var insertions = [IndexPath]() 19 | public var updates = [IndexPath]() 20 | public var moves = [(from: IndexPath, to: IndexPath)]() 21 | 22 | public init(_ result: [Operation], _ section: Int) { 23 | for step in result { 24 | switch step { 25 | case .delete(let index): 26 | deletions.append(IndexPath(item: index, section: section)) 27 | case .insert(let index): 28 | insertions.append(IndexPath(item: index, section: section)) 29 | case .update(let index): 30 | updates.append(IndexPath(item: index, section: section)) 31 | case let .move(fromIndex, toIndex): 32 | moves.append((from: IndexPath(item: fromIndex, section: section), to: IndexPath(item: toIndex, section: section))) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Source/UICollectionView+Diff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Diff.swift 3 | // HeckelDiff 4 | // 5 | // Created by Matias Cudich on 11/23/16. 6 | // Copyright © 2016 Matias Cudich. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | import UIKit 12 | 13 | public extension UICollectionView { 14 | /// Applies a batch update to the receiver, efficiently reporting changes between old and new. 15 | /// 16 | /// - parameter old: The previous state of the collection view. 17 | /// - parameter new: The current state of the collection view. 18 | /// - parameter section: The section where these changes took place. 19 | /// - parameter reloadUpdated: Whether or not updated cells should be reloaded (default: true) 20 | func applyDiff(_ old: T, _ new: T, inSection section: Int, reloadUpdated: Bool = true, completion: ((Bool) -> Void)?) where T.Iterator.Element: Hashable, T.Index == Int { 21 | let update = ListUpdate(diff(old, new), section) 22 | 23 | performBatchUpdates({ 24 | self.deleteItems(at: update.deletions) 25 | self.insertItems(at: update.insertions) 26 | for move in update.moves { 27 | self.moveItem(at: move.from, to: move.to) 28 | } 29 | }, completion: reloadUpdated ? nil : completion) 30 | 31 | if reloadUpdated { 32 | // reloadItems is done separately as the update indexes returne by diff() are in respect to the 33 | // "after" state, but the collectionView.reloadItems() call wants the "before" indexPaths. 34 | performBatchUpdates({ 35 | self.reloadItems(at: update.updates) 36 | }, completion: completion) 37 | } 38 | } 39 | } 40 | #endif 41 | -------------------------------------------------------------------------------- /Source/UITableView+Diff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Diff.swift 3 | // HeckelDiff 4 | // 5 | // Created by Matias Cudich on 11/23/16. 6 | // Copyright © 2016 Matias Cudich. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | import UIKit 12 | 13 | public extension UITableView { 14 | /// Applies a batch update to the receiver, efficiently reporting changes between old and new. 15 | /// 16 | /// - parameter old: The previous state of the table view. 17 | /// - parameter new: The current state of the table view. 18 | /// - parameter section: The section where these changes took place. 19 | /// - parameter animation: The animation type. 20 | /// - parameter reloadUpdated: Whether or not updated cells should be reloaded (default: true) 21 | func applyDiff(_ old: T, _ new: T, inSection section: Int, withAnimation animation: UITableView.RowAnimation, reloadUpdated: Bool = true) where T.Iterator.Element: Hashable, T.Index == Int { 22 | let update = ListUpdate(diff(old, new), section) 23 | 24 | beginUpdates() 25 | 26 | deleteRows(at: update.deletions, with: animation) 27 | insertRows(at: update.insertions, with: animation) 28 | for move in update.moves { 29 | moveRow(at: move.from, to: move.to) 30 | } 31 | endUpdates() 32 | 33 | // reloadItems is done separately as the update indexes returne by diff() are in respect to the 34 | // "after" state, but the collectionView.reloadItems() call wants the "before" indexPaths. 35 | if reloadUpdated && update.updates.count > 0 { 36 | beginUpdates() 37 | reloadRows(at: update.updates, with: animation) 38 | endUpdates() 39 | } 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /Tests/DiffTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffTests.swift 3 | // HeckelDiff 4 | // 5 | // Created by Matias Cudich on 11/22/16. 6 | // Copyright © 2016 Matias Cudich. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import HeckelDiff 11 | 12 | struct FakeItem: Hashable { 13 | let value: Int 14 | let eValue: Int 15 | 16 | var hashValue: Int { 17 | return value.hashValue 18 | } 19 | } 20 | 21 | func ==(lhs: FakeItem, rhs: FakeItem) -> Bool { 22 | return lhs.eValue == rhs.eValue 23 | } 24 | 25 | func ==(lhs: (from: Int, to: Int), rhs: (from: Int, to: Int)) -> Bool { 26 | return lhs.0 == rhs.0 && lhs.1 == rhs.1 27 | } 28 | 29 | class DiffTests: XCTestCase { 30 | func testEmptyArrays() { 31 | let o = [Int]() 32 | let n = [Int]() 33 | let result = diff(o, n) 34 | XCTAssertEqual(0, result.count) 35 | } 36 | 37 | func testDiffingFromEmptyArray() { 38 | let o = [Int]() 39 | let n = [1] 40 | let result = diff(o, n) 41 | XCTAssertEqual(.insert(0), result[0]) 42 | XCTAssertEqual(1, result.count) 43 | } 44 | 45 | func testDiffingToEmptyArray() { 46 | let o = [1] 47 | let n = [Int]() 48 | let result = diff(o, n) 49 | XCTAssertEqual(.delete(0), result[0]) 50 | XCTAssertEqual(1, result.count) 51 | } 52 | 53 | func testSwapHasMoves() { 54 | let o = [1, 2, 3] 55 | let n = [2, 3, 1] 56 | let result = diff(o, n) 57 | XCTAssertEqual([.move(1, 0), .move(2, 1), .move(0, 2)], result) 58 | } 59 | 60 | func testSwapHasMovesWithOrder() { 61 | let o = [1, 2, 3] 62 | let n = [2, 3, 1] 63 | let result = orderedDiff(o, n) 64 | XCTAssertEqual([.delete(2), .delete(1), .delete(0), .insert(0), .insert(1), .insert(2)], result) 65 | } 66 | 67 | func testMovingTogether() { 68 | let o = [1, 2, 3, 3, 4] 69 | let n = [2, 3, 1, 3, 4] 70 | let result = diff(o, n) 71 | XCTAssertEqual([.move(1, 0), .move(2, 1), .move(0, 2)], result) 72 | } 73 | 74 | func testMovingTogetherWithOrder() { 75 | let o = [1, 2, 3, 3, 4] 76 | let n = [2, 3, 1, 3, 4] 77 | let result = orderedDiff(o, n) 78 | XCTAssertEqual([.delete(2), .delete(1), .delete(0), .insert(0), .insert(1), .insert(2)], result) 79 | } 80 | 81 | func testSwappedValuesHaveMoves() { 82 | let o = [1, 2, 3, 4] 83 | let n = [2, 4, 5, 3] 84 | let result = diff(o, n) 85 | XCTAssertEqual([.delete(0), .move(3, 1), .insert(2), .move(2, 3)], result) 86 | } 87 | 88 | func testSwappedValuesHaveMovesWithOrder() { 89 | let o = [1, 2, 3, 4] 90 | let n = [2, 4, 5, 3] 91 | let result = orderedDiff(o, n) 92 | XCTAssertEqual([.delete(3), .delete(2), .delete(0), .insert(1), .insert(2), .insert(3)], result) 93 | } 94 | 95 | func testUpdates() { 96 | let o = [ 97 | FakeItem(value: 0, eValue: 0), 98 | FakeItem(value: 1, eValue: 1), 99 | FakeItem(value: 2, eValue: 2) 100 | ] 101 | let n = [ 102 | FakeItem(value: 0, eValue: 1), 103 | FakeItem(value: 1, eValue: 2), 104 | FakeItem(value: 2, eValue: 3) 105 | ] 106 | let result = diff(o, n) 107 | XCTAssertEqual([.update(0), .update(1), .update(2)], result) 108 | } 109 | 110 | func testDeletionLeadingToInsertionDeletionMoves() { 111 | let o = [0, 1, 2, 3, 4, 5, 6, 7, 8] 112 | let n = [0, 2, 3, 4, 7, 6, 9, 5, 10] 113 | let result = diff(o, n) 114 | XCTAssertEqual([.delete(1), .delete(8), .move(7, 4), .insert(6), .move(5, 7), .insert(8)], result) 115 | } 116 | 117 | func testDeletionLeadingToInsertionDeletionMovesWithOrder() { 118 | let o = [0, 1, 2, 3, 4, 5, 6, 7, 8] 119 | let n = [0, 2, 3, 4, 7, 6, 9, 5, 10] 120 | let result = orderedDiff(o, n) 121 | XCTAssertEqual([.delete(8), .delete(7), .delete(5), .delete(1), .insert(4), .insert(6), .insert(7), .insert(8)], result) 122 | } 123 | 124 | func testMovingWithEqualityChanges() { 125 | let o = [ 126 | FakeItem(value: 0, eValue: 0), 127 | FakeItem(value: 1, eValue: 1), 128 | FakeItem(value: 2, eValue: 2) 129 | ] 130 | let n = [ 131 | FakeItem(value: 2, eValue: 3), 132 | FakeItem(value: 1, eValue: 1), 133 | FakeItem(value: 0, eValue: 0) 134 | ] 135 | let result = orderedDiff(o, n) 136 | XCTAssertEqual([.delete(2), .delete(0), .insert(0), .insert(2), .update(0)], result) 137 | } 138 | 139 | func testDeletingEqualObjects() { 140 | let o = [0, 0, 0, 0] 141 | let n = [0, 0] 142 | let result = diff(o, n) 143 | XCTAssertEqual(2, result.count) 144 | } 145 | 146 | func testInsertingEqualObjects() { 147 | let o = [0, 0] 148 | let n = [0, 0, 0, 0] 149 | let result = diff(o, n) 150 | XCTAssertEqual(2, result.count) 151 | } 152 | 153 | func testInsertingWithOldArrayHavingMultipleCopies() { 154 | let o = [NSObject(), NSObject(), NSObject(), 49, 33, "cat", "cat", 0, 14] as [AnyHashable] 155 | var n = o 156 | n.insert("cat", at: 5) 157 | let result = diff(o, n) 158 | XCTAssertEqual(1, result.count) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Tests/HeckelDiffTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeckelDiffTests.swift 3 | // HeckelDiffTests 4 | // 5 | // Created by Matias Cudich on 11/22/16. 6 | // Copyright © 2016 Matias Cudich. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import HeckelDiff 11 | 12 | class HeckelDiffTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Tests/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 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | --------------------------------------------------------------------------------