├── .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 | [](#)
3 | [](https://github.com/Carthage/Carthage)
4 | [](https://img.shields.io/cocoapods/v/HeckelDiff)
5 | [](http://cocoadocs.org/docsets/HeckelDiff)
6 | [](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 |
--------------------------------------------------------------------------------