├── .gitignore ├── Gemfile ├── Gemfile.lock ├── Podfile ├── Podfile.lock ├── Pods ├── Differ │ ├── LICENSE.md │ ├── README.md │ └── Sources │ │ └── Differ │ │ ├── BatchUpdate.swift │ │ ├── Diff+UIKit.swift │ │ ├── Diff.swift │ │ ├── ExtendedDiff.swift │ │ ├── ExtendedPatch+Apply.swift │ │ ├── ExtendedPatch.swift │ │ ├── GenericPatch.swift │ │ ├── LinkedList.swift │ │ ├── NestedBatchUpdate.swift │ │ ├── NestedDiff.swift │ │ ├── NestedExtendedDiff.swift │ │ ├── Patch+Apply.swift │ │ ├── Patch+Sort.swift │ │ └── Patch.swift ├── DifferenceKit │ ├── LICENSE │ ├── README.md │ └── Sources │ │ ├── Algorithm.swift │ │ ├── AnyDifferentiable.swift │ │ ├── ArraySection.swift │ │ ├── Changeset.swift │ │ ├── ContentEquatable.swift │ │ ├── Differentiable.swift │ │ ├── DifferentiableSection.swift │ │ ├── ElementPath.swift │ │ ├── Extensions │ │ └── UIKitExtension.swift │ │ └── StagedChangeset.swift ├── Dwifft │ ├── Dwifft │ │ ├── AbstractDiffCalculator.swift │ │ ├── Dwifft+AppKit.swift │ │ ├── Dwifft+UIKit.swift │ │ ├── Dwifft.swift │ │ └── SectionedValues.swift │ ├── LICENSE │ └── README.md ├── Manifest.lock ├── Pods.xcodeproj │ └── project.pbxproj └── Target Support Files │ ├── Differ │ ├── Differ-Info.plist │ ├── Differ-dummy.m │ ├── Differ-prefix.pch │ ├── Differ-umbrella.h │ ├── Differ.modulemap │ ├── Differ.xcconfig │ └── Info.plist │ ├── DifferenceKit │ ├── DifferenceKit-Info.plist │ ├── DifferenceKit-dummy.m │ ├── DifferenceKit-prefix.pch │ ├── DifferenceKit-umbrella.h │ ├── DifferenceKit.modulemap │ ├── DifferenceKit.xcconfig │ └── Info.plist │ ├── Dwifft │ ├── Dwifft-Info.plist │ ├── Dwifft-dummy.m │ ├── Dwifft-prefix.pch │ ├── Dwifft-umbrella.h │ ├── Dwifft.modulemap │ ├── Dwifft.xcconfig │ └── Info.plist │ └── Pods-TetrisDiffingCompetition │ ├── Info.plist │ ├── Pods-TetrisDiffingCompetition-Info.plist │ ├── Pods-TetrisDiffingCompetition-acknowledgements.markdown │ ├── Pods-TetrisDiffingCompetition-acknowledgements.plist │ ├── Pods-TetrisDiffingCompetition-dummy.m │ ├── Pods-TetrisDiffingCompetition-frameworks-Debug-input-files.xcfilelist │ ├── Pods-TetrisDiffingCompetition-frameworks-Debug-output-files.xcfilelist │ ├── Pods-TetrisDiffingCompetition-frameworks-Release-input-files.xcfilelist │ ├── Pods-TetrisDiffingCompetition-frameworks-Release-output-files.xcfilelist │ ├── Pods-TetrisDiffingCompetition-frameworks.sh │ ├── Pods-TetrisDiffingCompetition-resources.sh │ ├── Pods-TetrisDiffingCompetition-umbrella.h │ ├── Pods-TetrisDiffingCompetition.debug.xcconfig │ ├── Pods-TetrisDiffingCompetition.modulemap │ └── Pods-TetrisDiffingCompetition.release.xcconfig ├── README.md ├── TetrisDiffingCompetition.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── TetrisDiffingCompetition.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── TetrisDiffingCompetition ├── Adapters ├── Differ │ └── DifferTetrisAdapter.swift ├── DifferenceKit │ └── DifferenceKitTetrisAdapter.swift ├── Dwifft │ └── DwifftTetrisAdapter.swift ├── SKRBatchUpdates │ ├── BatchChanges.swift │ ├── DataSource.swift │ └── SKRBatchUpdatesTetrisAdapter.swift ├── TetrisAdapter.swift └── UICollectionViewDiffableDataSource │ └── UICollectionViewDiffableDataSourceTetrisAdapter.swift ├── Common ├── Common.swift └── UIViewController+common.swift ├── Main ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-1024.png │ │ ├── icon-120.png │ │ ├── icon-152.png │ │ ├── icon-167.png │ │ ├── icon-180.png │ │ ├── icon-20.png │ │ ├── icon-29.png │ │ ├── icon-40.png │ │ ├── icon-58.png │ │ ├── icon-60.png │ │ ├── icon-76.png │ │ ├── icon-80.png │ │ └── icon-87.png │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── MenuViewController.swift └── RootViewController.swift └── Tetris ├── Tetris.storyboard ├── Tetris.swift └── TetrisViewController.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | # 51 | # Add this line if you want to avoid checking in source code from the Xcode workspace 52 | # *.xcworkspace 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 67 | 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots/**/*.png 71 | fastlane/test_output 72 | 73 | # Code Injection 74 | # 75 | # After new code Injection tools there's a generated folder /iOSInjectionProject 76 | # https://github.com/johnno1962/injectionforxcode 77 | 78 | iOSInjectionProject/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.0) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | atomos (0.1.3) 11 | claide (1.0.2) 12 | cocoapods (1.7.3) 13 | activesupport (>= 4.0.2, < 5) 14 | claide (>= 1.0.2, < 2.0) 15 | cocoapods-core (= 1.7.3) 16 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 17 | cocoapods-downloader (>= 1.2.2, < 2.0) 18 | cocoapods-plugins (>= 1.0.0, < 2.0) 19 | cocoapods-search (>= 1.0.0, < 2.0) 20 | cocoapods-stats (>= 1.0.0, < 2.0) 21 | cocoapods-trunk (>= 1.3.1, < 2.0) 22 | cocoapods-try (>= 1.1.0, < 2.0) 23 | colored2 (~> 3.1) 24 | escape (~> 0.0.4) 25 | fourflusher (>= 2.3.0, < 3.0) 26 | gh_inspector (~> 1.0) 27 | molinillo (~> 0.6.6) 28 | nap (~> 1.0) 29 | ruby-macho (~> 1.4) 30 | xcodeproj (>= 1.10.0, < 2.0) 31 | cocoapods-core (1.7.3) 32 | activesupport (>= 4.0.2, < 6) 33 | fuzzy_match (~> 2.0.4) 34 | nap (~> 1.0) 35 | cocoapods-deintegrate (1.0.4) 36 | cocoapods-downloader (1.2.2) 37 | cocoapods-plugins (1.0.0) 38 | nap 39 | cocoapods-search (1.0.0) 40 | cocoapods-stats (1.1.0) 41 | cocoapods-trunk (1.3.1) 42 | nap (>= 0.8, < 2.0) 43 | netrc (~> 0.11) 44 | cocoapods-try (1.1.0) 45 | colored2 (3.1.2) 46 | concurrent-ruby (1.1.5) 47 | escape (0.0.4) 48 | fourflusher (2.3.1) 49 | fuzzy_match (2.0.4) 50 | gh_inspector (1.1.3) 51 | i18n (0.9.5) 52 | concurrent-ruby (~> 1.0) 53 | minitest (5.11.3) 54 | molinillo (0.6.6) 55 | nanaimo (0.2.6) 56 | nap (1.1.0) 57 | netrc (0.11.0) 58 | ruby-macho (1.4.0) 59 | thread_safe (0.3.6) 60 | tzinfo (1.2.5) 61 | thread_safe (~> 0.1) 62 | xcodeproj (1.10.0) 63 | CFPropertyList (>= 2.3.3, < 4.0) 64 | atomos (~> 0.1.3) 65 | claide (>= 1.0.2, < 2.0) 66 | colored2 (~> 3.1) 67 | nanaimo (~> 0.2.6) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | cocoapods 74 | 75 | BUNDLED WITH 76 | 1.17.2 77 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '11.0' 2 | 3 | target 'TetrisDiffingCompetition' do 4 | use_frameworks! 5 | 6 | pod 'DifferenceKit' 7 | pod 'Differ' 8 | pod 'Dwifft' 9 | end 10 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Differ (1.4.3) 3 | - DifferenceKit (1.1.3): 4 | - DifferenceKit/Core (= 1.1.3) 5 | - DifferenceKit/UIKitExtension (= 1.1.3) 6 | - DifferenceKit/Core (1.1.3) 7 | - DifferenceKit/UIKitExtension (1.1.3): 8 | - DifferenceKit/Core 9 | - Dwifft (0.9) 10 | 11 | DEPENDENCIES: 12 | - Differ 13 | - DifferenceKit 14 | - Dwifft 15 | 16 | SPEC REPOS: 17 | https://github.com/cocoapods/specs.git: 18 | - Differ 19 | - DifferenceKit 20 | - Dwifft 21 | 22 | SPEC CHECKSUMS: 23 | Differ: 6c7477d6187e8c36d02ec342a3c321061b85a0ea 24 | DifferenceKit: 5018791b6c1fc839921a3c171a0a539ace6ea60c 25 | Dwifft: 42912068ed2a8146077d1a1404df18625bd086e1 26 | 27 | PODFILE CHECKSUM: b38c134b980325949f29003cc8867113169b9422 28 | 29 | COCOAPODS: 1.7.3 30 | -------------------------------------------------------------------------------- /Pods/Differ/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 | -------------------------------------------------------------------------------- /Pods/Differ/README.md: -------------------------------------------------------------------------------- 1 | # Differ 2 | 3 | [![Build Status](https://travis-ci.com/tonyarnold/Differ.svg?branch=master)](https://travis-ci.com/tonyarnold/Differ) [![codecov](https://codecov.io/gh/tonyarnold/Differ/branch/master/graph/badge.svg)](https://codecov.io/gh/tonyarnold/Differ) 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 | ```swift 87 | // The following will automatically animate deletions, insertions, and moves: 88 | 89 | tableView.animateRowChanges(oldData: old, newData: new) 90 | 91 | collectionView.animateItemChanges(oldData: old, newData: new) 92 | 93 | // It can work with sections, too! 94 | 95 | tableView.animateRowAndSectionChanges(oldData: old, newData: new) 96 | 97 | collectionView.animateItemAndSectionChanges(oldData: old, newData: new) 98 | 99 | ``` 100 | 101 | Please see the [included examples](/Examples/) for a working sample. 102 | 103 | ### Using Patch and Diff 104 | 105 | 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: 106 | 107 | ```swift 108 | let from: T 109 | let to: T 110 | 111 | // patch() only includes insertions and deletions 112 | let patch: [Patch] = patch(from: from, to: to) 113 | 114 | // extendedPatch() includes insertions, deletions and moves 115 | let patch: [ExtendedPatch] = extendedPatch(from: from, to: to) 116 | ``` 117 | 118 | When you need additional control over ordering, you could use the following: 119 | 120 | ```swift 121 | let insertionsFirst = { element1, element2 -> Bool in 122 | switch (element1, element2) { 123 | case (.insert(let at1), .insert(let at2)): 124 | return at1 < at2 125 | case (.insert, .delete): 126 | return true 127 | case (.delete, .insert): 128 | return false 129 | case (.delete(let at1), .delete(let at2)): 130 | return at1 < at2 131 | default: fatalError() // Unreachable 132 | } 133 | } 134 | 135 | // Results in a list of patches with insertions preceding deletions 136 | let patch = patch(from: from, to: to, sort: insertionsFirst) 137 | ``` 138 | 139 | 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. 140 | 141 | `D` is the length of a diff: 142 | 143 | - Generating a sorted patch takes `O(D^2)` time. 144 | - The default order takes `O(D)` to generate. 145 | 146 | ```swift 147 | // Generate the difference first 148 | let diff = from.diff(to) 149 | 150 | // Now generate the list of patches utilising the diff we've just calculated 151 | let patch = diff.patch(from: from, to: to) 152 | ``` 153 | 154 | If you'd like to learn more about how this library works, `Graph.playground` is a great place to start. 155 | 156 | ## Performance notes 157 | 158 | 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. 159 | 160 | In the following benchmarks, you should see an order of magnitude difference in calculation time between the two algorithms. 161 | 162 | Each measurement is the mean time in seconds it takes to calculate a diff, over 10 runs on an iPhone 6. 163 | 164 | | | Diff | Dwifft | 165 | |---------|:----------|:--------| 166 | | same | 0.0213 | 52.3642 | 167 | | created | 0.0188 | 0.0033 | 168 | | deleted | 0.0184 | 0.0050 | 169 | | diff | 0.1320 | 63.4084 | 170 | 171 | You can run these benchmarks yourself by [checking out the Diff Performance Suite](https://github.com/tonyarnold/DiffPerformanceSuite). 172 | 173 | 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). 174 | 175 | ## Requirements 176 | 177 | Differ requires Swift 4.2 or 5.0 and Xcode 10.2 or later to compile. 178 | 179 | ## Installation 180 | 181 | You can add Differ to your project using Carthage, CocoaPods, Swift Package Manager, or as an Xcode subproject. 182 | 183 | ### Carthage 184 | 185 | ```ruby 186 | github "tonyarnold/Differ" 187 | ``` 188 | 189 | ### CocoaPods 190 | 191 | ```ruby 192 | pod 'Differ' 193 | ``` 194 | 195 | ## Acknowledgements 196 | 197 | 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. 198 | 199 | Please, [file issues with this fork here in this repository](https://github.com/tonyarnold/Diff/issues/new), not in Wojtek's original repository. 200 | -------------------------------------------------------------------------------- /Pods/Differ/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 | -------------------------------------------------------------------------------- /Pods/Differ/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 | -------------------------------------------------------------------------------- /Pods/Differ/Sources/Differ/ExtendedPatch+Apply.swift: -------------------------------------------------------------------------------- 1 | public extension RangeReplaceableCollection { 2 | 3 | func apply(_ patch: [ExtendedPatch]) -> Self { 4 | var mutableSelf = self 5 | 6 | for change in patch { 7 | switch change { 8 | case let .insertion(i, element): 9 | let target = mutableSelf.index(mutableSelf.startIndex, offsetBy: i) 10 | mutableSelf.insert(element, at: target) 11 | case let .deletion(i): 12 | let target = mutableSelf.index(mutableSelf.startIndex, offsetBy: i) 13 | mutableSelf.remove(at: target) 14 | case let .move(from, to): 15 | let fromIndex = mutableSelf.index(mutableSelf.startIndex, offsetBy: from) 16 | let toIndex = mutableSelf.index(mutableSelf.startIndex, offsetBy: to) 17 | let element = mutableSelf.remove(at: fromIndex) 18 | mutableSelf.insert(element, at: toIndex) 19 | } 20 | } 21 | 22 | return mutableSelf 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Pods/Differ/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 | /// 24 | /// - insertion: A single patch step containing an insertion index and an element to be inserted 25 | /// - deletion: A single patch step containing a deletion index 26 | /// - move: A single patch step containing the origin and target of a move 27 | public enum ExtendedPatch { 28 | /// A single patch step containing the origin and target of a move 29 | case insertion(index: Int, element: Element) 30 | /// A single patch step containing a deletion index 31 | case deletion(index: Int) 32 | /// A single patch step containing the origin and target of a move 33 | case move(from: Int, to: Int) 34 | } 35 | 36 | /// 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. 37 | /// 38 | /// - Complexity: O((N+M)*D) 39 | /// 40 | /// - Parameters: 41 | /// - from: The source collection 42 | /// - to: The target collection 43 | /// - sort: A sorting function 44 | /// - Returns: Arbitrarly sorted sequence of steps to obtain `to` collection from the `from` one. 45 | public func extendedPatch( 46 | from: T, 47 | to: T, 48 | sort: ExtendedDiff.OrderedBefore? = nil 49 | ) -> [ExtendedPatch] where T.Element: Equatable { 50 | return from.extendedDiff(to).patch(from: from, to: to, sort: sort) 51 | } 52 | 53 | extension ExtendedDiff { 54 | public typealias OrderedBefore = (_ fst: ExtendedDiff.Element, _ snd: ExtendedDiff.Element) -> Bool 55 | 56 | /// 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. 57 | /// 58 | /// - Complexity: O(D^2) 59 | /// 60 | /// - Parameters: 61 | /// - from: The source collection (usually the source collecetion of the callee) 62 | /// - to: The target collection (usually the target collecetion of the callee) 63 | /// - sort: A sorting function 64 | /// - Returns: Arbitrarly sorted sequence of steps to obtain `to` collection from the `from` one. 65 | public func patch( 66 | from: T, 67 | to: T, 68 | sort: OrderedBefore? = nil 69 | ) -> [ExtendedPatch] { 70 | 71 | let result: [SortedPatchElement] 72 | if let sort = sort { 73 | result = shiftedPatchElements(from: generateSortedPatchElements(from: from, to: to, sort: sort)) 74 | } else { 75 | result = shiftedPatchElements(from: generateSortedPatchElements(from: from, to: to)) 76 | } 77 | 78 | return result.indices.compactMap { i -> ExtendedPatch? in 79 | let patchElement = result[i] 80 | if moveIndices.contains(patchElement.sourceIndex) { 81 | let to = result[i + 1].value 82 | switch patchElement.value { 83 | case let .deletion(index): 84 | if case let .insertion(toIndex, _) = to { 85 | return .move(from: index, to: toIndex) 86 | } else { 87 | fatalError() 88 | } 89 | case let .insertion(index, _): 90 | if case let .deletion(fromIndex) = to { 91 | return .move(from: fromIndex, to: index) 92 | } else { 93 | fatalError() 94 | } 95 | } 96 | } else if !(i > 0 && moveIndices.contains(result[i - 1].sourceIndex)) { 97 | switch patchElement.value { 98 | case let .deletion(index): 99 | return .deletion(index: index) 100 | case let .insertion(index, element): 101 | return .insertion(index: index, element: element) 102 | } 103 | } 104 | return nil 105 | } 106 | } 107 | 108 | func generateSortedPatchElements( 109 | from: T, 110 | to: T, 111 | sort: @escaping OrderedBefore 112 | ) -> [SortedPatchElement] { 113 | let unboxed = boxDiffAndPatchElements( 114 | from: from, 115 | to: to 116 | ).sorted { from, to -> Bool in 117 | return sort(from.diffElement, to.diffElement) 118 | }.flatMap(unbox) 119 | 120 | return unboxed.indices.map { index -> SortedPatchElement in 121 | let old = unboxed[index] 122 | return SortedPatchElement( 123 | value: old.value, 124 | sourceIndex: old.sourceIndex, 125 | sortedIndex: index) 126 | }.sorted { (fst, snd) -> Bool in 127 | fst.sourceIndex < snd.sourceIndex 128 | } 129 | } 130 | 131 | func generateSortedPatchElements(from: T, to: T) -> [SortedPatchElement] { 132 | let patch = source.patch(to: to) 133 | return patch.indices.map { 134 | SortedPatchElement( 135 | value: patch[$0], 136 | sourceIndex: $0, 137 | sortedIndex: reorderedIndex[$0] 138 | ) 139 | } 140 | } 141 | 142 | func boxDiffAndPatchElements( 143 | from: T, 144 | to: T 145 | ) -> [BoxedDiffAndPatchElement] { 146 | let sourcePatch = generateSortedPatchElements(from: from, to: to) 147 | var indexDiff = 0 148 | return elements.indices.map { i in 149 | let diffElement = elements[i] 150 | switch diffElement { 151 | case .move: 152 | indexDiff += 1 153 | return .move( 154 | diffElement: diffElement, 155 | deletion: sourcePatch[sourceIndex[i + indexDiff - 1]], 156 | insertion: sourcePatch[sourceIndex[i + indexDiff]] 157 | ) 158 | default: 159 | return .single( 160 | diffElement: diffElement, 161 | patchElement: sourcePatch[sourceIndex[i + indexDiff]] 162 | ) 163 | } 164 | } 165 | } 166 | } 167 | 168 | func unbox(_ element: BoxedDiffAndPatchElement) -> [SortedPatchElement] { 169 | switch element { 170 | case let .move(_, deletion, insertion): 171 | return [deletion, insertion] 172 | case let .single(_, patchElement): 173 | return [patchElement] 174 | } 175 | } 176 | 177 | extension ExtendedPatch: CustomDebugStringConvertible { 178 | public var debugDescription: String { 179 | switch self { 180 | case let .deletion(at): 181 | return "D(\(at))" 182 | case let .insertion(at, element): 183 | return "I(\(at),\(element))" 184 | case let .move(from, to): 185 | return "M(\(from),\(to))" 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Pods/Differ/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: DoublyLinkedList>) { 38 | var from = node.previous 39 | while let nextFrom = from, nextFrom.value.sourceIndex < node.value.sourceIndex { 40 | shiftPatchElement(from: nextFrom, to: node) 41 | from = nextFrom.previous 42 | } 43 | 44 | if let next = node.next { 45 | shiftPatchElement(node: next) 46 | } 47 | } 48 | 49 | func shiftPatchElement(from: DoublyLinkedList>, to: DoublyLinkedList>) { 50 | let type = edgeType(from: from, to: to) 51 | switch type { 52 | case .cycle: 53 | fatalError() 54 | case let .neighbor(direction), let .jump(direction): 55 | if case .left = direction { 56 | switch (from.value.value, to.value.value) { 57 | case (.insertion, _): 58 | to.value = to.value.decremented() 59 | case (.deletion, _): 60 | to.value = to.value.incremented() 61 | } 62 | } 63 | } 64 | } 65 | 66 | extension SortedPatchElement { 67 | func incremented() -> SortedPatchElement { 68 | return SortedPatchElement( 69 | value: value.incremented(), 70 | sourceIndex: sourceIndex, 71 | sortedIndex: sortedIndex) 72 | } 73 | 74 | func decremented() -> SortedPatchElement { 75 | return SortedPatchElement( 76 | value: value.decremented(), 77 | sourceIndex: sourceIndex, 78 | sortedIndex: sortedIndex) 79 | } 80 | } 81 | 82 | extension Patch { 83 | 84 | func incremented() -> Patch { 85 | return shiftedIndex(by: 1) 86 | } 87 | 88 | func decremented() -> Patch { 89 | return shiftedIndex(by: -1) 90 | } 91 | 92 | func shiftedIndex(by n: Int) -> Patch { 93 | switch self { 94 | case let .insertion(index, element): 95 | return .insertion(index: index + n, element: element) 96 | case let .deletion(index): 97 | return .deletion(index: index + n) 98 | } 99 | } 100 | } 101 | 102 | func shiftedPatchElements(from sortedPatchElements: [SortedPatchElement]) -> [SortedPatchElement] { 103 | let linkedList = DoublyLinkedList(linkedList: LinkedList(array: sortedPatchElements)) 104 | if let secondElement = linkedList?.next { 105 | shiftPatchElement(node: secondElement) 106 | } 107 | 108 | guard let result = linkedList?.array().sorted(by: { (fst, second) -> Bool in 109 | fst.sortedIndex < second.sortedIndex 110 | }) else { 111 | return [] 112 | } 113 | return result 114 | } 115 | -------------------------------------------------------------------------------- /Pods/Differ/Sources/Differ/LinkedList.swift: -------------------------------------------------------------------------------- 1 | class LinkedList { 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 | func array() -> Array { 27 | if let next = next { 28 | return [value] + next.array() 29 | } 30 | return [value] 31 | } 32 | } 33 | 34 | class DoublyLinkedList { 35 | let next: DoublyLinkedList? 36 | private(set) weak var previous: DoublyLinkedList? 37 | var head: DoublyLinkedList { 38 | guard let previous = previous else { 39 | return self 40 | } 41 | return previous.head 42 | } 43 | 44 | var value: T 45 | 46 | init(next: DoublyLinkedList?, value: T) { 47 | self.value = value 48 | self.next = next 49 | self.next?.previous = self 50 | } 51 | 52 | init?(array: [T]) { 53 | let reversed = array.reversed() 54 | guard let first = array.first else { 55 | return nil 56 | } 57 | 58 | var tailDoublyLinkedList: DoublyLinkedList? 59 | 60 | for i in 0 ..< reversed.count - 1 { 61 | let nextTail = DoublyLinkedList(next: tailDoublyLinkedList, value: reversed.itemOnStartIndex(advancedBy: i)) 62 | tailDoublyLinkedList?.previous = nextTail 63 | tailDoublyLinkedList = nextTail 64 | } 65 | 66 | value = first 67 | next = tailDoublyLinkedList 68 | next?.previous = self 69 | } 70 | 71 | convenience init?(linkedList: LinkedList?) { 72 | guard let linkedList = linkedList else { 73 | return nil 74 | } 75 | self.init(array: linkedList.array()) 76 | } 77 | 78 | func array() -> Array { 79 | if let next = next { 80 | return [value] + next.array() 81 | } 82 | return [value] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Pods/Differ/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(move): 40 | sectionMoves.append((sectionTransform(move.from), sectionTransform(move.to))) 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 | -------------------------------------------------------------------------------- /Pods/Differ/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 | -------------------------------------------------------------------------------- /Pods/Differ/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 | -------------------------------------------------------------------------------- /Pods/Differ/Sources/Differ/Patch+Apply.swift: -------------------------------------------------------------------------------- 1 | public extension RangeReplaceableCollection { 2 | 3 | func apply(_ patch: [Patch]) -> Self { 4 | var mutableSelf = self 5 | 6 | for change in patch { 7 | switch change { 8 | case let .insertion(i, element): 9 | let target = mutableSelf.index(mutableSelf.startIndex, offsetBy: i) 10 | mutableSelf.insert(element, at: target) 11 | case let .deletion(i): 12 | let target = mutableSelf.index(mutableSelf.startIndex, offsetBy: i) 13 | mutableSelf.remove(at: target) 14 | } 15 | } 16 | 17 | return mutableSelf 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Pods/Differ/Sources/Differ/Patch+Sort.swift: -------------------------------------------------------------------------------- 1 | /// Generates arbitrarly sorted 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. 2 | /// 3 | /// - Complexity: O((N+M)*D) 4 | /// 5 | /// - Parameters: 6 | /// - from: The source collection 7 | /// - to: The target collection 8 | /// - sort: A sorting function 9 | /// - Returns: Arbitrarly sorted sequence of steps to obtain `to` collection from the `from` one. 10 | public func patch( 11 | from: T, 12 | to: T, 13 | sort: Diff.OrderedBefore 14 | ) -> [Patch] where T.Element: Equatable { 15 | return from.diff(to).patch(from: from, to: to, sort: sort) 16 | } 17 | 18 | public extension Diff { 19 | 20 | typealias OrderedBefore = (_ fst: Diff.Element, _ snd: Diff.Element) -> Bool 21 | 22 | /// Generates arbitrarly sorted 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. 23 | /// 24 | /// - Complexity: O(D^2) 25 | /// 26 | /// - Parameters: 27 | /// - from: The source collection (usually the source collecetion of the callee) 28 | /// - to: The target collection (usually the target collecetion of the callee) 29 | /// - sort: A sorting function 30 | /// - Returns: Arbitrarly sorted sequence of steps to obtain `to` collection from the `from` one. 31 | func patch( 32 | from: T, 33 | to: T, 34 | sort: OrderedBefore 35 | ) -> [Patch] { 36 | let shiftedPatch = patch(to: to) 37 | return shiftedPatchElements(from: sortedPatchElements( 38 | from: shiftedPatch, 39 | sortBy: sort 40 | )).map { $0.value } 41 | } 42 | 43 | private func sortedPatchElements(from source: [Patch], sortBy areInIncreasingOrder: OrderedBefore) -> [SortedPatchElement] { 44 | let sorted = indices.map { (self[$0], $0) } 45 | .sorted { areInIncreasingOrder($0.0, $1.0) } 46 | return sorted.indices.map { i in 47 | let p = sorted[i] 48 | return SortedPatchElement( 49 | value: source[p.1], 50 | sourceIndex: p.1, 51 | sortedIndex: i) 52 | }.sorted(by: { (fst, snd) -> Bool in 53 | fst.sourceIndex < snd.sourceIndex 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Pods/Differ/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. It is a list of steps to be applied to obtain the `to` collection from the `from` one. 43 | /// 44 | /// - Complexity: O((N+M)*D) 45 | /// 46 | /// - Parameters: 47 | /// - from: The source collection 48 | /// - to: The target collection 49 | /// - Returns: A sequence of steps to obtain `to` collection from the `from` one. 50 | public func patch( 51 | from: T, 52 | to: T 53 | ) -> [Patch] where T.Element: Equatable { 54 | return from.diff(to).patch(to: to) 55 | } 56 | 57 | extension Patch: CustomDebugStringConvertible { 58 | public var debugDescription: String { 59 | switch self { 60 | case let .deletion(at): 61 | return "D(\(at))" 62 | case let .insertion(at, element): 63 | return "I(\(at),\(element))" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/AnyDifferentiable.swift: -------------------------------------------------------------------------------- 1 | /// A type-erased differentiable value. 2 | /// 3 | /// The `AnyDifferentiable` type hides the specific underlying types. 4 | /// Associated type `DifferenceIdentifier` is erased by `AnyHashable`. 5 | /// The comparisons of whether has updated is forwards to an underlying differentiable value. 6 | /// 7 | /// You can store mixed-type elements in collection that require `Differentiable` conformance by 8 | /// wrapping mixed-type elements in `AnyDifferentiable`: 9 | /// 10 | /// extension String: Differentiable {} 11 | /// extension Int: Differentiable {} 12 | /// 13 | /// let source = [ 14 | /// AnyDifferentiable("ABC"), 15 | /// AnyDifferentiable(100) 16 | /// ] 17 | /// let target = [ 18 | /// AnyDifferentiable("ABC"), 19 | /// AnyDifferentiable(100), 20 | /// AnyDifferentiable(200) 21 | /// ] 22 | /// 23 | /// let changeset = StagedChangeset(source: source, target: target) 24 | /// print(changeset.isEmpty) // prints "false" 25 | public struct AnyDifferentiable: Differentiable { 26 | /// The value wrapped by this instance. 27 | @inlinable 28 | public var base: Any { 29 | return box.base 30 | } 31 | 32 | /// A type-erased identifier value for difference calculation. 33 | @inlinable 34 | public var differenceIdentifier: AnyHashable { 35 | return box.differenceIdentifier 36 | } 37 | 38 | @usableFromInline 39 | internal let box: AnyDifferentiableBox 40 | 41 | /// Creates a type-erased differentiable value that wraps the given instance. 42 | /// 43 | /// - Parameters: 44 | /// - base: A differentiable value to wrap. 45 | @inlinable 46 | public init(_ base: D) { 47 | if let anyDifferentiable = base as? AnyDifferentiable { 48 | self = anyDifferentiable 49 | } 50 | else { 51 | box = DifferentiableBox(base) 52 | } 53 | } 54 | 55 | /// Indicate whether the content of `base` is equals to the content of the given source value. 56 | /// 57 | /// - Parameters: 58 | /// - source: A source value to be compared. 59 | /// 60 | /// - Returns: A Boolean value indicating whether the content of `base` is equals 61 | /// to the content of `base` of the given source value. 62 | @inlinable 63 | public func isContentEqual(to source: AnyDifferentiable) -> Bool { 64 | return box.isContentEqual(to: source.box) 65 | } 66 | } 67 | 68 | extension AnyDifferentiable: CustomDebugStringConvertible { 69 | public var debugDescription: String { 70 | return "AnyDifferentiable(\(String(reflecting: base)))" 71 | } 72 | } 73 | 74 | @usableFromInline 75 | internal protocol AnyDifferentiableBox { 76 | var base: Any { get } 77 | var differenceIdentifier: AnyHashable { get } 78 | 79 | func isContentEqual(to source: AnyDifferentiableBox) -> Bool 80 | } 81 | 82 | @usableFromInline 83 | internal struct DifferentiableBox: AnyDifferentiableBox { 84 | @usableFromInline 85 | internal let baseComponent: Base 86 | 87 | @inlinable 88 | internal var base: Any { 89 | return baseComponent 90 | } 91 | 92 | @inlinable 93 | internal var differenceIdentifier: AnyHashable { 94 | return baseComponent.differenceIdentifier 95 | } 96 | 97 | @inlinable 98 | internal init(_ base: Base) { 99 | baseComponent = base 100 | } 101 | 102 | @inlinable 103 | internal func isContentEqual(to source: AnyDifferentiableBox) -> Bool { 104 | guard let sourceBase = source.base as? Base else { 105 | return false 106 | } 107 | return baseComponent.isContentEqual(to: sourceBase) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/ArraySection.swift: -------------------------------------------------------------------------------- 1 | /// A differentiable section with model and array of elements. 2 | /// 3 | /// Arrays are can not be identify each one and comparing whether has updated from other one. 4 | /// ArraySection is a generic wrapper to hold a model to allow it. 5 | public struct ArraySection: DifferentiableSection { 6 | /// The model of section for differentiated with other section. 7 | public var model: Model 8 | /// The array of element in the section. 9 | public var elements: [Element] 10 | 11 | /// An identifier value that of model for difference calculation. 12 | @inlinable 13 | public var differenceIdentifier: Model.DifferenceIdentifier { 14 | return model.differenceIdentifier 15 | } 16 | 17 | /// Creates a section with the model and the elements. 18 | /// 19 | /// - Parameters: 20 | /// - model: A differentiable model of section. 21 | /// - elements: The collection of element in the section. 22 | @inlinable 23 | public init(model: Model, elements: C) where C.Element == Element { 24 | self.model = model 25 | self.elements = Array(elements) 26 | } 27 | 28 | /// Creates a new section reproducing the given source section with replacing the elements. 29 | /// 30 | /// - Parameters: 31 | /// - source: A source section to reproduce. 32 | /// - elements: The collection of elements for the new section. 33 | @inlinable 34 | public init(source: ArraySection, elements: C) where C.Element == Element { 35 | self.init(model: source.model, elements: elements) 36 | } 37 | 38 | /// Indicate whether the content of `self` is equals to the content of 39 | /// the given source section. 40 | /// 41 | /// - Note: It's compared by the model of `self` and the specified section. 42 | /// 43 | /// - Parameters: 44 | /// - source: A source section to compare. 45 | /// 46 | /// - Returns: A Boolean value indicating whether the content of `self` is equals 47 | /// to the content of the given source section. 48 | @inlinable 49 | public func isContentEqual(to source: ArraySection) -> Bool { 50 | return model.isContentEqual(to: source.model) 51 | } 52 | } 53 | 54 | extension ArraySection: Equatable where Model: Equatable, Element: Equatable { 55 | public static func == (lhs: ArraySection, rhs: ArraySection) -> Bool { 56 | return lhs.model == rhs.model && lhs.elements == rhs.elements 57 | } 58 | } 59 | 60 | extension ArraySection: CustomDebugStringConvertible { 61 | public var debugDescription: String { 62 | return """ 63 | ArraySection( 64 | model: \(model), 65 | elements: \(elements) 66 | ) 67 | """ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/Changeset.swift: -------------------------------------------------------------------------------- 1 | /// A set of changes in the sectioned collection. 2 | /// 3 | /// Changes to the section of the linear collection should be empty. 4 | /// 5 | /// Notice that the value of the changes represents offsets of collection not index. 6 | /// Since offsets are unordered, order is ignored when comparing two `Changeset`s. 7 | public struct Changeset { 8 | /// The collection after changed. 9 | public var data: Collection 10 | 11 | /// The offsets of deleted sections. 12 | public var sectionDeleted: [Int] 13 | /// The offsets of inserted sections. 14 | public var sectionInserted: [Int] 15 | /// The offsets of updated sections. 16 | public var sectionUpdated: [Int] 17 | /// The pairs of source and target offset of moved sections. 18 | public var sectionMoved: [(source: Int, target: Int)] 19 | 20 | /// The paths of deleted elements. 21 | public var elementDeleted: [ElementPath] 22 | /// The paths of inserted elements. 23 | public var elementInserted: [ElementPath] 24 | /// The paths of updated elements. 25 | public var elementUpdated: [ElementPath] 26 | /// The pairs of source and target path of moved elements. 27 | public var elementMoved: [(source: ElementPath, target: ElementPath)] 28 | 29 | /// Creates a new `Changeset`. 30 | /// 31 | /// - Parameters: 32 | /// - data: The collection after changed. 33 | /// - sectionDeleted: The offsets of deleted sections. 34 | /// - sectionInserted: The offsets of inserted sections. 35 | /// - sectionUpdated: The offsets of updated sections. 36 | /// - sectionMoved: The pairs of source and target offset of moved sections. 37 | /// - elementDeleted: The paths of deleted elements. 38 | /// - elementInserted: The paths of inserted elements. 39 | /// - elementUpdated: The paths of updated elements. 40 | /// - elementMoved: The pairs of source and target path of moved elements. 41 | @inlinable 42 | public init( 43 | data: Collection, 44 | sectionDeleted: [Int] = [], 45 | sectionInserted: [Int] = [], 46 | sectionUpdated: [Int] = [], 47 | sectionMoved: [(source: Int, target: Int)] = [], 48 | elementDeleted: [ElementPath] = [], 49 | elementInserted: [ElementPath] = [], 50 | elementUpdated: [ElementPath] = [], 51 | elementMoved: [(source: ElementPath, target: ElementPath)] = [] 52 | ) { 53 | self.data = data 54 | self.sectionDeleted = sectionDeleted 55 | self.sectionInserted = sectionInserted 56 | self.sectionUpdated = sectionUpdated 57 | self.sectionMoved = sectionMoved 58 | self.elementDeleted = elementDeleted 59 | self.elementInserted = elementInserted 60 | self.elementUpdated = elementUpdated 61 | self.elementMoved = elementMoved 62 | } 63 | } 64 | 65 | public extension Changeset { 66 | /// The number of section changes. 67 | @inlinable 68 | var sectionChangeCount: Int { 69 | return sectionDeleted.count 70 | + sectionInserted.count 71 | + sectionUpdated.count 72 | + sectionMoved.count 73 | } 74 | 75 | /// The number of element changes. 76 | @inlinable 77 | var elementChangeCount: Int { 78 | return elementDeleted.count 79 | + elementInserted.count 80 | + elementUpdated.count 81 | + elementMoved.count 82 | } 83 | 84 | /// The number of all changes. 85 | @inlinable 86 | var changeCount: Int { 87 | return sectionChangeCount + elementChangeCount 88 | } 89 | 90 | /// A Boolean value indicating whether has section changes. 91 | @inlinable 92 | var hasSectionChanges: Bool { 93 | return sectionChangeCount > 0 94 | } 95 | 96 | /// A Boolean value indicating whether has element changes. 97 | @inlinable 98 | var hasElementChanges: Bool { 99 | return elementChangeCount > 0 100 | } 101 | 102 | /// A Boolean value indicating whether has changes. 103 | @inlinable 104 | var hasChanges: Bool { 105 | return changeCount > 0 106 | } 107 | } 108 | 109 | extension Changeset: Equatable where Collection: Equatable { 110 | public static func == (lhs: Changeset, rhs: Changeset) -> Bool { 111 | return lhs.data == rhs.data 112 | && Set(lhs.sectionDeleted) == Set(rhs.sectionDeleted) 113 | && Set(lhs.sectionInserted) == Set(rhs.sectionInserted) 114 | && Set(lhs.sectionUpdated) == Set(rhs.sectionUpdated) 115 | && Set(lhs.sectionMoved.map(HashablePair.init)) == Set(rhs.sectionMoved.map(HashablePair.init)) 116 | && Set(lhs.elementDeleted) == Set(rhs.elementDeleted) 117 | && Set(lhs.elementInserted) == Set(rhs.elementInserted) 118 | && Set(lhs.elementUpdated) == Set(rhs.elementUpdated) 119 | && Set(lhs.elementMoved.map(HashablePair.init)) == Set(rhs.elementMoved.map(HashablePair.init)) 120 | } 121 | } 122 | 123 | extension Changeset: CustomDebugStringConvertible { 124 | public var debugDescription: String { 125 | guard !data.isEmpty || hasChanges else { 126 | return """ 127 | Changeset( 128 | data: [] 129 | )" 130 | """ 131 | } 132 | 133 | var description = """ 134 | Changeset( 135 | data: \(data.isEmpty ? "[]" : "[\n \(data.map { "\($0)" }.joined(separator: ",\n").split(separator: "\n").joined(separator: "\n "))\n ]") 136 | """ 137 | 138 | func appendDescription(name: String, elements: [T]) { 139 | guard !elements.isEmpty else { return } 140 | 141 | description += ",\n \(name): [\n \(elements.map { "\($0)" }.joined(separator: ",\n").split(separator: "\n").joined(separator: "\n "))\n ]" 142 | } 143 | 144 | appendDescription(name: "sectionDeleted", elements: sectionDeleted) 145 | appendDescription(name: "sectionInserted", elements: sectionInserted) 146 | appendDescription(name: "sectionUpdated", elements: sectionUpdated) 147 | appendDescription(name: "sectionMoved", elements: sectionMoved) 148 | appendDescription(name: "elementDeleted", elements: elementDeleted) 149 | appendDescription(name: "elementInserted", elements: elementInserted) 150 | appendDescription(name: "elementUpdated", elements: elementUpdated) 151 | appendDescription(name: "elementMoved", elements: elementMoved) 152 | 153 | description += "\n)" 154 | return description 155 | } 156 | } 157 | 158 | private struct HashablePair: Hashable { 159 | let first: H 160 | let second: H 161 | } 162 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/ContentEquatable.swift: -------------------------------------------------------------------------------- 1 | /// Represents a value that can compare whether the content are equal. 2 | public protocol ContentEquatable { 3 | /// Indicate whether the content of `self` is equals to the content of 4 | /// the given source value. 5 | /// 6 | /// - Parameters: 7 | /// - source: A source value to be compared. 8 | /// 9 | /// - Returns: A Boolean value indicating whether the content of `self` is equals 10 | /// to the content of the given source value. 11 | func isContentEqual(to source: Self) -> Bool 12 | } 13 | 14 | public extension ContentEquatable where Self: Equatable { 15 | /// Indicate whether the content of `self` is equals to the content of the given source value. 16 | /// Compared using `==` operator of `Equatable'. 17 | /// 18 | /// - Parameters: 19 | /// - source: A source value to be compared. 20 | /// 21 | /// - Returns: A Boolean value indicating whether the content of `self` is equals 22 | /// to the content of the given source value. 23 | @inlinable 24 | func isContentEqual(to source: Self) -> Bool { 25 | return self == source 26 | } 27 | } 28 | 29 | extension Optional: ContentEquatable where Wrapped: ContentEquatable { 30 | /// Indicate whether the content of `self` is equals to the content of the given source value. 31 | /// Returns `true` if both values compared are nil. 32 | /// The result of comparison between nil and non-nil values is `false`. 33 | /// 34 | /// - Parameters: 35 | /// - source: An optional source value to be compared. 36 | /// 37 | /// - Returns: A Boolean value indicating whether the content of `self` is equals 38 | /// to the content of the given source value. 39 | @inlinable 40 | public func isContentEqual(to source: Wrapped?) -> Bool { 41 | switch (self, source) { 42 | case let (lhs?, rhs?): 43 | return lhs.isContentEqual(to: rhs) 44 | 45 | case (.none, .none): 46 | return true 47 | 48 | case (.none, .some), (.some, .none): 49 | return false 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/Differentiable.swift: -------------------------------------------------------------------------------- 1 | /// Represents the value that identified for differentiate. 2 | public protocol Differentiable: ContentEquatable { 3 | /// A type representing the identifier. 4 | associatedtype DifferenceIdentifier: Hashable 5 | 6 | /// An identifier value for difference calculation. 7 | var differenceIdentifier: DifferenceIdentifier { get } 8 | } 9 | 10 | public extension Differentiable where Self: Hashable { 11 | /// The `self` value as an identifier for difference calculation. 12 | @inlinable 13 | var differenceIdentifier: Self { 14 | return self 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/DifferentiableSection.swift: -------------------------------------------------------------------------------- 1 | /// Represents the section of collection that can be identified and compared to whether has updated. 2 | public protocol DifferentiableSection: Differentiable { 3 | /// A type representing the elements in section. 4 | associatedtype Collection: Swift.Collection where Collection.Element: Differentiable 5 | 6 | /// The collection of element in the section. 7 | var elements: Collection { get } 8 | 9 | /// Creates a new section reproducing the given source section with replacing the elements. 10 | /// 11 | /// - Parameters: 12 | /// - source: A source section to reproduce. 13 | /// - elements: The collection of elements for the new section. 14 | init(source: Self, elements: C) where C.Element == Collection.Element 15 | } 16 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/ElementPath.swift: -------------------------------------------------------------------------------- 1 | /// Represents the path to a specific element in a tree of nested collections. 2 | /// 3 | /// - Note: `Foundation.IndexPath` is disadvantageous in performance. 4 | public struct ElementPath: Hashable { 5 | /// The element index (or offset) of this path. 6 | public var element: Int 7 | /// The section index (or offset) of this path. 8 | public var section: Int 9 | 10 | /// Creates a new `ElementPath`. 11 | /// 12 | /// - Parameters: 13 | /// - element: The element index (or offset). 14 | /// - section: The section index (or offset). 15 | @inlinable 16 | public init(element: Int, section: Int) { 17 | self.element = element 18 | self.section = section 19 | } 20 | } 21 | 22 | extension ElementPath: CustomDebugStringConvertible { 23 | public var debugDescription: String { 24 | return "[element: \(element), section: \(section)]" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/Extensions/UIKitExtension.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(tvOS) 2 | import UIKit 3 | 4 | public extension UITableView { 5 | /// Applies multiple animated updates in stages using `StagedChangeset`. 6 | /// 7 | /// - Note: There are combination of changes that crash when applied simultaneously in `performBatchUpdates`. 8 | /// Assumes that `StagedChangeset` has a minimum staged changesets to avoid it. 9 | /// The data of the data-source needs to be updated synchronously before `performBatchUpdates` in every stages. 10 | /// 11 | /// - Parameters: 12 | /// - stagedChangeset: A staged set of changes. 13 | /// - animation: An option to animate the updates. 14 | /// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated 15 | /// updates should be stopped and performed reloadData. Default is nil. 16 | /// - setData: A closure that takes the collection as a parameter. 17 | /// The collection should be set to data-source of UITableView. 18 | func reload( 19 | using stagedChangeset: StagedChangeset, 20 | with animation: @autoclosure () -> RowAnimation, 21 | interrupt: ((Changeset) -> Bool)? = nil, 22 | setData: (C) -> Void 23 | ) { 24 | reload( 25 | using: stagedChangeset, 26 | deleteSectionsAnimation: animation(), 27 | insertSectionsAnimation: animation(), 28 | reloadSectionsAnimation: animation(), 29 | deleteRowsAnimation: animation(), 30 | insertRowsAnimation: animation(), 31 | reloadRowsAnimation: animation(), 32 | interrupt: interrupt, 33 | setData: setData 34 | ) 35 | } 36 | 37 | /// Applies multiple animated updates in stages using `StagedChangeset`. 38 | /// 39 | /// - Note: There are combination of changes that crash when applied simultaneously in `performBatchUpdates`. 40 | /// Assumes that `StagedChangeset` has a minimum staged changesets to avoid it. 41 | /// The data of the data-source needs to be updated synchronously before `performBatchUpdates` in every stages. 42 | /// 43 | /// - Parameters: 44 | /// - stagedChangeset: A staged set of changes. 45 | /// - deleteSectionsAnimation: An option to animate the section deletion. 46 | /// - insertSectionsAnimation: An option to animate the section insertion. 47 | /// - reloadSectionsAnimation: An option to animate the section reload. 48 | /// - deleteRowsAnimation: An option to animate the row deletion. 49 | /// - insertRowsAnimation: An option to animate the row insertion. 50 | /// - reloadRowsAnimation: An option to animate the row reload. 51 | /// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated 52 | /// updates should be stopped and performed reloadData. Default is nil. 53 | /// - setData: A closure that takes the collection as a parameter. 54 | /// The collection should be set to data-source of UITableView. 55 | func reload( 56 | using stagedChangeset: StagedChangeset, 57 | deleteSectionsAnimation: @autoclosure () -> RowAnimation, 58 | insertSectionsAnimation: @autoclosure () -> RowAnimation, 59 | reloadSectionsAnimation: @autoclosure () -> RowAnimation, 60 | deleteRowsAnimation: @autoclosure () -> RowAnimation, 61 | insertRowsAnimation: @autoclosure () -> RowAnimation, 62 | reloadRowsAnimation: @autoclosure () -> RowAnimation, 63 | interrupt: ((Changeset) -> Bool)? = nil, 64 | setData: (C) -> Void 65 | ) { 66 | if case .none = window, let data = stagedChangeset.last?.data { 67 | setData(data) 68 | return reloadData() 69 | } 70 | 71 | for changeset in stagedChangeset { 72 | if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { 73 | setData(data) 74 | return reloadData() 75 | } 76 | 77 | _performBatchUpdates { 78 | setData(changeset.data) 79 | 80 | if !changeset.sectionDeleted.isEmpty { 81 | deleteSections(IndexSet(changeset.sectionDeleted), with: deleteSectionsAnimation()) 82 | } 83 | 84 | if !changeset.sectionInserted.isEmpty { 85 | insertSections(IndexSet(changeset.sectionInserted), with: insertSectionsAnimation()) 86 | } 87 | 88 | if !changeset.sectionUpdated.isEmpty { 89 | reloadSections(IndexSet(changeset.sectionUpdated), with: reloadSectionsAnimation()) 90 | } 91 | 92 | for (source, target) in changeset.sectionMoved { 93 | moveSection(source, toSection: target) 94 | } 95 | 96 | if !changeset.elementDeleted.isEmpty { 97 | deleteRows(at: changeset.elementDeleted.map { IndexPath(row: $0.element, section: $0.section) }, with: deleteRowsAnimation()) 98 | } 99 | 100 | if !changeset.elementInserted.isEmpty { 101 | insertRows(at: changeset.elementInserted.map { IndexPath(row: $0.element, section: $0.section) }, with: insertRowsAnimation()) 102 | } 103 | 104 | if !changeset.elementUpdated.isEmpty { 105 | reloadRows(at: changeset.elementUpdated.map { IndexPath(row: $0.element, section: $0.section) }, with: reloadRowsAnimation()) 106 | } 107 | 108 | for (source, target) in changeset.elementMoved { 109 | moveRow(at: IndexPath(row: source.element, section: source.section), to: IndexPath(row: target.element, section: target.section)) 110 | } 111 | } 112 | } 113 | } 114 | 115 | private func _performBatchUpdates(_ updates: () -> Void) { 116 | if #available(iOS 11.0, tvOS 11.0, *) { 117 | performBatchUpdates(updates) 118 | } 119 | else { 120 | beginUpdates() 121 | updates() 122 | endUpdates() 123 | } 124 | } 125 | } 126 | 127 | public extension UICollectionView { 128 | /// Applies multiple animated updates in stages using `StagedChangeset`. 129 | /// 130 | /// - Note: There are combination of changes that crash when applied simultaneously in `performBatchUpdates`. 131 | /// Assumes that `StagedChangeset` has a minimum staged changesets to avoid it. 132 | /// The data of the data-source needs to be updated synchronously before `performBatchUpdates` in every stages. 133 | /// 134 | /// - Parameters: 135 | /// - stagedChangeset: A staged set of changes. 136 | /// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated 137 | /// updates should be stopped and performed reloadData. Default is nil. 138 | /// - setData: A closure that takes the collection as a parameter. 139 | /// The collection should be set to data-source of UICollectionView. 140 | func reload( 141 | using stagedChangeset: StagedChangeset, 142 | interrupt: ((Changeset) -> Bool)? = nil, 143 | setData: (C) -> Void 144 | ) { 145 | if case .none = window, let data = stagedChangeset.last?.data { 146 | setData(data) 147 | return reloadData() 148 | } 149 | 150 | for changeset in stagedChangeset { 151 | if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { 152 | setData(data) 153 | return reloadData() 154 | } 155 | 156 | performBatchUpdates({ 157 | setData(changeset.data) 158 | 159 | if !changeset.sectionDeleted.isEmpty { 160 | deleteSections(IndexSet(changeset.sectionDeleted)) 161 | } 162 | 163 | if !changeset.sectionInserted.isEmpty { 164 | insertSections(IndexSet(changeset.sectionInserted)) 165 | } 166 | 167 | if !changeset.sectionUpdated.isEmpty { 168 | reloadSections(IndexSet(changeset.sectionUpdated)) 169 | } 170 | 171 | for (source, target) in changeset.sectionMoved { 172 | moveSection(source, toSection: target) 173 | } 174 | 175 | if !changeset.elementDeleted.isEmpty { 176 | deleteItems(at: changeset.elementDeleted.map { IndexPath(item: $0.element, section: $0.section) }) 177 | } 178 | 179 | if !changeset.elementInserted.isEmpty { 180 | insertItems(at: changeset.elementInserted.map { IndexPath(item: $0.element, section: $0.section) }) 181 | } 182 | 183 | if !changeset.elementUpdated.isEmpty { 184 | reloadItems(at: changeset.elementUpdated.map { IndexPath(item: $0.element, section: $0.section) }) 185 | } 186 | 187 | for (source, target) in changeset.elementMoved { 188 | moveItem(at: IndexPath(item: source.element, section: source.section), to: IndexPath(item: target.element, section: target.section)) 189 | } 190 | }) 191 | } 192 | } 193 | } 194 | #endif 195 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/StagedChangeset.swift: -------------------------------------------------------------------------------- 1 | /// An ordered collection of `Changeset` as staged set of changes in the sectioned collection. 2 | /// 3 | /// The order is representing the stages of changesets. 4 | /// 5 | /// We know that there are combination of changes that crash when applied simultaneously 6 | /// in batch-updates of UI such as UITableView or UICollectionView. 7 | /// The `StagedChangeset` created from the two collection is split at the minimal stages 8 | /// that can be perform batch-updates with no crashes. 9 | /// 10 | /// Example for calculating differences between the two linear collections. 11 | /// 12 | /// extension String: Differentiable {} 13 | /// 14 | /// let source = ["A", "B", "C"] 15 | /// let target = ["B", "C", "D"] 16 | /// 17 | /// let changeset = StagedChangeset(source: source, target: target) 18 | /// print(changeset.isEmpty) // prints "false" 19 | /// 20 | /// Example for calculating differences between the two sectioned collections. 21 | /// 22 | /// let source = [ 23 | /// Section(model: "A", elements: ["😉"]), 24 | /// ] 25 | /// let target = [ 26 | /// Section(model: "A", elements: ["😉, 😺"]), 27 | /// Section(model: "B", elements: ["😪"]) 28 | /// ] 29 | /// 30 | /// let changeset = StagedChangeset(source: sectionedSource, target: sectionedTarget) 31 | /// print(changeset.isEmpty) // prints "false" 32 | public struct StagedChangeset { 33 | @usableFromInline 34 | internal var changesets: ContiguousArray> 35 | 36 | /// Creates a new `StagedChangeset`. 37 | /// 38 | /// - Parameters: 39 | /// - changesets: The collection of `Changeset`. 40 | @inlinable 41 | public init(_ changesets: C) where C.Element == Changeset { 42 | self.changesets = ContiguousArray(changesets) 43 | } 44 | } 45 | 46 | extension StagedChangeset: RandomAccessCollection, RangeReplaceableCollection, MutableCollection { 47 | public typealias Element = Changeset 48 | 49 | @inlinable 50 | public init() { 51 | self.init([]) 52 | } 53 | 54 | @inlinable 55 | public var startIndex: Int { 56 | return changesets.startIndex 57 | } 58 | 59 | @inlinable 60 | public var endIndex: Int { 61 | return changesets.endIndex 62 | } 63 | 64 | @inlinable 65 | public func index(after i: Int) -> Int { 66 | return changesets.index(after: i) 67 | } 68 | 69 | @inlinable 70 | public subscript(position: Int) -> Changeset { 71 | get { return changesets[position] } 72 | set { changesets[position] = newValue } 73 | } 74 | 75 | @inlinable 76 | public mutating func replaceSubrange(_ subrange: R, with newElements: C) where C.Element == Changeset, R.Bound == Int { 77 | changesets.replaceSubrange(subrange, with: newElements) 78 | } 79 | } 80 | 81 | extension StagedChangeset: Equatable where Collection: Equatable { 82 | @inlinable 83 | public static func == (lhs: StagedChangeset, rhs: StagedChangeset) -> Bool { 84 | return lhs.changesets == rhs.changesets 85 | } 86 | } 87 | 88 | extension StagedChangeset: ExpressibleByArrayLiteral { 89 | @inlinable 90 | public init(arrayLiteral elements: Changeset...) { 91 | self.init(elements) 92 | } 93 | } 94 | 95 | extension StagedChangeset: CustomDebugStringConvertible { 96 | public var debugDescription: String { 97 | guard !isEmpty else { return "[]" } 98 | 99 | return "[\n\(map { " \($0.debugDescription.split(separator: "\n").joined(separator: "\n "))" }.joined(separator: ",\n"))\n]" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Pods/Dwifft/Dwifft/AbstractDiffCalculator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractDiffCalculator.swift 3 | // Dwifft 4 | // 5 | // Created by Jack Flintermann on 12/11/17. 6 | // Copyright © 2017 jflinter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A parent class for all diff calculators. Don't use it directly. 12 | public class AbstractDiffCalculator { 13 | 14 | internal init(initialSectionedValues: SectionedValues) { 15 | self._sectionedValues = initialSectionedValues 16 | } 17 | 18 | /// The number of sections in the diff calculator. Return this inside 19 | /// `numberOfSections(in: tableView)` or `numberOfSections(in: collectionView)`. 20 | /// Don't implement that method any other way (see the docs for `numberOfObjects(inSection:)` 21 | /// for more context). 22 | public final func numberOfSections() -> Int { 23 | return self.sectionedValues.sections.count 24 | } 25 | 26 | /// The section at a given index. If you implement `tableView:titleForHeaderInSection` or 27 | /// `collectionView:viewForSupplementaryElementOfKind:atIndexPath`, you can use this 28 | /// method to get information about that section out of Dwifft. 29 | /// 30 | /// - Parameter forSection: the index of the section you care about. 31 | /// - Returns: the Section at that index. 32 | public final func value(forSection: Int) -> Section { 33 | return self.sectionedValues[forSection].0 34 | } 35 | 36 | 37 | /// The, uh, number of objects in a given section. Use this to implement 38 | /// `UITableViewDataSource.numberOfRowsInSection:` or `UICollectionViewDataSource.numberOfItemsInSection:`. 39 | /// Seriously, don't implement that method any other way - there is some subtle timing stuff 40 | /// around when this value should change in order to satisfy `UITableView`/`UICollectionView`'s internal 41 | /// assertions, that Dwifft knows how to handle correctly. Read the source for 42 | /// Dwifft+UIKit.swift if you don't believe me/want to learn more. 43 | /// 44 | /// - Parameter section: a section of your table/collection view 45 | /// - Returns: the number of objects in that section. 46 | public final func numberOfObjects(inSection section: Int) -> Int { 47 | return self.sectionedValues[section].1.count 48 | } 49 | 50 | 51 | /// The value at a given index path. Use this to implement 52 | /// `UITableViewDataSource.cellForRowAtIndexPath` or `UICollectionViewDataSource.cellForItemAtIndexPath`. 53 | /// 54 | /// - Parameter indexPath: the index path you are interested in 55 | /// - Returns: the thing at that index path 56 | public final func value(atIndexPath indexPath: IndexPath) -> Value { 57 | #if os(iOS) || os(tvOS) 58 | let row = indexPath.row 59 | #endif 60 | #if os(macOS) 61 | let row = indexPath.item 62 | #endif 63 | return self.sectionedValues[indexPath.section].1[row] 64 | } 65 | 66 | 67 | /// Set this variable to automatically trigger the correct section/row/item insertion/deletions 68 | /// on your table/collection view. 69 | public final var sectionedValues: SectionedValues { 70 | get { 71 | return _sectionedValues 72 | } 73 | set { 74 | let oldSectionedValues = sectionedValues 75 | let newSectionedValues = newValue 76 | let diff = Dwifft.diff(lhs: oldSectionedValues, rhs: newSectionedValues) 77 | if (diff.count > 0) { 78 | self.processChanges(newState: newSectionedValues, diff: diff) 79 | } 80 | } 81 | } 82 | 83 | internal static func buildSectionedValues(values: [Value], sectionIndex: Int) -> SectionedValues { 84 | let firstRows = (0.. 90 | internal func processChanges(newState: SectionedValues, diff: [SectionedDiffStep]){ 91 | fatalError("override me") 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Pods/Dwifft/Dwifft/Dwifft+AppKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dwifft+NSKit.swift 3 | // Dwifft 4 | // 5 | // Created by Jack Flintermann on 3/13/15. 6 | // Copyright (c) 2015 jflinter. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | 11 | import Cocoa 12 | 13 | /// This class manages a `NSTableView`'s rows. It will make the necessary 14 | /// calls to the table view to ensure that its UI is kept in sync with the contents of the `rows` property. 15 | public final class TableViewDiffCalculator: AbstractDiffCalculator { 16 | 17 | /// The table view to be managed 18 | public weak var tableView: NSTableView? 19 | 20 | /// All insertion/deletion calls will be made on this index. 21 | public let sectionIndex: Int 22 | 23 | /// You can change insertion/deletion animations like this! Fade works well. 24 | /// So does Top/Bottom. Left/Right/Middle are a little weird, but hey, do your thing. 25 | public var insertionAnimation = NSTableView.AnimationOptions.slideUp 26 | 27 | public var deletionAnimation = NSTableView.AnimationOptions.slideUp 28 | 29 | /// Set this variable to automatically trigger the correct row insertion/deletions 30 | /// on your table view. 31 | public var rows : [Value] { 32 | get { 33 | return self._sectionedValues[self.sectionIndex].1 34 | } 35 | set { 36 | self.sectionedValues = AbstractDiffCalculator.buildSectionedValues(values: newValue, sectionIndex: self.sectionIndex) 37 | } 38 | } 39 | 40 | /// Initializes a new diff calculator. 41 | /// 42 | /// - Parameters: 43 | /// - tableView: the table view to be managed 44 | /// - initialRows: optional - if specified, these will be the initial contents of the diff calculator. 45 | /// - sectionIndex: optional - all insertion/deletion calls will be made on this index. 46 | public init(tableView: NSTableView?, initialRows: [Value] = [], sectionIndex: Int = 0) { 47 | self.tableView = tableView 48 | self.sectionIndex = sectionIndex 49 | super.init(initialSectionedValues: AbstractDiffCalculator.buildSectionedValues(values: initialRows, sectionIndex: sectionIndex)) 50 | } 51 | 52 | override internal func processChanges(newState: SectionedValues, diff: [SectionedDiffStep]) { 53 | guard let tableView = self.tableView else { return } 54 | tableView.beginUpdates() 55 | self._sectionedValues = newState 56 | for result in diff { 57 | switch result { 58 | case let .delete(_, row, _): tableView.removeRows(at: [row], withAnimation: self.deletionAnimation) 59 | case let .insert(_, row, _): tableView.insertRows(at: [row], withAnimation: self.insertionAnimation) 60 | default: fatalError("NSTableViews do not have sections") 61 | } 62 | } 63 | tableView.endUpdates() 64 | } 65 | 66 | } 67 | 68 | /// If your collection view only has a single section, or you only want to power a single section of it with Dwifft, 69 | /// use a `SingleSectionCollectionViewDiffCalculator`. Note that this approach is not highly recommended, and you should 70 | /// do so only if it *really* doesn't make sense to just power your whole view with a `CollectionViewDiffCalculator`. 71 | /// You'll be less likely to mess up the index math :P 72 | public final class SingleSectionCollectionViewDiffCalculator { 73 | 74 | /// The collection view to be managed 75 | public weak var collectionView: NSCollectionView? 76 | 77 | /// All insertion/deletion calls will be made for items at this section. 78 | public let sectionIndex: Int 79 | 80 | /// Set this variable to automatically trigger the correct item insertion/deletions 81 | /// on your collection view. 82 | public var items : [Value] { 83 | get { 84 | return self.internalDiffCalculator.sectionedValues[self.sectionIndex].1 85 | } 86 | set { 87 | self.internalDiffCalculator.sectionedValues = AbstractDiffCalculator.buildSectionedValues(values: newValue, sectionIndex: self.sectionIndex) 88 | } 89 | } 90 | 91 | /// Initializes a new diff calculator. 92 | /// 93 | /// - Parameters: 94 | /// - tableView: the table view to be managed 95 | /// - initialItems: optional - if specified, these will be the initial contents of the diff calculator. 96 | /// - sectionIndex: optional - all insertion/deletion calls will be made on this index. 97 | public init(collectionView: NSCollectionView?, initialItems: [Value] = [], sectionIndex: Int = 0) { 98 | self.collectionView = collectionView 99 | let initialSectionedValues = AbstractDiffCalculator.buildSectionedValues(values: initialItems, sectionIndex: sectionIndex) 100 | self.internalDiffCalculator = CollectionViewDiffCalculator(collectionView: collectionView, initialSectionedValues: initialSectionedValues) 101 | self.sectionIndex = sectionIndex 102 | } 103 | 104 | private let internalDiffCalculator: CollectionViewDiffCalculator 105 | 106 | } 107 | 108 | 109 | /// This class manages a `NSCollectionView`'s items and sections. It will make the necessary 110 | /// calls to the collection view to ensure that its UI is kept in sync with the contents 111 | /// of the `sectionedValues` property. 112 | public final class CollectionViewDiffCalculator : AbstractDiffCalculator { 113 | 114 | /// The collection view to be managed. 115 | public weak var collectionView: NSCollectionView? 116 | 117 | /// Initializes a new diff calculator. 118 | /// 119 | /// - Parameters: 120 | /// - collectionView: the collection view to be managed. 121 | /// - initialSectionedValues: optional - if specified, these will be the initial contents of the diff calculator. 122 | public init(collectionView: NSCollectionView?, initialSectionedValues: SectionedValues = SectionedValues()) { 123 | self.collectionView = collectionView 124 | super.init(initialSectionedValues: initialSectionedValues) 125 | } 126 | 127 | override internal func processChanges(newState: SectionedValues, diff: [SectionedDiffStep]) { 128 | guard let collectionView = self.collectionView else { return } 129 | collectionView.animator().performBatchUpdates({ 130 | self._sectionedValues = newState 131 | for result in diff { 132 | switch result { 133 | case let .delete(section, item, _): collectionView.deleteItems(at: [IndexPath(item: item, section: section)]) 134 | case let .insert(section, item, _): collectionView.insertItems(at: [IndexPath(item: item, section: section)]) 135 | case let .sectionDelete(section, _): collectionView.deleteSections(IndexSet(integer: section)) 136 | case let .sectionInsert(section, _): 137 | // NSCollectionViews don't seem to like it when inserting sections beyond numberOfSections 138 | // so adjust for that 139 | if section > collectionView.numberOfSections { 140 | collectionView.insertSections(IndexSet(integer: collectionView.numberOfSections)) 141 | } else { 142 | collectionView.insertSections(IndexSet(integer: section)) 143 | } 144 | } 145 | } 146 | }, completionHandler: nil) 147 | } 148 | } 149 | 150 | /// Let's say your data model consists of different sections containing different model types. Since 151 | /// `SectionedValues` requires a uniform type for all of its rows, this can be a clunky situation. You 152 | /// can address this in a couple of ways. The first is to define a custom enum that encompasses all of the 153 | /// things that *could* be in your data model - if section 1 has a bunch of `String`s, and section 2 has a bunch 154 | /// of `Int`s, define a `StringOrInt` enum that conforms to `Equatable`, and fill the `SectionedValues` 155 | /// that you use to drive your DiffCalculator up with those. Alternatively, if you are lazy, and your 156 | /// models all conform to `Hashable`, you can use a SimpleTableViewDiffCalculator instead. 157 | typealias SimpleCollectionViewDiffCalculator = CollectionViewDiffCalculator 158 | 159 | #endif 160 | -------------------------------------------------------------------------------- /Pods/Dwifft/Dwifft/Dwifft+UIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dwifft+UIKit.swift 3 | // Dwifft 4 | // 5 | // Created by Jack Flintermann on 3/13/15. 6 | // Copyright (c) 2015 jflinter. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | 11 | import UIKit 12 | /// This class manages a `UITableView`'s rows and sections. It will make the necessary calls to 13 | /// the table view to ensure that its UI is kept in sync with the contents of the `sectionedValues` property. 14 | public final class TableViewDiffCalculator: AbstractDiffCalculator { 15 | 16 | /// The table view to be managed 17 | public weak var tableView: UITableView? 18 | 19 | /// Initializes a new diff calculator. 20 | /// 21 | /// - Parameters: 22 | /// - tableView: the table view to be managed 23 | /// - initialSectionedValues: optional - if specified, these will be the initial contents of the diff calculator. 24 | public init(tableView: UITableView?, initialSectionedValues: SectionedValues = SectionedValues()) { 25 | self.tableView = tableView 26 | super.init(initialSectionedValues: initialSectionedValues) 27 | } 28 | 29 | /// You can change insertion/deletion animations like this! Fade works well. 30 | /// So does Top/Bottom. Left/Right/Middle are a little weird, but hey, do your thing. 31 | public var insertionAnimation = UITableView.RowAnimation.automatic, deletionAnimation = UITableView.RowAnimation.automatic 32 | 33 | override internal func processChanges(newState: SectionedValues, diff: [SectionedDiffStep]) { 34 | guard let tableView = self.tableView else { return } 35 | tableView.beginUpdates() 36 | self._sectionedValues = newState 37 | for result in diff { 38 | switch result { 39 | case let .delete(section, row, _): tableView.deleteRows(at: [IndexPath(row: row, section: section)], with: self.deletionAnimation) 40 | case let .insert(section, row, _): tableView.insertRows(at: [IndexPath(row: row, section: section)], with: self.insertionAnimation) 41 | case let .sectionDelete(section, _): tableView.deleteSections(IndexSet(integer: section), with: self.deletionAnimation) 42 | case let .sectionInsert(section, _): tableView.insertSections(IndexSet(integer: section), with: self.insertionAnimation) 43 | } 44 | } 45 | tableView.endUpdates() 46 | } 47 | } 48 | 49 | /// This class manages a `UICollectionView`'s items and sections. It will make the necessary 50 | /// calls to the collection view to ensure that its UI is kept in sync with the contents 51 | /// of the `sectionedValues` property. 52 | public final class CollectionViewDiffCalculator : AbstractDiffCalculator { 53 | 54 | /// The collection view to be managed. 55 | public weak var collectionView: UICollectionView? 56 | 57 | /// Initializes a new diff calculator. 58 | /// 59 | /// - Parameters: 60 | /// - collectionView: the collection view to be managed. 61 | /// - initialSectionedValues: optional - if specified, these will be the initial contents of the diff calculator. 62 | public init(collectionView: UICollectionView?, initialSectionedValues: SectionedValues = SectionedValues()) { 63 | self.collectionView = collectionView 64 | super.init(initialSectionedValues: initialSectionedValues) 65 | } 66 | 67 | override internal func processChanges(newState: SectionedValues, diff: [SectionedDiffStep]) { 68 | guard let collectionView = self.collectionView else { return } 69 | collectionView.performBatchUpdates({ 70 | self._sectionedValues = newState 71 | for result in diff { 72 | switch result { 73 | case let .delete(section, row, _): collectionView.deleteItems(at: [IndexPath(row: row, section: section)]) 74 | case let .insert(section, row, _): collectionView.insertItems(at: [IndexPath(row: row, section: section)]) 75 | case let .sectionDelete(section, _): collectionView.deleteSections(IndexSet(integer: section)) 76 | case let .sectionInsert(section, _): collectionView.insertSections(IndexSet(integer: section)) 77 | } 78 | } 79 | }, completion: nil) 80 | } 81 | } 82 | 83 | /// Let's say your data model consists of different sections containing different model types. Since 84 | /// `SectionedValues` requires a uniform type for all of its rows, this can be a clunky situation. You 85 | /// can address this in a couple of ways. The first is to define a custom enum that encompasses all of the 86 | /// things that *could* be in your data model - if section 1 has a bunch of `String`s, and section 2 has a bunch 87 | /// of `Int`s, define a `StringOrInt` enum that conforms to `Equatable`, and fill the `SectionedValues` 88 | /// that you use to drive your DiffCalculator up with those. Alternatively, if you are lazy, and your 89 | /// models all conform to `Hashable`, you can use a SimpleTableViewDiffCalculator instead. 90 | typealias SimpleTableViewDiffCalculator = TableViewDiffCalculator 91 | 92 | /// See SimpleTableViewDiffCalculator for explanation 93 | typealias SimpleCollectionViewDiffCalculator = CollectionViewDiffCalculator 94 | 95 | /// If your table view only has a single section, or you only want to power a single section of it with Dwifft, 96 | /// use a `SingleSectionTableViewDiffCalculator`. Note that this approach is not highly recommended, and you should 97 | /// do so only if it *really* doesn't make sense to just power your whole table with a `TableViewDiffCalculator`. 98 | /// You'll be less likely to mess up the index math :P 99 | public final class SingleSectionTableViewDiffCalculator { 100 | 101 | /// The table view to be managed 102 | public weak var tableView: UITableView? 103 | 104 | /// All insertion/deletion calls will be made on this index. 105 | public let sectionIndex: Int 106 | 107 | /// You can change insertion/deletion animations like this! Fade works well. 108 | /// So does Top/Bottom. Left/Right/Middle are a little weird, but hey, do your thing. 109 | public var insertionAnimation = UITableView.RowAnimation.automatic { 110 | didSet { 111 | self.internalDiffCalculator.insertionAnimation = self.insertionAnimation 112 | } 113 | } 114 | 115 | public var deletionAnimation = UITableView.RowAnimation.automatic { 116 | didSet { 117 | self.internalDiffCalculator.deletionAnimation = self.deletionAnimation 118 | } 119 | } 120 | 121 | /// Set this variable to automatically trigger the correct row insertion/deletions 122 | /// on your table view. 123 | public var rows : [Value] { 124 | get { 125 | return self.internalDiffCalculator.sectionedValues[self.sectionIndex].1 126 | } 127 | set { 128 | self.internalDiffCalculator.sectionedValues = AbstractDiffCalculator.buildSectionedValues(values: newValue, sectionIndex: self.sectionIndex) 129 | } 130 | } 131 | 132 | /// Initializes a new diff calculator. 133 | /// 134 | /// - Parameters: 135 | /// - tableView: the table view to be managed 136 | /// - initialRows: optional - if specified, these will be the initial contents of the diff calculator. 137 | /// - sectionIndex: optional - all insertion/deletion calls will be made on this index. 138 | public init(tableView: UITableView?, initialRows: [Value] = [], sectionIndex: Int = 0) { 139 | self.tableView = tableView 140 | let initialSectionedValues = AbstractDiffCalculator.buildSectionedValues(values: initialRows, sectionIndex: sectionIndex) 141 | self.internalDiffCalculator = TableViewDiffCalculator(tableView: tableView, initialSectionedValues: initialSectionedValues) 142 | self.sectionIndex = sectionIndex 143 | } 144 | 145 | private let internalDiffCalculator: TableViewDiffCalculator 146 | 147 | } 148 | 149 | /// If your collection view only has a single section, or you only want to power a single section of it with Dwifft, 150 | /// use a `SingleSectionCollectionViewDiffCalculator`. Note that this approach is not highly recommended, and you should 151 | /// do so only if it *really* doesn't make sense to just power your whole view with a `CollectionViewDiffCalculator`. 152 | /// You'll be less likely to mess up the index math :P 153 | public final class SingleSectionCollectionViewDiffCalculator { 154 | 155 | /// The collection view to be managed 156 | public weak var collectionView: UICollectionView? 157 | 158 | /// All insertion/deletion calls will be made for items at this section. 159 | public let sectionIndex: Int 160 | 161 | /// Set this variable to automatically trigger the correct item insertion/deletions 162 | /// on your collection view. 163 | public var items : [Value] { 164 | get { 165 | return self.internalDiffCalculator.sectionedValues[self.sectionIndex].1 166 | } 167 | set { 168 | self.internalDiffCalculator.sectionedValues = AbstractDiffCalculator.buildSectionedValues(values: newValue, sectionIndex: self.sectionIndex) 169 | } 170 | } 171 | 172 | /// Initializes a new diff calculator. 173 | /// 174 | /// - Parameters: 175 | /// - tableView: the table view to be managed 176 | /// - initialItems: optional - if specified, these will be the initial contents of the diff calculator. 177 | /// - sectionIndex: optional - all insertion/deletion calls will be made on this index. 178 | public init(collectionView: UICollectionView?, initialItems: [Value] = [], sectionIndex: Int = 0) { 179 | self.collectionView = collectionView 180 | self.internalDiffCalculator = CollectionViewDiffCalculator(collectionView: collectionView, initialSectionedValues: AbstractDiffCalculator.buildSectionedValues(values: initialItems, sectionIndex: sectionIndex)) 181 | self.sectionIndex = sectionIndex 182 | } 183 | 184 | private let internalDiffCalculator: CollectionViewDiffCalculator 185 | 186 | } 187 | 188 | #endif 189 | -------------------------------------------------------------------------------- /Pods/Dwifft/Dwifft/SectionedValues.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionedValues.swift 3 | // Dwifft 4 | // 5 | // Created by Jack Flintermann on 4/14/17. 6 | // Copyright © 2017 jflinter. All rights reserved. 7 | // 8 | 9 | /// SectionedValues represents, well, a bunch of sections and their associated values. 10 | /// You can think of it sort of like an "ordered dictionary", or an order of key-pairs. 11 | /// If you are diffing a multidimensional structure of values (what might normally be, 12 | /// for example, a 2D array), you will want to use this. 13 | public struct SectionedValues: Equatable { 14 | 15 | /// Initializes the struct with an array of key-pairs. 16 | /// 17 | /// - Parameter sectionsAndValues: An array of tuples. The first element in the tuple is 18 | /// the value of the section. The second element is an array of values to be associated with 19 | /// that section. Ordering matters, obviously. Note, it's totally ok if `sectionsAndValues` 20 | /// contains duplicate sections (or duplicate values within those sections). 21 | public init(_ sectionsAndValues: [(Section, [Value])] = []) { 22 | self.sectionsAndValues = sectionsAndValues 23 | } 24 | 25 | /// The underlying tuples contained in the receiver 26 | public let sectionsAndValues: [(Section, [Value])] 27 | 28 | internal var sections: [Section] { get { return self.sectionsAndValues.map { $0.0 } } } 29 | internal subscript(i: Int) -> (Section, [Value]) { 30 | return self.sectionsAndValues[i] 31 | } 32 | 33 | 34 | /// Returns a new SectionedValues appending a new key-value pair. I think this might be useful 35 | /// if you're building up a SectionedValues conditionally? (Well, I hope it is, anyway.) 36 | /// 37 | /// - Parameter sectionAndValue: the new key-value pair 38 | /// - Returns: a new SectionedValues containing the receiever's contents plus the new pair. 39 | public func appending(sectionAndValue: (Section, [Value])) -> SectionedValues { 40 | return SectionedValues(self.sectionsAndValues + [sectionAndValue]) 41 | } 42 | 43 | /// Compares two `SectionedValues` instances 44 | public static func ==(lhs: SectionedValues, rhs: SectionedValues) -> Bool { 45 | guard lhs.sectionsAndValues.count == rhs.sectionsAndValues.count else { return false } 46 | for i in 0..<(lhs.sectionsAndValues.count) { 47 | let ltuple = lhs.sectionsAndValues[i] 48 | let rtuple = rhs.sectionsAndValues[i] 49 | if (ltuple.0 != rtuple.0 || ltuple.1 != rtuple.1) { 50 | return false 51 | } 52 | } 53 | return true 54 | } 55 | } 56 | 57 | // MARK: - Custom grouping 58 | public extension SectionedValues where Section: Hashable { 59 | 60 | /// This is a convenience initializer of sorts for `SectionedValues`. It acknowledges 61 | /// that sometimes you have an array of things that are naturally "groupable" - maybe 62 | /// a list of names in an address book, that can be grouped into their first initial, 63 | /// or a bunch of events that can be grouped into buckets of timestamps. This will handle 64 | /// clumping all of your values into the correct sections, and ordering everything correctly. 65 | /// 66 | /// - Parameters: 67 | /// - values: All of the values that will end up in the `SectionedValues` you're making. 68 | /// - valueToSection: A function that maps each value to the section it will inhabit. 69 | /// In the above examples, this would take a name and return its first initial, 70 | /// or take an event and return its bucketed timestamp. 71 | /// - sortSections: A function that compares two sections, and returns true if the first 72 | /// should be sorted before the second. Used to sort the sections in the returned `SectionedValues`. 73 | /// - sortValues: A function that compares two values, and returns true if the first 74 | /// should be sorted before the second. Used to sort the values in each section of the returned `SectionedValues`. 75 | public init( 76 | values: [Value], 77 | valueToSection: ((Value) -> Section), 78 | sortSections: ((Section, Section) -> Bool), 79 | sortValues: ((Value, Value) -> Bool)) { 80 | var dictionary = [Section: [Value]]() 81 | for value in values { 82 | let section = valueToSection(value) 83 | var current = dictionary[section] ?? [] 84 | current.append(value) 85 | dictionary[section] = current 86 | } 87 | let sortedSections = dictionary.keys.sorted(by: sortSections) 88 | self.init(sortedSections.map { section in 89 | let values = dictionary[section] ?? [] 90 | let sortedValues = values.sorted(by: sortValues) 91 | return (section, sortedValues) 92 | }) 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /Pods/Dwifft/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jack Flintermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Pods/Dwifft/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/travis/jflinter/Dwifft/master.svg)](https://travis-ci.org/jflinter/Dwifft) 2 | ![Current Version](https://img.shields.io/github/tag/jflinter/dwifft.svg?label=Current%20Version) 3 | 4 | Dwifft! 5 | === 6 | 7 | In 10 seconds 8 | --- 9 | Dwifft is a small Swift library that tells you what the "diff" is between two collections, namely, the series of "edit operations" required to turn one into the other. It also comes with UIKit bindings, to automatically, animatedly keep a UITableView/UICollectionView in sync with a piece of data by making the necessary row/section insertion/deletion calls for you as the data changes. 10 | 11 | Longer version 12 | --- 13 | Dwifft is a Swift library that does two things. The first thing sounds interesting but perhaps only abstractly useful, and the other thing is a very concretely useful thing based off the first thing. 14 | 15 | The first thing (found in `Dwifft.swift`) is an algorithm that calculates the diff between two collections using the [Longest Common Subsequence method](https://en.wikipedia.org/wiki/Longest_common_subsequence_problem). If this kind of thing is interesting to you, there's a pretty great paper on diffing algorithms: http://www.xmailserver.org/diff2.pdf 16 | 17 | The second thing (found in `Dwifft+UIKit.swift`) is a series of diff calculators for `UITableView`s and `UICollectionView`s. Let's say you have a `UITableView` that's backed by a simple array of values (like a list of names, e.g. `["Alice", "Bob", "Carol"]`. If that array changes (maybe Bob leaves, and is replaced by Dave, so our list is now `["Alice, "Carol", "Dave"]`), we'll want to update the table. The easiest way to do this is by calling `reloadData` on it. This has a couple of downsides: the transition isn't animated, and it'll cause your user to lose their scroll position if they've scrolled the table. The nicer way is to use the `insertRowsAtIndexPaths:withRowAnimation` and `deleteRowsAtIndexPaths:withRowAnimation` methods on `UITableView`, but this requires you to figure out which index paths have changed in your array (in our example, you'd have to figure out that the row at index 1 should be removed, and a new row should be inserted at index 2 should then be added). If only we had a way to diff the previous value of our array with it's new value. Wait a minute. 18 | 19 | When you wire up a `TableViewDiffCalculator` to your `UITableView` (or a `CollectionViewDiffCalculator` to your `UICollectionView`, it'll _automatically_ calculate diffs and trigger the necessary animations on it whenever you change its `sectionedValues` property. Neat, right? Notably, as of Dwifft 0.6, Dwifft will also figure out _section_ insertions and deletions, as well as how to efficiently insert and delete rows across different sections, which is just so massively useful if you have a multi-section table. If you're currently using a <0.6 version of Dwifft and want to do this, read the [0.6 release notes](https://github.com/jflinter/Dwifft/releases/tag/0.6). 20 | 21 | Even longer version 22 | --- 23 | Learn more about the history of Dwifft, and how it works, in this [exciting video of a talk](https://vimeo.com/211194798) recorded at the Brooklyn Swift meetup in March 2017. 24 | 25 | Why you should use Dwifft 26 | --- 27 | - Dwifft is *useful* - it can help you build a substantially better user experience if you have table/collection views with dynamic content in your app. 28 | - Dwifft is *safe* - there is some non-trivial index math inside of this diff algorithm that is easy to screw up. Dwifft has 100% test coverage on all of its core algorithms. Additionally, all of Dwifft's core functionality is tested with [SwiftCheck](https://github.com/typelift/SwiftCheck), meaning it has been shown to behave correctly under an exhausting set of inputs and edge cases. 29 | - Dwifft is *fast* - a lot of time has been spent making Dwifft considerably (many orders of magnitude) faster than a naïve implementation. It almost certainly won't be the bottleneck in your UI code. 30 | - Dwifft is *small* - Dwifft believes (to the extent that a software library can "believe" in things) in the unix philosophy of small, easily-composed tools. It's unopinionated and flexible enough to fit into most apps, and leaves a lot of control in your hands as a developer. As such, you can probably cram it into your app in less than 5 minutes. Also, because it's small, it can actually achieve nice goals like 100% test and documentation coverage. 31 | 32 | How to get started 33 | --- 34 | - First, you should take a look at the example app, to get a feel for how Dwifft is meant to be used. 35 | - Next, you should just sit down and read the [entire documentation](https://www.jackflintermann.com/Dwifft) - it will take you <10 minutes, and you'll leave knowing everything there is to know about Dwifft. 36 | - Then, install Dwifft via cocoapods or carthage or whatever people are using these days. 37 | - Then get to Dwiffing. 38 | 39 | Contributing 40 | --- 41 | Contributions are welcome, with some caveats - please read the [contributing guidelines](https://github.com/jflinter/Dwifft/blob/master/CONTRIBUTING.md) before opening a PR to avoid wasting both our time. 42 | 43 | Ok, that's it, there's nothing more here. -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Differ (1.4.3) 3 | - DifferenceKit (1.1.3): 4 | - DifferenceKit/Core (= 1.1.3) 5 | - DifferenceKit/UIKitExtension (= 1.1.3) 6 | - DifferenceKit/Core (1.1.3) 7 | - DifferenceKit/UIKitExtension (1.1.3): 8 | - DifferenceKit/Core 9 | - Dwifft (0.9) 10 | 11 | DEPENDENCIES: 12 | - Differ 13 | - DifferenceKit 14 | - Dwifft 15 | 16 | SPEC REPOS: 17 | https://github.com/cocoapods/specs.git: 18 | - Differ 19 | - DifferenceKit 20 | - Dwifft 21 | 22 | SPEC CHECKSUMS: 23 | Differ: 6c7477d6187e8c36d02ec342a3c321061b85a0ea 24 | DifferenceKit: 5018791b6c1fc839921a3c171a0a539ace6ea60c 25 | Dwifft: 42912068ed2a8146077d1a1404df18625bd086e1 26 | 27 | PODFILE CHECKSUM: b38c134b980325949f29003cc8867113169b9422 28 | 29 | COCOAPODS: 1.7.3 30 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Differ/Differ-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.4.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Differ/Differ-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Differ : NSObject 3 | @end 4 | @implementation PodsDummy_Differ 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Differ/Differ-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Differ/Differ-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double DifferVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char DifferVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Differ/Differ.modulemap: -------------------------------------------------------------------------------- 1 | framework module Differ { 2 | umbrella header "Differ-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Differ/Differ.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Differ 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/Differ 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Differ/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.4.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.1.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_DifferenceKit : NSObject 3 | @end 4 | @implementation PodsDummy_DifferenceKit 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double DifferenceKitVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char DifferenceKitVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit.modulemap: -------------------------------------------------------------------------------- 1 | framework module DifferenceKit { 2 | umbrella header "DifferenceKit-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_LDFLAGS = $(inherited) -framework "UIKit" 4 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/DifferenceKit 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.1.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Dwifft/Dwifft-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 | 0.9.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Dwifft/Dwifft-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Dwifft : NSObject 3 | @end 4 | @implementation PodsDummy_Dwifft 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Dwifft/Dwifft-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Dwifft/Dwifft-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double DwifftVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char DwifftVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Dwifft/Dwifft.modulemap: -------------------------------------------------------------------------------- 1 | framework module Dwifft { 2 | umbrella header "Dwifft-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Dwifft/Dwifft.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Dwifft 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/Dwifft 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Dwifft/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 | 0.9.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_TetrisDiffingCompetition : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_TetrisDiffingCompetition 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-frameworks-Debug-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/Differ/Differ.framework 3 | ${BUILT_PRODUCTS_DIR}/DifferenceKit/DifferenceKit.framework 4 | ${BUILT_PRODUCTS_DIR}/Dwifft/Dwifft.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-frameworks-Debug-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Differ.framework 2 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DifferenceKit.framework 3 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Dwifft.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-frameworks-Release-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/Differ/Differ.framework 3 | ${BUILT_PRODUCTS_DIR}/DifferenceKit/DifferenceKit.framework 4 | ${BUILT_PRODUCTS_DIR}/Dwifft/Dwifft.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-frameworks-Release-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Differ.framework 2 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DifferenceKit.framework 3 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Dwifft.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | 23 | # Used as a return value for each invocation of `strip_invalid_archs` function. 24 | STRIP_BINARY_RETVAL=0 25 | 26 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 27 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 28 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 29 | 30 | # Copies and strips a vendored framework 31 | install_framework() 32 | { 33 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 34 | local source="${BUILT_PRODUCTS_DIR}/$1" 35 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 36 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 37 | elif [ -r "$1" ]; then 38 | local source="$1" 39 | fi 40 | 41 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 42 | 43 | if [ -L "${source}" ]; then 44 | echo "Symlinked..." 45 | source="$(readlink "${source}")" 46 | fi 47 | 48 | # Use filter instead of exclude so missing patterns don't throw errors. 49 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 50 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 51 | 52 | local basename 53 | basename="$(basename -s .framework "$1")" 54 | binary="${destination}/${basename}.framework/${basename}" 55 | 56 | if ! [ -r "$binary" ]; then 57 | binary="${destination}/${basename}" 58 | elif [ -L "${binary}" ]; then 59 | echo "Destination binary is symlinked..." 60 | dirname="$(dirname "${binary}")" 61 | binary="${dirname}/$(readlink "${binary}")" 62 | fi 63 | 64 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 65 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 66 | strip_invalid_archs "$binary" 67 | fi 68 | 69 | # Resign the code if required by the build settings to avoid unstable apps 70 | code_sign_if_enabled "${destination}/$(basename "$1")" 71 | 72 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 73 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 74 | local swift_runtime_libs 75 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 76 | for lib in $swift_runtime_libs; do 77 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 78 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 79 | code_sign_if_enabled "${destination}/${lib}" 80 | done 81 | fi 82 | } 83 | 84 | # Copies and strips a vendored dSYM 85 | install_dsym() { 86 | local source="$1" 87 | if [ -r "$source" ]; then 88 | # Copy the dSYM into a the targets temp dir. 89 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 90 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 91 | 92 | local basename 93 | basename="$(basename -s .framework.dSYM "$source")" 94 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 95 | 96 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 97 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 98 | strip_invalid_archs "$binary" 99 | fi 100 | 101 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 102 | # Move the stripped file into its final destination. 103 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 104 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 105 | else 106 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 107 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 108 | fi 109 | fi 110 | } 111 | 112 | # Copies the bcsymbolmap files of a vendored framework 113 | install_bcsymbolmap() { 114 | local bcsymbolmap_path="$1" 115 | local destination="${BUILT_PRODUCTS_DIR}" 116 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 117 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 118 | } 119 | 120 | # Signs a framework with the provided identity 121 | code_sign_if_enabled() { 122 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 123 | # Use the current code_sign_identity 124 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 125 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 126 | 127 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 128 | code_sign_cmd="$code_sign_cmd &" 129 | fi 130 | echo "$code_sign_cmd" 131 | eval "$code_sign_cmd" 132 | fi 133 | } 134 | 135 | # Strip invalid architectures 136 | strip_invalid_archs() { 137 | binary="$1" 138 | # Get architectures for current target binary 139 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 140 | # Intersect them with the architectures we are building for 141 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 142 | # If there are no archs supported by this binary then warn the user 143 | if [[ -z "$intersected_archs" ]]; then 144 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 145 | STRIP_BINARY_RETVAL=0 146 | return 147 | fi 148 | stripped="" 149 | for arch in $binary_archs; do 150 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 151 | # Strip non-valid architectures in-place 152 | lipo -remove "$arch" -output "$binary" "$binary" 153 | stripped="$stripped $arch" 154 | fi 155 | done 156 | if [[ "$stripped" ]]; then 157 | echo "Stripped $binary of architectures:$stripped" 158 | fi 159 | STRIP_BINARY_RETVAL=1 160 | } 161 | 162 | 163 | if [[ "$CONFIGURATION" == "Debug" ]]; then 164 | install_framework "${BUILT_PRODUCTS_DIR}/Differ/Differ.framework" 165 | install_framework "${BUILT_PRODUCTS_DIR}/DifferenceKit/DifferenceKit.framework" 166 | install_framework "${BUILT_PRODUCTS_DIR}/Dwifft/Dwifft.framework" 167 | fi 168 | if [[ "$CONFIGURATION" == "Release" ]]; then 169 | install_framework "${BUILT_PRODUCTS_DIR}/Differ/Differ.framework" 170 | install_framework "${BUILT_PRODUCTS_DIR}/DifferenceKit/DifferenceKit.framework" 171 | install_framework "${BUILT_PRODUCTS_DIR}/Dwifft/Dwifft.framework" 172 | fi 173 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 174 | wait 175 | fi 176 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH+x} ]; then 7 | # If UNLOCALIZED_RESOURCES_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # resources to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 13 | 14 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 15 | > "$RESOURCES_TO_COPY" 16 | 17 | XCASSET_FILES=() 18 | 19 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 20 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 21 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 22 | 23 | case "${TARGETED_DEVICE_FAMILY:-}" in 24 | 1,2) 25 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 26 | ;; 27 | 1) 28 | TARGET_DEVICE_ARGS="--target-device iphone" 29 | ;; 30 | 2) 31 | TARGET_DEVICE_ARGS="--target-device ipad" 32 | ;; 33 | 3) 34 | TARGET_DEVICE_ARGS="--target-device tv" 35 | ;; 36 | 4) 37 | TARGET_DEVICE_ARGS="--target-device watch" 38 | ;; 39 | *) 40 | TARGET_DEVICE_ARGS="--target-device mac" 41 | ;; 42 | esac 43 | 44 | install_resource() 45 | { 46 | if [[ "$1" = /* ]] ; then 47 | RESOURCE_PATH="$1" 48 | else 49 | RESOURCE_PATH="${PODS_ROOT}/$1" 50 | fi 51 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 52 | cat << EOM 53 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 54 | EOM 55 | exit 1 56 | fi 57 | case $RESOURCE_PATH in 58 | *.storyboard) 59 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 60 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 61 | ;; 62 | *.xib) 63 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 64 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 65 | ;; 66 | *.framework) 67 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 68 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 69 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 70 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 71 | ;; 72 | *.xcdatamodel) 73 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true 74 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 75 | ;; 76 | *.xcdatamodeld) 77 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true 78 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 79 | ;; 80 | *.xcmappingmodel) 81 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true 82 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 83 | ;; 84 | *.xcassets) 85 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 86 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 87 | ;; 88 | *) 89 | echo "$RESOURCE_PATH" || true 90 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 91 | ;; 92 | esac 93 | } 94 | 95 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 97 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 98 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 99 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 100 | fi 101 | rm -f "$RESOURCES_TO_COPY" 102 | 103 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ] 104 | then 105 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 106 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 107 | while read line; do 108 | if [[ $line != "${PODS_ROOT}*" ]]; then 109 | XCASSET_FILES+=("$line") 110 | fi 111 | done <<<"$OTHER_XCASSETS" 112 | 113 | if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then 114 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 115 | else 116 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist" 117 | fi 118 | fi 119 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_TetrisDiffingCompetitionVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_TetrisDiffingCompetitionVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Differ" "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" "${PODS_CONFIGURATION_BUILD_DIR}/Dwifft" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Differ/Differ.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Dwifft/Dwifft.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "Differ" -framework "DifferenceKit" -framework "Dwifft" -framework "UIKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_TetrisDiffingCompetition { 2 | umbrella header "Pods-TetrisDiffingCompetition-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-TetrisDiffingCompetition/Pods-TetrisDiffingCompetition.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Differ" "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" "${PODS_CONFIGURATION_BUILD_DIR}/Dwifft" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Differ/Differ.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Dwifft/Dwifft.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "Differ" -framework "DifferenceKit" -framework "Dwifft" -framework "UIKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Tetris Diffing Competition 2 | 3 | This is a comparison of diffing frameworks for UIKit table views and collection views. I wrote a blog post called [Collection View Tetris][cwt], where I presented a fun hack: Tetris implemented using a quite ordinary Collection View and its default animation. This requires that all possible animations are implemented – inserts, deletes and moves of items and sections, including moving items from one section to another. 4 | 5 | After writing that, I got curious: how do existing diffing libs meet the task of being the diffing engine from Collection View Tetris? I did this little comparison of existing diffing libs [in September 2018][tetris-diffing]. Then, in 2019, Apple announced their own diffable data sources to be released with iOS 13. I recommend the WWDC talk ["Advances in UI Data Sources"][wwdc] for a description of this API. 6 | 7 | ## The (slightly ad hoc) rules 8 | 9 | There is a whole bunch of diffing frameworks available for iOS, but to be eligble for the Tetris Diffing Competition, they have to meet the following: 10 | 11 | * There has to be a diffing API that handles both items and sections. 12 | * There has to also be a method for running the whole animation – as you can read about in my blog post, supporting all of this is non-trivial and requires more than the diffing itself. 13 | * I have to understand how to use it in reasonable time. 14 | 15 | Three of the libraries I found in 2018 seemed to fit the criteria. In 2019, UIKit itself was added. 16 | 17 | ## The results 18 | 19 | |Framework |Version |Result|Comment | 20 | |:-------------------|:-------:|:----:|:--------------------------------------------| 21 | |[UIKit][uikit] |13 beta 2| 🎉 |Works just as intended. 22 | |[DifferenceKit][dk] |1.1.3 | 🎉 |Works just as intended. | 23 | |[Dwifft][dwifft] |0.9 | 😐 |Works, but does not seem to animate moves. | 24 | |[Differ][differ] |1.4.3 | 😞 |Does not work. | 25 | 26 | Out of the third party frameworks, the clear winner here is DifferenceKit, which was the only library that truly passed the test. Whether or not you want to take this as an indicator of general quality of the framework, I'm not sure – but I happen to think that this library does seem like a good choice for your general purpose diffing needs. It has a flexible and powerful API, uses a performant algorithm and is well documented. 27 | 28 | There are some things I think could be improved. Notably, it does not allow you to pass on a completion handler to the `performBatchUpdates` methods. This seem to be something it has in common with many other of these frameworks; indeed, not even UIKit's UICollectionViewDiffableDataSource 29 | 30 | **My advice** would be, for any app that is able to use iOS 13 as deployment target, to use UIKit's own diffable data sources. Until then, use DifferenceKit. 31 | 32 | Oh, I should mention that there is one more diffing framework that is up to the task – my own [SKRBatchUpdates](https://github.com/skagedal/SKRBatchUpdates), which is what led me to do this Tetris thing. It is, however, not packaged for general consumtion as a framework – any of these other links here will quite probably better fulfill your diffing needs. 33 | 34 | ## Other diffing frameworks 35 | 36 | Here's another nice thing about [DifferenceKit][dk]: its README has a nice comparison of available diffing frameworks. So I will refer you to that. It may very well be another one that best matches your needs. 37 | 38 | ## Prove me wrong with a Pull Request! 39 | 40 | This repository contains an app where you can test Collection View Tetris with each of the tested frameworks. If you find that I'm being unfair towards some framework, using it wrong, or if there's just something missing – please file a pull request. The competition is always up for re-evaluation. 41 | 42 | [wwdc]: https://developer.apple.com/videos/play/wwdc2019/220/ 43 | [uikit]: https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource 44 | [cwt]: https://blog.skagedal.tech/2018/08/23/collection-view-tetris.html 45 | [tetris-diffing]: https://blog.skagedal.tech/2018/09/28/tetris-diffing-competition.html 46 | [dk]: https://github.com/ra1028/DifferenceKit 47 | [dwifft]: https://github.com/jflinter/Dwifft 48 | [differ]: https://github.com/tonyarnold/Differ 49 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Adapters/Differ/DifferTetrisAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | import Differ 7 | 8 | class DifferTetrisAdapter: NSObject, TetrisAdapter, UICollectionViewDataSource { 9 | let name = "Differ" 10 | let comment = """ 11 | I didn't get this one to work at all. I think I'm calling it correctly, \ 12 | but nothing shows. 13 | """ 14 | 15 | func configure(collectionView: UICollectionView, cellProvider: @escaping (UICollectionView, IndexPath) -> UICollectionViewCell) { 16 | self.collectionView = collectionView 17 | collectionView.dataSource = self 18 | self.cellProvider = cellProvider 19 | } 20 | 21 | private var collectionView: UICollectionView! 22 | private var cellProvider: ((UICollectionView, IndexPath) -> UICollectionViewCell)! 23 | 24 | var board = TetrisBoard(rows: []) 25 | func setBoard(_ tetrisBoard: TetrisBoard) { 26 | self.board = tetrisBoard 27 | } 28 | 29 | func animateBoard(to board: TetrisBoard, completion: ((Bool) -> Void)?) { 30 | collectionView.animateItemAndSectionChanges(oldData: self.board.rows, 31 | newData: board.rows, 32 | completion: completion) 33 | } 34 | 35 | // MARK: UICollectionViewDataSource 36 | 37 | func numberOfSections(in collectionView: UICollectionView) -> Int { 38 | return board.rows.count 39 | } 40 | 41 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 42 | return board.rows[section].blocks.count 43 | } 44 | 45 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 46 | let cell = cellProvider(collectionView, indexPath) 47 | let block = board.rows[indexPath.section].blocks[indexPath.row] 48 | cell.configure(with: block) 49 | return cell 50 | } 51 | } 52 | 53 | extension TetrisRow: Collection { 54 | typealias Element = TetrisBlock 55 | typealias Index = Array.Index 56 | 57 | public var startIndex: Index { return blocks.startIndex } 58 | 59 | var endIndex: Index { return blocks.endIndex } 60 | 61 | subscript(position: Index) -> TetrisBlock { 62 | return blocks[position] 63 | } 64 | 65 | func index(after i: Index) -> Index { 66 | return blocks.index(after: i) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Adapters/DifferenceKit/DifferenceKitTetrisAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | import DifferenceKit 7 | 8 | class DifferenceKitTetrisAdapter: NSObject, TetrisAdapter, UICollectionViewDataSource { 9 | let name = "DifferenceKit" 10 | let comment = """ 11 | Works super-nice! 12 | """ 13 | 14 | func configure(collectionView: UICollectionView, cellProvider: @escaping (UICollectionView, IndexPath) -> UICollectionViewCell) { 15 | self.collectionView = collectionView 16 | collectionView.dataSource = self 17 | self.cellProvider = cellProvider 18 | } 19 | 20 | private var collectionView: UICollectionView! 21 | private var cellProvider: ((UICollectionView, IndexPath) -> UICollectionViewCell)! 22 | 23 | private var sections: [ArraySection] = [] 24 | 25 | func setBoard(_ tetrisBoard: TetrisBoard) { 26 | sections = tetrisBoard.sections 27 | } 28 | 29 | func animateBoard(to board: TetrisBoard, completion: ((Bool) -> Void)?) { 30 | let changes = StagedChangeset(source: sections, target: board.sections) 31 | collectionView.reload(using: changes, setData: { newSections in 32 | self.sections = newSections 33 | }) 34 | // Since DifferenceKit does not let us provide completion block 35 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { 36 | completion?(true) 37 | } 38 | } 39 | 40 | // MARK: UICollectionViewDataSource 41 | 42 | func numberOfSections(in collectionView: UICollectionView) -> Int { 43 | return sections.count 44 | } 45 | 46 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 47 | return sections[section].elements.count 48 | } 49 | 50 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 51 | let cell = cellProvider(collectionView, indexPath) 52 | let block = sections[indexPath.section].elements[indexPath.row] 53 | cell.configure(with: block) 54 | return cell 55 | } 56 | } 57 | 58 | private extension TetrisBoard { 59 | var sections: [ArraySection] { 60 | return rows.map { 61 | ArraySection(model: $0, elements: $0.blocks) 62 | } 63 | } 64 | } 65 | 66 | extension TetrisRow: Differentiable { 67 | typealias DifferenceIdentifier = Int 68 | 69 | var differenceIdentifier: DifferenceIdentifier { 70 | return identifier 71 | } 72 | 73 | func isContentEqual(to source: TetrisRow) -> Bool { 74 | return source.identifier == identifier 75 | } 76 | } 77 | 78 | extension TetrisBlock: Differentiable { 79 | typealias DifferenceIdentifier = Int 80 | 81 | var differenceIdentifier: DifferenceIdentifier { 82 | return identifier 83 | } 84 | 85 | func isContentEqual(to source: TetrisBlock) -> Bool { 86 | return source.identifier == identifier 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Adapters/Dwifft/DwifftTetrisAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | import Dwifft 7 | 8 | class DwifftTetrisAdapter: NSObject, TetrisAdapter, UICollectionViewDataSource { 9 | let name = "Dwifft" 10 | let comment = """ 11 | Works, but doesn't seem to be doing moves, so the block pieces jump between sections. 12 | """ 13 | 14 | func configure(collectionView: UICollectionView, cellProvider: @escaping (UICollectionView, IndexPath) -> UICollectionViewCell) { 15 | self.collectionView = collectionView 16 | collectionView.dataSource = self 17 | self.cellProvider = cellProvider 18 | diffCalculator = CollectionViewDiffCalculator(collectionView: collectionView) 19 | } 20 | 21 | private var collectionView: UICollectionView! 22 | private var cellProvider: ((UICollectionView, IndexPath) -> UICollectionViewCell)! 23 | private var diffCalculator: CollectionViewDiffCalculator! 24 | 25 | func setBoard(_ tetrisBoard: TetrisBoard) { 26 | // Setting diffCalculator?.sectionedValues = SectionedValues(tetrisBoard.dataSource) here crashes. 27 | } 28 | 29 | func animateBoard(to board: TetrisBoard, completion: ((Bool) -> Void)?) { 30 | diffCalculator.sectionedValues = SectionedValues(board.dataSource) 31 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { 32 | completion?(true) 33 | } 34 | } 35 | 36 | // MARK: UICollectionViewDataSource 37 | 38 | func numberOfSections(in collectionView: UICollectionView) -> Int { 39 | return diffCalculator.numberOfSections() 40 | } 41 | 42 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 43 | return diffCalculator.numberOfObjects(inSection: section) 44 | } 45 | 46 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 47 | let cell = cellProvider(collectionView, indexPath) 48 | let block = diffCalculator.value(atIndexPath: indexPath) 49 | cell.configure(with: block) 50 | return cell 51 | } 52 | } 53 | 54 | private extension TetrisBoard { 55 | var dataSource: [(TetrisRow, [TetrisBlock])] { 56 | return rows.map { ($0, $0.blocks) } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Adapters/SKRBatchUpdates/BatchChanges.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | struct BatchSectionMove { 8 | let source: Int 9 | let destination: Int 10 | } 11 | 12 | struct BatchItemMove { 13 | let source: IndexPath 14 | let destination: IndexPath 15 | } 16 | 17 | struct BatchSectionChanges { 18 | let sectionsToDelete: IndexSet 19 | let sectionsToInsert: IndexSet 20 | let sectionMoves: [BatchSectionMove] 21 | } 22 | 23 | struct BatchItemChanges { 24 | let itemsToDelete: [IndexPath] 25 | let itemsToInsert: [IndexPath] 26 | let itemMoves: [BatchItemMove] 27 | } 28 | 29 | // MARK: - Diffing for hashable items and sections 30 | 31 | extension BatchSectionChanges { 32 | init(from old: [SectionType], to new: [SectionType]) where SectionType: Hashable { 33 | let (deletes, inserts, moves) = diff(from: old.indexDictionary(), to: new.indexDictionary()) 34 | self.init(sectionsToDelete: IndexSet(deletes), 35 | sectionsToInsert: IndexSet(inserts), 36 | sectionMoves: moves.map { BatchSectionMove(source: $0, destination: $1) }) 37 | } 38 | } 39 | 40 | extension BatchItemChanges { 41 | init(from old: [[ItemType]], to new: [[ItemType]]) where ItemType: Hashable { 42 | let (deletes, inserts, moves) = diff(from: old.indexPathDictionary(), to: new.indexPathDictionary()) 43 | self.init(itemsToDelete: deletes, 44 | itemsToInsert: inserts, 45 | itemMoves: moves.map { BatchItemMove(source: $0, destination: $1) }) 46 | } 47 | } 48 | 49 | private func diff(from old: [ElementType: IndexType], to new: [ElementType: IndexType]) -> ([IndexType], [IndexType], [(IndexType, IndexType)]) where IndexType: Equatable { 50 | var deletes: [IndexType] = [] 51 | var inserts: [IndexType] = [] 52 | var moves: [(IndexType, IndexType)] = [] 53 | 54 | // Deletes and moves 55 | for (element, oldIndex) in old { 56 | if let newIndex = new[element] { 57 | if newIndex != oldIndex { 58 | moves.append((oldIndex, newIndex)) 59 | } 60 | } else { 61 | deletes.append(oldIndex) 62 | } 63 | } 64 | 65 | // Inserts 66 | for (element, newIndex) in new { 67 | if old[element] == nil { 68 | inserts.append(newIndex) 69 | } 70 | } 71 | 72 | return (deletes, inserts, moves) 73 | } 74 | 75 | private extension Sequence where Iterator.Element: Hashable { 76 | func indexDictionary() -> [Iterator.Element: Int] { 77 | var dictionary: [Iterator.Element: Int] = [:] 78 | for (index, element) in self.enumerated() { 79 | dictionary[element] = index 80 | } 81 | return dictionary 82 | } 83 | } 84 | 85 | private extension Sequence where Iterator.Element: Sequence, Iterator.Element.Iterator.Element: Hashable { 86 | typealias ItemElement = Iterator.Element.Iterator.Element 87 | 88 | func indexPathDictionary() -> [ItemElement: IndexPath] { 89 | var dictionary: [ItemElement: IndexPath] = [:] 90 | for (section, items) in self.enumerated() { 91 | for (item, element) in items.enumerated() { 92 | dictionary[element] = [section, item] 93 | } 94 | } 95 | return dictionary 96 | } 97 | } 98 | 99 | // MARK: - Applying sections changes to items 100 | 101 | extension BatchSectionChanges { 102 | func apply(to items: [[ItemType]]) -> [[ItemType]] { 103 | let newCount = items.count + sectionsToInsert.count - sectionsToDelete.count 104 | 105 | let movedIndices = sectionMoves.destinationToSourceDictionary() 106 | let deletedIndices = Set(sectionsToDelete + sectionMoves.map { $0.source }) 107 | var unchangedIndices = items.indices.lazy.filter({ !deletedIndices.contains($0) }).makeIterator() 108 | 109 | return (0.. [Int: Int] { 126 | var moves: [Int: Int] = [:] 127 | for move in self { 128 | moves[move.destination] = move.source 129 | } 130 | return moves 131 | } 132 | } 133 | 134 | // MARK: - Debugging 135 | 136 | //extension BatchChanges { 137 | // func debugPrint() { 138 | // print("Sections") 139 | // print(" - Deletes: \(Array(sectionsToDelete))") 140 | // print(" - Inserts: \(Array(sectionsToInsert))") 141 | // print(" - Moves: \(sectionMoves)") 142 | // print("Items") 143 | // print(" - Deletes: \(itemsToDelete)") 144 | // print(" - Inserts: \(itemsToInsert)") 145 | // print(" - Moves: \(itemMoves)") 146 | // } 147 | //} 148 | 149 | extension BatchItemMove: CustomStringConvertible { 150 | var description: String { 151 | return "\(source) → \(destination)" 152 | } 153 | } 154 | 155 | extension BatchSectionMove: CustomStringConvertible { 156 | var description: String { 157 | return "\(source) → \(destination)" 158 | } 159 | } 160 | 161 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Adapters/SKRBatchUpdates/DataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | public class DataSource { 8 | 9 | public init() { 10 | } 11 | 12 | public var sections: [(SectionType, [ItemType])] = [] 13 | 14 | var halfTimeItemCounts: [Int]? = nil 15 | 16 | public func numberOfSections() -> Int { 17 | if let rowCounts = halfTimeItemCounts { 18 | return rowCounts.count 19 | } else { 20 | return sections.count 21 | } 22 | } 23 | 24 | public func numberOfRows(in section: Int) -> Int { 25 | if let rowCounts = halfTimeItemCounts { 26 | return rowCounts[section] 27 | } else { 28 | let (_, items) = sections[section] 29 | return items.count 30 | } 31 | } 32 | 33 | public func section(at sectionIndex: Int) -> SectionType { 34 | let (section, _) = sections[sectionIndex] 35 | return section 36 | } 37 | 38 | public func itemForRow(at indexPath: IndexPath) -> ItemType { 39 | guard halfTimeItemCounts == nil else { 40 | fatalError("Cells are expected to not be accessed in this state") 41 | } 42 | let (_, items) = sections[indexPath.section] 43 | return items[indexPath.item] 44 | } 45 | 46 | public func animate(to sections: [(SectionType, [ItemType])], in tableView: UITableView, with animation: UITableView.RowAnimation) { 47 | let (oldSections, oldItems) = unzip(self.sections) 48 | let (newSections, newItems) = unzip(sections) 49 | 50 | self.sections = sections 51 | 52 | let sectionChanges = BatchSectionChanges(from: oldSections, to: newSections) 53 | let patchedItems = sectionChanges.apply(to: oldItems) 54 | let itemChanges = BatchItemChanges(from: patchedItems, to: newItems) 55 | 56 | halfTimeItemCounts = patchedItems.map { $0.count } 57 | tableView.beginUpdates() 58 | tableView.updateSections(for: sectionChanges, with: animation) 59 | tableView.endUpdates() 60 | 61 | halfTimeItemCounts = nil 62 | tableView.beginUpdates() 63 | tableView.updateRows(for: itemChanges, with: animation) 64 | tableView.endUpdates() 65 | } 66 | 67 | public func animate(to sections: [(SectionType, [ItemType])], in collectionView: UICollectionView, completion: ((Bool) -> Void)? = nil) { 68 | let (oldSections, oldItems) = unzip(self.sections) 69 | let (newSections, newItems) = unzip(sections) 70 | 71 | self.sections = sections 72 | 73 | let sectionChanges = BatchSectionChanges(from: oldSections, to: newSections) 74 | let patchedItems = sectionChanges.apply(to: oldItems) 75 | let itemChanges = BatchItemChanges(from: patchedItems, to: newItems) 76 | 77 | halfTimeItemCounts = patchedItems.map { $0.count } 78 | collectionView.performBatchUpdates({ 79 | collectionView.updateSections(for: sectionChanges) 80 | }, completion: nil) 81 | 82 | halfTimeItemCounts = nil 83 | collectionView.performBatchUpdates({ 84 | collectionView.updateItems(for: itemChanges) 85 | }, completion: completion) 86 | } 87 | } 88 | 89 | // MARK: - UIKit Extensions 90 | 91 | extension UITableView { 92 | func updateRows(for changes: BatchItemChanges, with animation: UITableView.RowAnimation) { 93 | deleteRows(at: changes.itemsToDelete, with: animation) 94 | insertRows(at: changes.itemsToInsert, with: animation) 95 | for move in changes.itemMoves { 96 | moveRow(at: move.source, to: move.destination) 97 | } 98 | } 99 | 100 | func updateSections(for changes: BatchSectionChanges, with animation: UITableView.RowAnimation) { 101 | deleteSections(changes.sectionsToDelete, with: animation) 102 | insertSections(changes.sectionsToInsert, with: animation) 103 | for move in changes.sectionMoves { 104 | moveSection(move.source, toSection: move.destination) 105 | } 106 | } 107 | } 108 | 109 | extension UICollectionView { 110 | func updateItems(for changes: BatchItemChanges) { 111 | deleteItems(at: changes.itemsToDelete) 112 | insertItems(at: changes.itemsToInsert) 113 | for move in changes.itemMoves { 114 | moveItem(at: move.source, to: move.destination) 115 | } 116 | } 117 | 118 | func updateSections(for changes: BatchSectionChanges) { 119 | deleteSections(changes.sectionsToDelete) 120 | insertSections(changes.sectionsToInsert) 121 | for move in changes.sectionMoves { 122 | moveSection(move.source, toSection: move.destination) 123 | } 124 | } 125 | } 126 | 127 | // MARK: - Helpers 128 | 129 | private func unzip(_ array: [(T, U)]) -> ([T], [U]) { 130 | var t: [T] = [], u: [U] = [] 131 | for (a, b) in array { 132 | t.append(a) 133 | u.append(b) 134 | } 135 | return (t, u) 136 | } 137 | 138 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Adapters/SKRBatchUpdates/SKRBatchUpdatesTetrisAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class SKRBatchUpdatesTetrisAdapter: NSObject, TetrisAdapter, UICollectionViewDataSource { 8 | let name = "SKRBatchUpdates" 9 | let comment = """ 10 | My diffing engine. The original Collection View Tetris diffing engine. 11 | """ 12 | 13 | func configure(collectionView: UICollectionView, cellProvider: @escaping (UICollectionView, IndexPath) -> UICollectionViewCell) { 14 | self.collectionView = collectionView 15 | collectionView.dataSource = self 16 | self.cellProvider = cellProvider 17 | } 18 | 19 | private var collectionView: UICollectionView! 20 | private var cellProvider: ((UICollectionView, IndexPath) -> UICollectionViewCell)! 21 | 22 | private let dataSource = DataSource() 23 | 24 | func setBoard(_ tetrisBoard: TetrisBoard) { 25 | dataSource.sections = tetrisBoard.dataSource 26 | } 27 | 28 | func animateBoard(to board: TetrisBoard, completion: ((Bool) -> Void)?) { 29 | dataSource.animate(to: board.dataSource, in: collectionView, completion: completion) 30 | } 31 | 32 | // MARK: UICollectionViewDataSource 33 | 34 | func numberOfSections(in collectionView: UICollectionView) -> Int { 35 | return dataSource.numberOfSections() 36 | } 37 | 38 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 39 | return dataSource.numberOfRows(in: section) 40 | } 41 | 42 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 43 | let cell = cellProvider(collectionView, indexPath) 44 | let block = dataSource.itemForRow(at: indexPath) 45 | cell.configure(with: block) 46 | return cell 47 | } 48 | } 49 | 50 | private extension TetrisBoard { 51 | var dataSource: [(TetrisRow, [TetrisBlock])] { 52 | return rows.map { ($0, $0.blocks) } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Adapters/TetrisAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | protocol TetrisAdapter { 8 | var name: String { get } 9 | var comment: String { get } 10 | 11 | func configure(collectionView: UICollectionView, cellProvider: @escaping (UICollectionView, IndexPath) -> UICollectionViewCell) 12 | 13 | func setBoard(_ tetrisBoard: TetrisBoard) 14 | func animateBoard(to board: TetrisBoard, completion: ((Bool) -> Void)?) 15 | } 16 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Adapters/UICollectionViewDiffableDataSource/UICollectionViewDiffableDataSourceTetrisAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | @available(iOS 13.0, *) 8 | class UICollectionViewDiffableDataSourceTetrisAdapter: TetrisAdapter { 9 | let name = "DiffableDataSource" 10 | let comment = """ 11 | Added in iOS 13, this is the one we're all soon gonna use. And it works great. 12 | """ 13 | 14 | private var dataSource: UICollectionViewDiffableDataSource! 15 | 16 | func configure(collectionView: UICollectionView, cellProvider: @escaping (UICollectionView, IndexPath) -> UICollectionViewCell) { 17 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, block in 18 | let cell = cellProvider(collectionView, indexPath) 19 | cell.configure(with: block) 20 | return cell 21 | } 22 | } 23 | 24 | func setBoard(_ board: TetrisBoard) { 25 | dataSource.apply(board.snapshot) 26 | } 27 | 28 | func animateBoard(to board: TetrisBoard, completion: ((Bool) -> Void)?) { 29 | dataSource.apply(board.snapshot, animatingDifferences: true) 30 | // Since we don't get a completion block 31 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { 32 | completion?(true) 33 | } 34 | } 35 | } 36 | 37 | @available(iOS 13.0, *) 38 | private extension TetrisBoard { 39 | var snapshot: NSDiffableDataSourceSnapshot { 40 | let snapshot = NSDiffableDataSourceSnapshot() 41 | for row in rows { 42 | snapshot.appendSections([row]) 43 | snapshot.appendItems(row.blocks, toSection: row) 44 | } 45 | return snapshot 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Common/Common.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | /// For use in required `init(coder:)` methods where this is not intended usage. 8 | func unsupportedInitializer() -> Never { 9 | fatalError("This view cannot be initialized from a storyboard or nib.") 10 | } 11 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Common/UIViewController+common.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | extension UIViewController { 8 | func embed(_ childViewController: UIViewController) { 9 | embed(childViewController, in: self.view) 10 | } 11 | 12 | func embed(_ childViewController: UIViewController, in embeddingView: UIView) { 13 | addChild(childViewController) 14 | 15 | let subview = childViewController.view! 16 | embeddingView.addSubview(subview) 17 | subview.translatesAutoresizingMaskIntoConstraints = false 18 | NSLayoutConstraint.activate([ 19 | subview.leadingAnchor.constraint(equalTo: embeddingView.leadingAnchor), 20 | subview.trailingAnchor.constraint(equalTo: embeddingView.trailingAnchor), 21 | subview.topAnchor.constraint(equalTo: embeddingView.topAnchor), 22 | subview.bottomAnchor.constraint(equalTo: embeddingView.bottomAnchor) 23 | ]) 24 | 25 | childViewController.didMove(toParent: self) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | @UIApplicationMain 8 | class AppDelegate: UIResponder, UIApplicationDelegate { 9 | var window: UIWindow? 10 | 11 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 12 | 13 | let window = UIWindow(frame: UIScreen.main.bounds) 14 | window.rootViewController = RootViewController() 15 | window.makeKeyAndVisible() 16 | self.window = window 17 | 18 | return true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "icon-40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "icon-60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon-58.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "icon-87.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "icon-80.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "icon-120.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon-120.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon-180.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "icon-20.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "icon-40.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "icon-29.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "icon-58.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "icon-40.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "icon-80.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "icon-76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "icon-152.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "icon-167.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "icon-1024.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "appicon-generator" 115 | } 116 | } -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-120.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-152.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-167.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-180.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-20.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-58.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-60.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-80.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skagedal/TetrisDiffingCompetition/304ef3bbb3feb6c4d4a281eda2fedd06dd105391/TetrisDiffingCompetition/Main/Assets.xcassets/AppIcon.appiconset/icon-87.png -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/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 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/MenuViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | private let reuseIdentifier = "cell" 8 | 9 | class MenuViewController: UITableViewController { 10 | weak var delegate: MenuViewControllerDelegate? 11 | 12 | private let adapters: [TetrisAdapter] 13 | 14 | init(adapters: [TetrisAdapter]) { 15 | self.adapters = adapters 16 | 17 | super.init(style: .grouped) 18 | 19 | navigationItem.title = "The Tetris Diffing Competition" 20 | navigationItem.backBarButtonItem = UIBarButtonItem(title: "Menu", style: .plain, target: nil, action: nil) 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { unsupportedInitializer() } 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | tableView.register(SubtitleCell.self, forCellReuseIdentifier: reuseIdentifier) 29 | } 30 | 31 | // MARK: - Table view data source 32 | 33 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 34 | return adapters.count 35 | } 36 | 37 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 38 | let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) 39 | let a = adapter(for: indexPath) 40 | cell.textLabel?.text = a.name 41 | cell.accessoryType = .disclosureIndicator 42 | cell.detailTextLabel?.numberOfLines = 0 43 | cell.detailTextLabel?.textColor = UIColor(white: 0, alpha: 0.8) 44 | cell.detailTextLabel?.text = a.comment 45 | return cell 46 | } 47 | 48 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 49 | delegate?.menuViewController(self, didSelect: adapter(for: indexPath)) 50 | } 51 | 52 | private func adapter(for indexPath: IndexPath) -> TetrisAdapter { 53 | return adapters[indexPath.row] 54 | } 55 | } 56 | 57 | protocol MenuViewControllerDelegate: AnyObject { 58 | func menuViewController(_ menuViewController: MenuViewController, didSelect adapter: TetrisAdapter) 59 | } 60 | 61 | final class SubtitleCell: UITableViewCell { 62 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 63 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) 64 | } 65 | 66 | required init?(coder aDecoder: NSCoder) { unsupportedInitializer() } 67 | } 68 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Main/RootViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | private func adapters() -> [TetrisAdapter] { 8 | if #available(iOS 13, *) { 9 | return [ 10 | SKRBatchUpdatesTetrisAdapter(), 11 | UICollectionViewDiffableDataSourceTetrisAdapter(), 12 | DifferenceKitTetrisAdapter(), 13 | DwifftTetrisAdapter(), 14 | DifferTetrisAdapter() 15 | ] 16 | } else { 17 | return [ 18 | SKRBatchUpdatesTetrisAdapter(), 19 | DifferenceKitTetrisAdapter(), 20 | DwifftTetrisAdapter(), 21 | DifferTetrisAdapter() 22 | ] 23 | } 24 | } 25 | 26 | class RootViewController: UIViewController { 27 | private lazy var menu = MenuViewController(adapters: adapters()) 28 | 29 | private lazy var navigation: UINavigationController = { 30 | let navigation = UINavigationController(rootViewController: menu) 31 | navigation.isToolbarHidden = false 32 | return navigation 33 | }() 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | 38 | menu.delegate = self 39 | embed(navigation) 40 | } 41 | 42 | private func makeGameViewController(with adapter: TetrisAdapter) -> TetrisViewController { 43 | return TetrisViewController.instantiate(with: adapter) 44 | } 45 | } 46 | 47 | extension RootViewController: MenuViewControllerDelegate { 48 | func menuViewController(_ menuViewController: MenuViewController, didSelect adapter: TetrisAdapter) { 49 | navigation.pushViewController(makeGameViewController(with: adapter), animated: true) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Tetris/Tetris.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 | 29 | 30 | 31 | 32 | 33 | 34 | 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 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /TetrisDiffingCompetition/Tetris/TetrisViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Simon Kågedal Reimer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class TetrisViewController: UIViewController { 8 | @IBOutlet private var collectionView: UICollectionView! 9 | 10 | private var adapter: TetrisAdapter! 11 | private var game = TetrisGame() 12 | private var timer: Timer? 13 | 14 | // MARK: Life cycle 15 | 16 | static func instantiate(with adapter: TetrisAdapter) -> TetrisViewController { 17 | let viewController = UIStoryboard(name: "Tetris", bundle: nil).instantiateInitialViewController() as! TetrisViewController 18 | viewController.adapter = adapter 19 | viewController.navigationItem.title = adapter.name 20 | return viewController 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | adapter.configure(collectionView: collectionView) { collectionView, indexPath in 27 | collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) 28 | } 29 | adapter.setBoard(game.currentBoard) 30 | } 31 | 32 | // MARK: Actions 33 | 34 | @IBAction private func play(_ sender: Any) { 35 | startGame() 36 | } 37 | 38 | @IBAction private func moveLeft(_ sender: UIBarButtonItem) { 39 | if game.left() { 40 | update() 41 | } 42 | } 43 | 44 | @IBAction private func moveRight(_ sender: UIBarButtonItem) { 45 | if game.right() { 46 | update() 47 | } 48 | } 49 | 50 | @IBAction private func rotateLeft(_ sender: UIBarButtonItem) { 51 | if game.rotateLeft() { 52 | update() 53 | } 54 | } 55 | 56 | @IBAction private func rotateRight(_ sender: UIBarButtonItem) { 57 | if game.rotateRight() { 58 | update() 59 | } 60 | } 61 | 62 | @IBAction private func drop(_ sender: UIBarButtonItem) { 63 | if game.drop() { 64 | stopTimer() 65 | update(completion: { _ in 66 | self.startTimer() 67 | }) 68 | } 69 | } 70 | 71 | // Game logic 72 | 73 | private func startGame() { 74 | game = TetrisGame() 75 | spawnOrGameOver() 76 | startTimer() 77 | } 78 | 79 | private func tick() { 80 | if game.down() { 81 | update() 82 | } else { 83 | game.fixBlock() 84 | if game.removeLines() { 85 | update() 86 | } 87 | spawnOrGameOver() 88 | } 89 | } 90 | 91 | private func spawnOrGameOver() { 92 | if game.spawn() { 93 | update() 94 | } else { 95 | gameOver() 96 | } 97 | } 98 | 99 | private func gameOver() { 100 | stopTimer() 101 | } 102 | 103 | private func update(completion: ((Bool) -> Swift.Void)? = nil) { 104 | adapter.animateBoard(to: game.currentBoard, completion: completion) 105 | } 106 | 107 | private func startTimer() { 108 | if let timer = self.timer { 109 | timer.invalidate() 110 | } 111 | let timer = Timer(fire: Date(), interval: 0.5, repeats: true) {_ in 112 | self.tick() 113 | } 114 | RunLoop.current.add(timer, forMode: .default) 115 | self.timer = timer 116 | } 117 | 118 | private func stopTimer() { 119 | timer?.invalidate() 120 | timer = nil 121 | } 122 | } 123 | 124 | extension UICollectionViewCell { 125 | func configure(with block: TetrisBlock) { 126 | switch block.type { 127 | case .empty: 128 | backgroundColor = .clear 129 | 130 | case .occupied(let color): 131 | backgroundColor = color 132 | } 133 | } 134 | } 135 | --------------------------------------------------------------------------------