Skip to content

Instantly share code, notes, and snippets.

@odlp
Created March 28, 2016 15:27
Show Gist options
  • Save odlp/2c8936f850f8996d9a3a to your computer and use it in GitHub Desktop.
Save odlp/2c8936f850f8996d9a3a to your computer and use it in GitHub Desktop.
Swift list differ for smarter UITableView refreshes
import Foundation
class ListDiffer<T: Equatable> {
private let beforeList: [T]
private let afterList: [T]
private let section: Int
private let elementComparison: ((T, T) -> Bool)
init(before: [T], after: [T], section: Int = 0, elementComparison: ((T, T) -> Bool) = { $0 == $1 }) {
self.beforeList = before
self.afterList = after
self.section = section
self.elementComparison = elementComparison
}
var deletions: [NSIndexPath] {
return beforeList.enumerate().flatMap { (i, el) in
guard !collectionContainsElement(self.afterList, el) else { return nil }
return NSIndexPath(forRow: i, inSection: section)
}
}
var additions: [NSIndexPath] {
return afterList.enumerate().flatMap { (i, el) in
guard !collectionContainsElement(self.beforeList, el) else { return nil }
return NSIndexPath(forRow: i, inSection: section)
}
}
var moves: [ListDifferMove] {
return beforeList.enumerate().flatMap { (beforeIndex, el) in
guard let afterIndex = indexOfElementInCollection(el, afterList) else {
return nil
}
if beforeIndex == afterIndex { return nil }
return ListDifferMove(
from: NSIndexPath(forRow: beforeIndex, inSection: section),
to: NSIndexPath(forRow: afterIndex, inSection: section)
)
}
}
var needsRefresh: [NSIndexPath] {
return beforeList.flatMap({ el -> NSIndexPath? in
guard let afterIndex = indexOfElementInCollection(el, afterList) else { return nil }
if noUpdateNeeded(el, afterList[afterIndex]) { return nil }
return NSIndexPath(forRow: afterIndex, inSection: section)
})
}
// MARK: - Private
private func collectionContainsElement(collection: [T], _ element: T) -> Bool {
return indexOfElementInCollection(element, collection) != nil
}
private func indexOfElementInCollection(element: T, _ collection: [T]) -> Int? {
return collection.indexOf { elementComparison($0, element) }
}
private func noUpdateNeeded(a: T, _ b: T) -> Bool {
return a == b
}
}
struct ListDifferMove: Equatable {
let from: NSIndexPath
let to: NSIndexPath
}
func ==(lhs: ListDifferMove, rhs: ListDifferMove) -> Bool {
return lhs.from == rhs.from
&& lhs.to == rhs.to
}
import XCTest
@testable import ListDiffer
class ListDifferTests: XCTestCase {
var item1: Item!
var item2: Item!
override func setUp() {
continueAfterFailure = false
item1 = Item(id: "123", name: "Foo")
item2 = Item(id: "456", name: "Bar")
}
func testNoChanges() {
let differ = ListDiffer(before: [item1], after: [item1])
XCTAssertEqual(differ.deletions, [], "Expected no deletions")
XCTAssertEqual(differ.additions, [], "Expected no additions")
XCTAssertEqual(differ.moves, [], "Expected no moves")
XCTAssertEqual(differ.needsRefresh, [], "Expected no refreshes")
}
func testDeletion() {
let differ = ListDiffer(before: [item1], after: [])
XCTAssertEqual(differ.deletions, [NSIndexPath(forRow: 0, inSection: 0)], "Expected deletion at index 0")
XCTAssertEqual(differ.additions, [], "Expected no additions")
XCTAssertEqual(differ.moves, [], "Expected no moves")
XCTAssertEqual(differ.needsRefresh, [], "Expected no refreshes")
}
func testAddition() {
let differ = ListDiffer(before: [], after: [item1])
XCTAssertEqual(differ.deletions, [], "Expected no deletions")
XCTAssertEqual(differ.additions, [NSIndexPath(forRow: 0, inSection: 0)], "Expected addition at index 0")
XCTAssertEqual(differ.moves, [], "Expected no moves")
XCTAssertEqual(differ.needsRefresh, [], "Expected no refreshes")
}
func testMoves() {
let differ = ListDiffer(before: [item1, item2], after: [item2, item1])
XCTAssertEqual(differ.deletions, [], "Expected no deletions")
XCTAssertEqual(differ.additions, [], "Expected no additions")
XCTAssertEqual(differ.needsRefresh, [], "Expected no refreshes")
let expectedMoveA = ListDifferMove(from: NSIndexPath(forRow: 0, inSection: 0), to: NSIndexPath(forRow: 1, inSection: 0))
let expectedMoveB = ListDifferMove(from: NSIndexPath(forRow: 1, inSection: 0), to: NSIndexPath(forRow: 0, inSection: 0))
let moves = differ.moves
XCTAssertEqual(moves.count, 2, "Expected 2 moves")
XCTAssertTrue(moves.contains(expectedMoveA), "Expected move from index 0 to 1")
XCTAssertTrue(moves.contains(expectedMoveB), "Expected move from index 1 to 0")
}
func testDefaultComparison_IsStrictEquality() {
let before = Item(id: "999", name: "Foo")
let after = Item(id: "999", name: "Foo updated")
XCTAssertNotEqual(before, after, "Items are not strictly equal (different name)")
let differ = ListDiffer(before: [before], after: [after])
XCTAssertEqual(differ.deletions.count, 1, "Strict equality considers this a deletion & addition")
XCTAssertEqual(differ.additions.count, 1, "Strict equality considers this a deletion & addition")
XCTAssertEqual(differ.moves, [], "Expected no moves")
XCTAssertEqual(differ.needsRefresh.count, 0, "Strict equality considers this a deletion & addition")
}
func testSpecifyingComparison_AllowsRefreshesToBeIdentified() {
let before = Item(id: "999", name: "Foo")
let after = Item(id: "999", name: "Foo updated")
let differ = ListDiffer(
before: [before],
after: [after],
elementComparison: { $0.id == $1.id }
)
XCTAssertEqual(differ.deletions, [], "Expected no deletions")
XCTAssertEqual(differ.additions, [], "Expected no additions")
XCTAssertEqual(differ.moves, [], "Expected no moves")
XCTAssertEqual(differ.needsRefresh, [NSIndexPath(forRow: 0, inSection: 0)], "Expected refreshes at index 0")
}
func testSpecifyingSection() {
let differ = ListDiffer(before: [item1], after: [], section: 99)
XCTAssertEqual(differ.deletions.first?.section, 99, "Expected deletion in section 99")
}
func testNeedsMoveAndUpdate() {
let item1After = Item(id: item1.id, name: "Foobar")
let differ = ListDiffer(
before: [item1, item2],
after: [item2, item1After],
elementComparison: { $0.id == $1.id }
)
XCTAssertEqual(differ.deletions.count, 0, "Expected no deletions")
XCTAssertEqual(differ.additions.count, 0, "Expected no additions")
XCTAssertEqual(differ.needsRefresh, [NSIndexPath(forRow: 1, inSection: 0)], "Expected needs update at index 1")
let expectedMoveA = ListDifferMove(from: NSIndexPath(forRow: 0, inSection: 0), to: NSIndexPath(forRow: 1, inSection: 0))
let expectedMoveB = ListDifferMove(from: NSIndexPath(forRow: 1, inSection: 0), to: NSIndexPath(forRow: 0, inSection: 0))
let moves = differ.moves
XCTAssertEqual(moves.count, 2, "Expected 2 moves")
XCTAssertTrue(moves.contains(expectedMoveA), "Expected move from index 0 to 1")
XCTAssertTrue(moves.contains(expectedMoveB), "Expected move from index 1 to 0")
}
}
import UIKit
class TestTableViewController: UITableViewController {
var items = [
Item(id: "123", name: "Foobar")
]
// Abridged
func updateTable(newItems: [Item]) {
let differ = ListDiffer(
before: self.items,
after: newItems,
elementComparison: { $0.id == $1.id }
)
self.items = newItems
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths(differ.additions, withRowAnimation: .Automatic)
tableView.deleteRowsAtIndexPaths(differ.deletions, withRowAnimation: .Automatic)
tableView.reloadRowsAtIndexPaths(differ.needsRefresh, withRowAnimation: .Automatic)
for move in differ.moves {
tableView.moveRowAtIndexPath(move.from, toIndexPath: move.to)
}
tableView.endUpdates()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment