osteslag / changeset Goto Github PK
View Code? Open in Web Editor NEWMinimal edits from one collection to another
License: MIT License
Minimal edits from one collection to another
License: MIT License
Xcode has been warning me that IndexDistance has been deprecated, the reason being that IndexDistance is now always defined as an Int.
.../Pods/Changeset/Sources/Changeset.swift:13:79: 'IndexDistance' is deprecated: all index distances are now of type Int
.../Pods/Changeset/Sources/Changeset.Edit.swift:16:31: 'IndexDistance' is deprecated: all index distances are now of type Int
With the release of Xcode 10 I was wondering about a Swift 4.2 compatible release.
This is a great lib and I was exited to implement it but I'm kind of lost in regards of using this with my actual models, normally you will not have just a String to be presented 🤐 .. Any chance for a sample about how to use it with custom models?
Version 1 of Changeset
was implemented from my recollection of how UITableView
interprets indices when batch inserting and deleting rows and sections in a beginUpdates
/endUpdates
block. I believed everything was expressed in source indices, before any changes; both insertions and deletions. It turns out, however, only deletions are. Insertions are deferred, thus insertion indices should be relative to any changes caused by deletions.
This is what Apple’s Table View Programming Guide for iOS says on the subject:
[
UITableView
] defers any insertions of rows or sections until after it has handled the deletions of rows or sections. The table view behaves the same way with reloading methods called inside an update block—the reload takes place with respect to the indexes of rows and sections before the animation block is executed. This behavior happens regardless of the ordering of the insertion, deletion, and reloading method calls.Deletion and reloading operations within an animation block specify which rows and sections in the original table should be removed or reloaded; insertions specify which rows and sections should be added to the resulting table. The index paths used to identify sections and rows follow this model. Inserting or removing an item in a mutable array, on the other hand, may affect the array index used for the successive insertion or removal operation; for example, if you insert an item at a certain index, the indexes of all subsequent items in the array are incremented.
TODO
Changeset.editDistance
Perhaps as a separate target if Changeset is to be kept small?
I can't figure out how to make the compiler happy when trying to express something like this:
public protocol ChangesetApplying: class {
associatedtype T
func apply(changeset: Changeset<T>)
}
The above will complain that Type 'Self.T' does not conform to protocol 'Collection'.
Changing it to associatedtype T: Collection
yields a different error: Type 'Self.T.Iterator.Element' does not conform to protocol 'Equatable'.
Changing it to associatedtype T: Collection where T.Iterator.Element: Equatable, T.IndexDistance == Int
yields the error 'where' clause cannot be attached to an associated type declaration, which appears to be the subject of SE-0142.
Any ideas?
just my 2 cents:
the code is very hard readable with all the one/two letter variables
it would be nice to have the variable names more verbose
I have a couple of examples of edit steps that are incorrect.
'64927513' -> '917546832':
delete 4 at index 1
replace with 1 at index 1
replace with 4 at index 4
move 6 from index 0 to 5
insert 8 at index 6
insert 2 at index 8
UICollectionView error: attempt to delete and reload the same index path (path = 0 - 1)
'8C9A2574361B' -> '897A34B215C6':
replace with 3 at index 4
delete 5 at index 5
move 7 from index 6 to 2
replace with B at index 6
replace with 2 at index 7
replace with 5 at index 9
move C from index 1 to 10
insert 6 at index 11
UICollectionView error: attempt to perform a delete and a move from the same index path (path = 0 - 6)
Xcode had two warnings when opening this project;
Hi! I'm curious why the table view extension translates Move
edits into insertion/deletion instead of using moveRowAtIndexPath(toIndexPath:)
? Some subtlety I'm missing?
Hey, I just read the readme. I like this a lot. Do you think this could be applied to trees as well? Imagine I have a tree of lightweight structs. Can I compare two trees and find the minimum changes required going from one to the other?
Are there any plans for supporting Carthage? I don't really want to add Pods to my project, and the Swift Package Manager option is currently failing with:
~ swift package generate-xcodeproj
Fetching https://github.com/osteslag/Changeset.git
Completed resolution in 4.39s
Cloning https://github.com/osteslag/Changeset.git
Resolving https://github.com/osteslag/Changeset.git at 3.1.1
warning: PackageDescription API v3 is deprecated and will be removed in the future; used by package(s): Changeset
'Changeset' /Users/pcferreira/Projects/OnboardingReactiveSwift/.build/checkouts/Changeset.git--4643115965670364357: error: package has unsupported layout; found loose source files: /Users/pcferreira/Projects/OnboardingReactiveSwift/.build/checkouts/Changeset.git--4643115965670364357/Tests/ChangesetTests.swift
'OnboardingReactiveSwift' /Users/pcferreira/Projects/OnboardingReactiveSwift: error: product dependency 'Changeset' not found
I have some thoughts and suggestions, but I'm not sure about if we need them resolved. Why I want to raise the issue is because maybe someone out there might be interested. They are mostly concerning substitution/reload and how UIKit and its documentation seems to treat it inconsistently.
My translation from changeset's edit operations to UITableView's row operations is as follow:
Edit | Row |
---|---|
Insert | Insert |
Delete | Delete |
Move | Move |
Substitution | Reload |
When executing row operations between beginUpdates
and endUpdates
, like delete, reload index is expected to be the ones prior to any updates. As stated in its API documentation:
The indexes that UITableView passes to the method are specified in the state of the table view prior to any updates.
Changeset provides the index after update instead. Making it unavailable to be mixed with other operations within beginUpdates
and endUpdates
.
As of 1.0.x, in order to use it with UITableView's row operation, I have to do something like this.
var deletedIndexPaths = [NSIndexPath]()
var insertedIndexPaths = [NSIndexPath]()
var movedIndexPaths = [(from: NSIndexPath, to: NSIndexPath)]()
var reloadIndexPaths = [NSIndexPath]()
for edit in changeset.edits {
switch edit.operation {
case .Insertion:
let insertedIndexPath = NSIndexPath(forRow: edit.destination, inSection: 0)
insertedIndexPaths.append(insertedIndexPath)
case .Deletion:
let deletedIndexPath = NSIndexPath(forRow: edit.destination, inSection: 0)
deletedIndexPaths.append(deletedIndexPath)
case .Substitution:
let reloadIndexPath = NSIndexPath(forRow: edit.destination, inSection: 0)
reloadIndexPaths.append(reloadIndexPath)
case .Move(origin: let origin):
let fromIndexPath = NSIndexPath(forRow: origin, inSection: 0)
let toIndexPath = NSIndexPath(forRow: edit.destination, inSection: 0)
movedIndexPaths.append((from: fromIndexPath, to: toIndexPath))
}
}
self.tableView.beginUpdates()
if deletedIndexPaths.count > 0 {
self.tableView.deleteRowsAtIndexPaths(deletedIndexPaths, withRowAnimation: .Automatic)
}
if insertedIndexPaths.count > 0 {
self.tableView.insertRowsAtIndexPaths(insertedIndexPaths, withRowAnimation: .Automatic)
}
for (from, to) in movedIndexPaths {
self.tableView.moveRowAtIndexPath(from, toIndexPath: to)
}
self.tableView.endUpdates()
if reloadIndexPaths.count > 0 {
self.tableView.reloadRowsAtIndexPaths(reloadIndexPaths, withRowAnimation: .Automatic)
}
Reload is outside the beginUpdates
and endUpdates
for reason stated previous. But if I exposes the origin index for substitution edit operation, theoretically, I probably can wrap all operations in beginUpdates
and endUpdates
.
But this is not the case. It seems a little counter intuitive but the exception thrown by UITableView is as follows:
2016-01-23 22:58:35.504 ChangesetTest[3247:111068] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to perform an insert and a move to the same index path (<NSIndexPath: 0xc000000003800016> {length = 2, path = 0 - 28})'
It stated that we are trying to attempt an insert and a move row operation but those code have not change. Funny thing is that if I commented out the reload row operation code (which is possible here as their value will just remain outdated), it will no longer complain.
I believe this is more of a wording bug, UIKit probably meant insert and reload of the same index. But this I can't be sure.
The funny things is that the API documentation for move only mention that it can work with insert and delete, but avoid mentioning reload.
As for UICollectionView, its API documentation for batch update only mention that it can work with insert, delete and move but did not mention about reload. Which will make the current solution for UITableView seems correct.
If I remove the reduce edit step. I can make it work nicely with reload in table view.
But the thing about reload is that it is the least obvious row operation visually. And there are several other options that are reasonable as well. For example, we can get visible rows and update the cell manually (no animation), or just not batching it like above (not an issue as well).
I have a comment regarding terminology.
The indices the library computes for the Edit
steps are currently always integers and always zero-based (i.e. the first index is 0
). This works great for the intended main use case (table and collection view updates) because the index paths of a table or collection view are also zero-based and composed of integers.
However, it doesn't reflect the reality of Swift's collection types. For example, ArraySlice
uses integer indices, but they aren't zero-based. If you compute the changeset for two ArraySlice
instances, the result is arguably wrong or at least confusing:
let source = [2,3,4,5].dropFirst() // ArraySlice<Int>
let target = [3,4,5,6].dropFirst() // ArraySlice<Int>
let edits = Changeset.edits(from: source, to: target)
print(edits)
// Prints "[delete 3 at index 0, insert 6 at index 2]"
If you tried to apply this changeset literally to the source collection (i.e. delete the element at index 0
from source
), youʼd get a crash because source
doesnʼt have an "index" 0
.
In a way, a similar issue is when you compute changesets for strings (as most of the unit tests do) because string indices are not integers, so saying something like "insert "a" at index 5" doesn't strictly make sense in Swift.
I find this a little confusing and misleading. Changeset
appears to be generic for any collection, and yet it doesn't follow Swift's conventions where the term "index" has a very specific meaning.
When I first noticed this "problem" I wanted to suggest replacing the integer indices in Edit
and EditOperation
with the collection's index type (i.e. something like T.Index
). On second thought, this doesn't make sense because a changeset would have to compute "virtual" indices for a collection that doesn't really exist, and since collections are free to invalidate any existing index upon mutation, I don't think you could compute the indices in a generic way. Moreover, using real collection indices would make supporting table/collection view updates harder, not easier.
After writing all this, I realize that the way this works is actually the best solution for the given problem. Having written this, I'm going to submit it too, even though I don't have a specific suggestion. Feel free to close this issue, and maybe it will help others understand this better when they find it in search.
My only suggestion would be to make it clear that Changeset
and Edit
aren't concerned with indices, but with offsets. I think if the debugDescription
read "delete x at offset 0"
etc. I would have been less confused. This is exemplified by the use of enumerated()
to compute the column
value in edits(from:to:)
, since enumerated()
computes offsets from 0 and not indices (see http://khanlou.com/2017/03/you-probably-don't-want-enumerated/ for more info on enumerated()
and how it's often misinterpreted).
I'm using Changeset v2.0, and I've run into an issue where I'm applying a batch delete which produces something akin to:
[0] { operation = .delete, value = …, destination = 0 }
[1] { operation = .delete, value = …, destination = 1 }
[2] { operation = .delete, value = …, destination = 2 }
[3] { operation = .delete, value = …, destination = 3 }
If I process this in order by applying the deletes to a table, this will (pretty obviously) crash when it tries to remove destination 3 (which in the target collection, no longer exists).
Is this something that Changeset should be handling for me? I don't recall this being an issue with v1.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.