Skip to content

Commit af86903

Browse files
authored
Merge pull request #18 from ra1028/v0.5.3
v0.5.3
2 parents 71acd7a + 5af3ce6 commit af86903

23 files changed

+359
-100
lines changed

DifferenceKit.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |spec|
22
spec.name = 'DifferenceKit'
3-
spec.version = '0.5.2'
3+
spec.version = '0.5.3'
44
spec.author = { 'ra1028' => 'r.fe51028.r@gmail.com' }
55
spec.homepage = 'https://github.com/ra1028/DifferenceKit'
66
spec.documentation_url = 'https://ra1028.github.io/DifferenceKit'

Examples/Example-iOS/Example-iOS.xcodeproj/project.pbxproj

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
6B956B832110B38700DE3D29 /* HeaderFooterSectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B956B7A2110B38700DE3D29 /* HeaderFooterSectionViewController.swift */; };
1313
6B956B842110B38700DE3D29 /* ShuffleEmoticonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B956B7B2110B38700DE3D29 /* ShuffleEmoticonViewController.swift */; };
1414
6B956B852110B38700DE3D29 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6B956B7C2110B38700DE3D29 /* Assets.xcassets */; };
15-
6B956B862110B38700DE3D29 /* String+Differentiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B956B7D2110B38700DE3D29 /* String+Differentiable.swift */; };
15+
6B956B862110B38700DE3D29 /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B956B7D2110B38700DE3D29 /* Tools.swift */; };
1616
6B956B872110B38700DE3D29 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B956B7E2110B38700DE3D29 /* HomeViewController.swift */; };
1717
6B956B882110B38700DE3D29 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6B956B7F2110B38700DE3D29 /* LaunchScreen.storyboard */; };
1818
6B956B892110B38700DE3D29 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B956B812110B38700DE3D29 /* AppDelegate.swift */; };
19+
6BD9445D2136EDC100093E7A /* RandomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD9445C2136EDC100093E7A /* RandomViewController.swift */; };
20+
6BD944602137062000093E7A /* TextCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD9445F2137062000093E7A /* TextCollectionReusableView.swift */; };
1921
/* End PBXBuildFile section */
2022

2123
/* Begin PBXContainerItemProxy section */
@@ -62,11 +64,13 @@
6264
6B956B7A2110B38700DE3D29 /* HeaderFooterSectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderFooterSectionViewController.swift; sourceTree = "<group>"; };
6365
6B956B7B2110B38700DE3D29 /* ShuffleEmoticonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShuffleEmoticonViewController.swift; sourceTree = "<group>"; };
6466
6B956B7C2110B38700DE3D29 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
65-
6B956B7D2110B38700DE3D29 /* String+Differentiable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Differentiable.swift"; sourceTree = "<group>"; };
67+
6B956B7D2110B38700DE3D29 /* Tools.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = "<group>"; };
6668
6B956B7E2110B38700DE3D29 /* HomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
6769
6B956B802110B38700DE3D29 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
6870
6B956B812110B38700DE3D29 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
6971
6B956B822110B38700DE3D29 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
72+
6BD9445C2136EDC100093E7A /* RandomViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomViewController.swift; sourceTree = "<group>"; };
73+
6BD9445F2137062000093E7A /* TextCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCollectionReusableView.swift; sourceTree = "<group>"; };
7074
/* End PBXFileReference section */
7175

7276
/* Begin PBXFrameworksBuildPhase section */
@@ -121,7 +125,9 @@
121125
6B956B7E2110B38700DE3D29 /* HomeViewController.swift */,
122126
6B956B7B2110B38700DE3D29 /* ShuffleEmoticonViewController.swift */,
123127
6B956B7A2110B38700DE3D29 /* HeaderFooterSectionViewController.swift */,
124-
6B956B7D2110B38700DE3D29 /* String+Differentiable.swift */,
128+
6BD9445C2136EDC100093E7A /* RandomViewController.swift */,
129+
6BD9445F2137062000093E7A /* TextCollectionReusableView.swift */,
130+
6B956B7D2110B38700DE3D29 /* Tools.swift */,
125131
6B956B812110B38700DE3D29 /* AppDelegate.swift */,
126132
6B956B7C2110B38700DE3D29 /* Assets.xcassets */,
127133
6B956B7F2110B38700DE3D29 /* LaunchScreen.storyboard */,
@@ -226,9 +232,11 @@
226232
buildActionMask = 2147483647;
227233
files = (
228234
6B956B842110B38700DE3D29 /* ShuffleEmoticonViewController.swift in Sources */,
229-
6B956B862110B38700DE3D29 /* String+Differentiable.swift in Sources */,
235+
6B956B862110B38700DE3D29 /* Tools.swift in Sources */,
230236
6B956B832110B38700DE3D29 /* HeaderFooterSectionViewController.swift in Sources */,
237+
6BD9445D2136EDC100093E7A /* RandomViewController.swift in Sources */,
231238
6B956B892110B38700DE3D29 /* AppDelegate.swift in Sources */,
239+
6BD944602137062000093E7A /* TextCollectionReusableView.swift in Sources */,
232240
6B956B872110B38700DE3D29 /* HomeViewController.swift in Sources */,
233241
);
234242
runOnlyForDeploymentPostprocessing = 0;

Examples/Example-iOS/Sources/HomeViewController.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ final class HomeViewController: UITableViewController {
55

66
init() {
77
let tableComponents: [(title: String, subtitle: String, initViewController: () -> UIViewController)] = [
8-
(title: "Shuffle Emoticons", subtitle: "Diff of shuffled emoticons in UICollectionView", initViewController: ShuffleEmoticonViewController.init),
9-
(title: "Header Footer Section", subtitle: "Diff including section header/footer in UITableView", initViewController: HeaderFooterSectionViewController.init)
8+
(title: "Shuffle Emoticons", subtitle: "Shuffle sectioned emoticons in UICollectionView", initViewController: ShuffleEmoticonViewController.init),
9+
(title: "Header Footer Section", subtitle: "Update header/footer by reload section in UITableView", initViewController: HeaderFooterSectionViewController.init),
10+
(title: "Random", subtitle: "Random diff in UICollectionView", initViewController: RandomViewController.init)
1011
]
1112

1213
data = tableComponents.map { component in
@@ -21,6 +22,7 @@ final class HomeViewController: UITableViewController {
2122
title = "Home"
2223
view.backgroundColor = .white
2324
tableView.tableFooterView = UIView()
25+
tableView.reloadData()
2426
}
2527

2628
@available(*, unavailable)
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import UIKit
2+
import DifferenceKit
3+
4+
private struct RandomModel: Differentiable {
5+
var id: UUID
6+
var isUpdated: Bool
7+
8+
var differenceIdentifier: UUID {
9+
return id
10+
}
11+
12+
init(_ id: UUID = UUID(), _ isUpdated: Bool = false) {
13+
self.id = id
14+
self.isUpdated = isUpdated
15+
}
16+
17+
func isContentEqual(to source: RandomModel) -> Bool {
18+
return isUpdated == source.isUpdated
19+
}
20+
}
21+
22+
private typealias RandomSection = ArraySection<RandomModel, RandomModel>
23+
24+
final class RandomViewController: UIViewController {
25+
private let collectionView: UICollectionView
26+
private var data = [RandomSection]()
27+
28+
private var dataInput: [RandomSection] {
29+
get { return data }
30+
set {
31+
let changeset = StagedChangeset(source: data, target: newValue)
32+
collectionView.reload(using: changeset) { data in
33+
self.data = data
34+
}
35+
}
36+
}
37+
38+
@objc private func refresh() {
39+
let defaultSourceSectionCount = 20
40+
let defaultSourceElementCount = 20
41+
42+
func randomSection() -> ArraySection<RandomModel, RandomModel> {
43+
let elements = (0..<defaultSourceElementCount).map { _ in RandomModel() }
44+
return ArraySection(model: RandomModel(), elements: elements)
45+
}
46+
47+
guard !data.isEmpty else {
48+
dataInput = (0..<defaultSourceSectionCount).map { _ in randomSection() }
49+
return
50+
}
51+
52+
let source = data
53+
var target = source
54+
55+
let sourceSectionCount = source.count
56+
let deleteSectionCount = Int.random(in: 0..<sourceSectionCount / 4)
57+
let deletedSourceSectionCount = sourceSectionCount - deleteSectionCount
58+
let updateSectionCount = Int.random(in: 0..<deletedSourceSectionCount / 4)
59+
let moveSectionCount = Int.random(in: 0..<deletedSourceSectionCount / 4)
60+
let minInsertCount = defaultSourceSectionCount > sourceSectionCount ? deleteSectionCount : 0
61+
let insertSectionCount = Int.random(in: minInsertCount..<sourceSectionCount / 4)
62+
let targetSectionCount = deletedSourceSectionCount + insertSectionCount
63+
64+
let deleteSectionIndices = (0..<deleteSectionCount).map { i in Int.random(in: 0..<sourceSectionCount - i) }
65+
let updateSectionIndices = (0..<updateSectionCount).map { _ in Int.random(in: 0..<deletedSourceSectionCount) }
66+
let moveSectionIndexPairs = (0..<moveSectionCount).map { _ in (source: Int.random(in: 0..<deletedSourceSectionCount), target: Int.random(in: 0..<deletedSourceSectionCount)) }
67+
let insertSectionIndices = (0..<insertSectionCount).map { i in Int.random(in: 0..<deletedSourceSectionCount + i) }
68+
69+
for index in deleteSectionIndices {
70+
target.remove(at: index)
71+
}
72+
73+
for index in target.indices {
74+
let sourceCount = target[index].elements.count
75+
let deleteCount = Int.random(in: 0..<sourceCount / 4)
76+
let deletedSourceCount = sourceCount - deleteCount
77+
let updateCount = Int.random(in: 0..<deletedSourceCount / 4)
78+
let moveCount = Int.random(in: 0..<deletedSourceCount / 4)
79+
let insertCount = Int.random(in: 0..<sourceCount / 4)
80+
81+
let deleteIndices = (0..<deleteCount).map { i in Int.random(in: 0..<sourceCount - i) }
82+
let updateIndices = (0..<updateCount).map { _ in Int.random(in: 0..<deletedSourceCount) }
83+
let moveIndexPairs = (0..<moveCount).map { _ in (source: Int.random(in: 0..<deletedSourceCount), target: Int.random(in: 0..<deletedSourceCount)) }
84+
let insertIndices = (0..<insertCount).map { i in Int.random(in: 0..<deletedSourceCount + i) }
85+
86+
for elementIndex in deleteIndices {
87+
target[index].elements.remove(at: elementIndex)
88+
}
89+
90+
for elementIndex in updateIndices {
91+
target[index].elements[elementIndex].isUpdated.toggle()
92+
}
93+
94+
for pair in moveIndexPairs {
95+
target[index].elements.swapAt(pair.source, pair.target)
96+
}
97+
98+
for elementIndex in insertIndices {
99+
target[index].elements.insert(RandomModel(), at: elementIndex)
100+
}
101+
}
102+
103+
for index in updateSectionIndices {
104+
target[index].model.isUpdated.toggle()
105+
}
106+
107+
for pair in moveSectionIndexPairs {
108+
target.swapAt(pair.source, pair.target)
109+
}
110+
111+
for index in insertSectionIndices {
112+
target.insert(randomSection(), at: index)
113+
}
114+
115+
let elementMoveAcrossSectionCount = Int.random(in: 0..<targetSectionCount * 2)
116+
for _ in (0..<elementMoveAcrossSectionCount) {
117+
func randomIndexPath() -> IndexPath {
118+
let sectionIndex = Int.random(in: 0..<targetSectionCount)
119+
let elementIndex = Int.random(in: 0..<target[sectionIndex].elements.count)
120+
return IndexPath(item: elementIndex, section: sectionIndex)
121+
}
122+
let sourceIndexPath = randomIndexPath()
123+
let targetIndexPath = randomIndexPath()
124+
target[sourceIndexPath.section].elements[sourceIndexPath.item] = target[targetIndexPath.section].elements[targetIndexPath.item]
125+
target[targetIndexPath.section].elements[targetIndexPath.item] = target[sourceIndexPath.section].elements[sourceIndexPath.item]
126+
}
127+
128+
dataInput = target
129+
}
130+
131+
override func viewWillAppear(_ animated: Bool) {
132+
super.viewWillAppear(animated)
133+
refresh()
134+
}
135+
136+
init() {
137+
let flowLayout = UICollectionViewFlowLayout()
138+
flowLayout.sectionInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
139+
flowLayout.headerReferenceSize = CGSize(width: UIScreen.main.bounds.size.width, height: 30)
140+
collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
141+
142+
super.init(nibName: nil, bundle: nil)
143+
144+
title = "Random"
145+
view.backgroundColor = .white
146+
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refresh))
147+
148+
collectionView.backgroundColor = .clear
149+
collectionView.dataSource = self
150+
collectionView.delegate = self
151+
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: UICollectionViewCell.reuseIdentifier)
152+
collectionView.register(TextCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: TextCollectionReusableView.reuseIdentifier)
153+
collectionView.translatesAutoresizingMaskIntoConstraints = false
154+
view.addSubview(collectionView)
155+
156+
let constraints = [
157+
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
158+
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
159+
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
160+
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
161+
]
162+
163+
NSLayoutConstraint.activate(constraints)
164+
}
165+
166+
@available(*, unavailable)
167+
required init?(coder aDecoder: NSCoder) {
168+
fatalError("init(coder:) has not been implemented")
169+
}
170+
}
171+
172+
extension RandomViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
173+
func numberOfSections(in collectionView: UICollectionView) -> Int {
174+
return data.count
175+
}
176+
177+
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
178+
return data[section].elements.count
179+
}
180+
181+
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
182+
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: UICollectionViewCell.reuseIdentifier, for: indexPath)
183+
let model = data[indexPath.section].elements[indexPath.item]
184+
cell.contentView.backgroundColor = model.isUpdated ? .yellow : .red
185+
return cell
186+
}
187+
188+
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
189+
guard kind == UICollectionElementKindSectionHeader else {
190+
return UICollectionReusableView()
191+
}
192+
193+
let model = data[indexPath.section].model
194+
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TextCollectionReusableView.reuseIdentifier, for: indexPath) as! TextCollectionReusableView
195+
view.text = "Section \(model.id)" + (model.isUpdated ? "+" : "")
196+
return view
197+
}
198+
}
199+
200+
private extension UICollectionViewCell {
201+
static let reuseIdentifier = String(describing: self)
202+
}

Examples/Example-iOS/Sources/ShuffleEmoticonViewController.swift

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -195,53 +195,3 @@ private final class EmoticonCollectionViewCell: UICollectionViewCell {
195195
fatalError("init(coder:) has not been implemented")
196196
}
197197
}
198-
199-
private final class TextCollectionReusableView: UICollectionReusableView {
200-
static let reuseIdentifier = String(describing: self)
201-
202-
var text: String? {
203-
get { return label.text }
204-
set { label.text = newValue }
205-
}
206-
207-
private let label = UILabel()
208-
209-
override init(frame: CGRect) {
210-
super.init(frame: frame)
211-
backgroundColor = UIColor.black.withAlphaComponent(0.05)
212-
addSubview(label)
213-
label.translatesAutoresizingMaskIntoConstraints = false
214-
215-
let constraints = [
216-
label.topAnchor.constraint(equalTo: topAnchor),
217-
label.bottomAnchor.constraint(equalTo: bottomAnchor),
218-
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
219-
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 16)
220-
]
221-
NSLayoutConstraint.activate(constraints)
222-
}
223-
224-
@available(*, unavailable)
225-
required init?(coder aDecoder: NSCoder) {
226-
fatalError("init(coder:) has not been implemented")
227-
}
228-
}
229-
230-
// FIXME: This extension is not required after Swift 4.2
231-
// https://github.com/apple/swift/blob/master/stdlib/public/core/CollectionAlgorithms.swift
232-
private extension MutableCollection {
233-
mutating func shuffle() {
234-
let count = self.count
235-
guard count > 1 else { return }
236-
237-
var amount = count
238-
var currentIndex = startIndex
239-
240-
while amount > 1 {
241-
let random = Int(arc4random_uniform(UInt32(amount)))
242-
amount -= 1
243-
swapAt(currentIndex, index(currentIndex, offsetBy: numericCast(random)))
244-
formIndex(after: &currentIndex)
245-
}
246-
}
247-
}

Examples/Example-iOS/Sources/String+Differentiable.swift

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import UIKit
2+
3+
final class TextCollectionReusableView: UICollectionReusableView {
4+
static let reuseIdentifier = String(describing: self)
5+
6+
var text: String? {
7+
get { return label.text }
8+
set { label.text = newValue }
9+
}
10+
11+
private let label = UILabel()
12+
13+
override init(frame: CGRect) {
14+
super.init(frame: frame)
15+
backgroundColor = UIColor.black.withAlphaComponent(0.05)
16+
addSubview(label)
17+
label.translatesAutoresizingMaskIntoConstraints = false
18+
19+
let constraints = [
20+
label.topAnchor.constraint(equalTo: topAnchor),
21+
label.bottomAnchor.constraint(equalTo: bottomAnchor),
22+
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
23+
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16)
24+
]
25+
NSLayoutConstraint.activate(constraints)
26+
}
27+
28+
@available(*, unavailable)
29+
required init?(coder aDecoder: NSCoder) {
30+
fatalError("init(coder:) has not been implemented")
31+
}
32+
}

0 commit comments

Comments
 (0)