Changesets is a small library used to drive UITableView
or UICollectionView
transitions when the underlying model changes.
If your table view or collection view's data source is backed by a Collection
(eg. Array
), Changesets
can calculate the inserts, deletes, and updates to transition between states.
Example:
var people: [Person] {
didSet {
// Calculate the changeset.
let changeset = oldValue.changeset(to: people)
// Start the transition animation - rows are animated using an .automatic row animation by default.
tableView.performUpdates(changeset: changeset)
}
}
This is just like the delegate callbacks that a NSFetchedResultsController
would emit when using Core Data. With Changesets
you get the fine-grained transitions without having to use Core Data.
For example: to transition an array from its original state:
A0 | B0 | C0 | D0 | H0 | I0 |
---|
to its new state:
B0 | C0 | D1 | F0 | H0 | I1 | K0 |
---|
The changeset would report the following changes to perform the transition:
- Update 'D' at index 3
- Update 'I' at index 5
- Delete 'A' at index 0
- Insert 'F' at index 3
- Insert 'G' at index 4
- Insert 'K' at index 7
Changesets
takes care of all the nitty-gritty details when updating a table view or collection view. It calculates the diff between the old value and the new value, applies the updates in the correct order, and batches them so they are animated together. All of this in two lines of code:
let changeset = oldValue.changeset(to: newValue)
collectionView.performUpdates(changeset: changeset))
To calculate the changeset it needs to match the collections' elements to each other. It supports two methods of matching:
Elements that implement Equatable
can be matched, with a restriction. When Equatable
is used, Changesets
can't tell the difference between an object in the collection that has changed and two completely different objects. All it knows is that the two values aren't equal. The Equatable
protocol doesn't have any concept of identity.
When an element is only Equatable
, we cannot know that A1 is an updated version of A0, so Changesets
will report this by doing a delete then insert, rather than an update:
- Delete A0
- Insert A1
To use finer-grained changesets you need to take identity into consideration. To do this you can use the Matchable
protocol. It's recommended that you implement Matchable
for all your types that are used for data sources. Matchable
requires one additional function to be implemented for each type:
func match(_ other: Self) -> MatchResult
Where the MatchResult
is one of three cases:
public enum MatchResult {
case sameIdentityEqualValue
case sameIdentityInequalValue
case differentIdentity
}
Matchable
also provides a default implementation for ==
, so implementing the Matchable
protocol gives you Equatable
for free.
A User
struct could be implemented as follows:
struct User: Matchable {
let userID: Int
let name: String
let messageCount: Int
func match(_ other: User) -> MatchResult {
guard userID == other.userID else {
return .differentIdentity
}
if name == other.name && messageCount == other.messageCount {
return .sameIdentityEqualValue
} else {
return .sameIdentityInequalValue
}
}
}
This way, when the messageCount
value changes, Changesets
can still recognize that the changed values represent the same user.
The first step is to calculate the changeset between the old state of the collection and the new state:
let changeset = oldValue.changeset(to: newValue)
If your data is in a single section you can perform the updates to a UITableView
with a single line:
tableView.performUpdates(changeset: changeset)
Or, a UICollectionView
:
collectionView.performUpdates(changeset: changeset)
The default row animation when updating a UITableView
is the .automatic
row animation. However, the animation can be configured using a custom TableViewChangesetPolicy
. The UITableViewRowAnimation
values for inserting, deleting, and updating can all be configured.
The animations for UICollectionViews
are defined by the collection view layout and are not affected by using changesets.
If your data is structured in sections you can split the calculations into two steps:
- Calculate the changeset between the sections.
- Calculate the changesets between each section's items.
Multiple changesets can be applied by using batch updates. For UITableView
:
tableView.beginUpdates()
tableView.applySectionUpdates(changeset: sectionsChangeset) // Insert / delete sections
tableView.applyRowUpdates(changeset: rowChangeset, fromSection: fromSection, toSection: toSection) // One changeset for each section
tableView.endUpdates()
And, for UICollectionView
:
collectionView.performBatchUpdates({
collectionView.applySectionUpdates(changeset: sectionsChangeset) // Insert / delete sections
collectionView.applyItemUpdates(changeset: itemsChangeset, fromSection: fromSection, toSection: toSection) // One changeset for each section
}, completion: nil)
If you’re using Carthage, simply add Changesets to your Cartfile
:
github "stevebrambilla/Changesets"
Otherwise, you can manually install it by following these steps:
- Add the Changesets repository as a submodule of your project's repository.
- Drag and drop Changesets.xcodeproj into your Xcode project or workspace.
- In the “General” tab of your application target’s settings, add
Changesets.framework
to the “Embedded Binaries” section.
Now import Changesets
and you're all set.
Changesets is available under the MIT license. See the LICENSE file for more information.