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