Skip to content

Instantly share code, notes, and snippets.

@JaviSoto
Last active July 17, 2017 06:16
Show Gist options
  • Save JaviSoto/ede392c04615cfcc6d61b733ae12e3a2 to your computer and use it in GitHub Desktop.
Save JaviSoto/ede392c04615cfcc6d61b733ae12e3a2 to your computer and use it in GitHub Desktop.
TableSectionDataDiffing
var sections: [MySectionType] {
didSet {
let operations = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldValue, toSections: sections)
self.tableView.applyTableOperations(operations, withAnimations: updateAnimations)}
}
}
//
// GenericTableViewDataSource.swift
// Instant
//
// Created by Javier Soto on 3/15/16.
// Copyright © 2016 Fabric. All rights reserved.
//
import UIKit
import ReactiveCocoa
import func FabricAPI.debugAssertMainThread
import enum Result.NoError
final class GenericTableViewDataSource<ObservableTableViewData: PropertyType, SectionType: TableSectionType, RowType: TableRowType where SectionType.AssociatedTableRowType == RowType, SectionType: Equatable, RowType: Equatable>: NSObject, UITableViewDataSource {
typealias ConfigureRow = (row: RowType, indexPath: NSIndexPath) -> UITableViewCell
private unowned let tableView: UITableView
private let tableViewData: ObservableTableViewData
private let updateAnimations: TableUpdateOperationAnimations?
private let computeSections: ObservableTableViewData.Value -> [SectionType]
private let configureRow: ConfigureRow
private var sectionsMutableProperty = MutableProperty<[SectionType]>([])
var sections: AnyProperty<[SectionType]>
private var tableViewUpdatesObserver: Observer<(), NoError>
var tableViewUpdatesProducer: SignalProducer<(), NoError>
init(tableView: UITableView, tableViewData: ObservableTableViewData, updateAnimations: TableUpdateOperationAnimations? = TableUpdateOperation.defaultAnimations, computeSections: ObservableTableViewData.Value -> [SectionType], configureRow: ConfigureRow) {
self.tableView = tableView
self.tableViewData = tableViewData
self.updateAnimations = updateAnimations
self.computeSections = computeSections
self.configureRow = configureRow
self.sections = AnyProperty(self.sectionsMutableProperty)
(self.tableViewUpdatesProducer, self.tableViewUpdatesObserver) = SignalProducer<(), NoError>.buffer(0)
super.init()
self.sectionsMutableProperty <~ self.tableViewData.producer
.map(computeSections)
self.sectionsMutableProperty.producer
.combinePrevious([])
.startWithNext { [unowned self] oldValue, sections in
debugAssertMainThread()
if let updateAnimations = self.updateAnimations {
let operations = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldValue, toSections: sections)
self.tableView.applyTableOperations(operations, withAnimations: updateAnimations) { [weak self] in
self?.tableViewUpdatesObserver.sendNext(())
}
}
else {
self.tableView.reloadData()
self.tableViewUpdatesObserver.sendNext(())
}
}
}
func indexPathsOfRowsPassingTest(test: RowType -> Bool) -> [NSIndexPath] {
return self.sections.value.enumerate().flatMap { sectionIndex, element in
element.rows
.enumerate()
.filter { test($0.element) }
.map { NSIndexPath(forRow: $0.index, inSection: sectionIndex) }
}
}
func rowAtIndexPath(indexPath: NSIndexPath) -> RowType {
return self.sections.value[indexPath.section].rows[indexPath.row]
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.sections.value[section].title
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.sections.value.count
}
@objc func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sections.value[section].rows.count
}
@objc func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return self.configureRow(row: self.rowAtIndexPath(indexPath), indexPath: indexPath)
}
}
//
// TableSectionDataDiffing.swift
// Fabric
//
// Created by Javier Soto on 1/10/16.
// Copyright © 2016 Fabric. All rights reserved.
//
import UIKit
protocol TableSectionType {
associatedtype AssociatedTableRowType: TableRowType
var rows: [AssociatedTableRowType] { get }
var title: String? { get }
}
extension TableSectionType {
var title: String? { return nil }
}
protocol TableRowType {
}
enum TableUpdateOperation {
case ReloadSections(NSIndexSet)
case InsertSections(NSIndexSet)
case DeleteSections(NSIndexSet)
case InsertRows([NSIndexPath])
case DeleteRows([NSIndexPath])
case ReloadRows([NSIndexPath])
}
struct AnySection<AssociatedTableRowType: protocol<TableRowType, Equatable>>: TableSectionType, Equatable {
let title: String?
let rows: [AssociatedTableRowType]
init(title: String? = nil, rows: [AssociatedTableRowType]) {
self.title = title
self.rows = rows
}
}
func ==<T>(lhs: AnySection<T>, rhs: AnySection<T>) -> Bool {
return lhs.title == rhs.title &&
lhs.rows == rhs.rows
}
struct TableSectionDataDiffing {
static func tableOperationsToUpdateFromRows<R: TableRowType where R: Equatable>(rows: [R], toRows newRows: [R]) -> [TableUpdateOperation] {
return self.tableOperationsToUpdateFromSections(sections: [AnySection(rows: rows)], toSections: [AnySection(rows: newRows)])
}
static func tableOperationsToUpdateFromSections<S: TableSectionType where S: Equatable, S.AssociatedTableRowType: Equatable>(sections oldSections: [S], toSections newSections: [S]) -> [TableUpdateOperation] {
guard newSections != oldSections else {
return []
}
var operations: [TableUpdateOperation] = []
// Section changes:
let oldSectionCount = oldSections.count
let newSectionCount = newSections.count
let sectionCountDelta = newSectionCount - oldSectionCount
if sectionCountDelta > 0 {
operations.append(.InsertSections(NSIndexSet(indexesInRange: NSMakeRange(oldSectionCount, sectionCountDelta))))
}
else if sectionCountDelta < 0 {
operations.append(.DeleteSections(NSIndexSet(indexesInRange: NSMakeRange(newSectionCount, -sectionCountDelta))))
}
let commonCount = min(oldSectionCount, newSectionCount)
let sectionsThatChanged = newSections.prefix(commonCount).enumerate().filter { $0.element != oldSections[$0.index] }
sectionsThatChanged.map { $0.index }.forEach { sectionIndex in
let titleChanged = oldSections[sectionIndex].title != newSections[sectionIndex].title
if titleChanged {
operations.append(.ReloadSections(NSIndexSet(index: sectionIndex)))
}
// Row changes in this section
let oldRowsInSection = oldSections[sectionIndex].rows
let newRowsInSection = newSections[sectionIndex].rows
let oldRowCount = oldRowsInSection.count
let newRowCount = newRowsInSection.count
let rowCountDelta = newRowCount - oldRowCount
if rowCountDelta > 0 {
let indexPaths = (oldRowCount..<newRowCount).map { NSIndexPath(forRow: $0, inSection: sectionIndex) }
operations.append(.InsertRows(indexPaths))
}
else if rowCountDelta < 0 {
let indexPaths = (newRowCount..<oldRowCount).map { NSIndexPath(forRow: $0, inSection: sectionIndex) }
operations.append(.DeleteRows(indexPaths))
}
let commonRowCount = min(oldRowCount, newRowCount)
let rowsThatChanged = newRowsInSection
.prefix(commonRowCount)
.enumerate()
.filter { $0.element != oldRowsInSection[$0.index] }
.map { NSIndexPath(forRow: $0.index, inSection: sectionIndex) }
if !rowsThatChanged.isEmpty {
operations.append(.ReloadRows(rowsThatChanged))
}
}
return operations
}
}
typealias TableUpdateOperationAnimations = TableUpdateOperation -> UITableViewRowAnimation
extension TableUpdateOperation {
static func noAnimations(_: TableUpdateOperation) -> UITableViewRowAnimation {
return .None
}
static func defaultAnimations(operation: TableUpdateOperation) -> UITableViewRowAnimation {
switch operation {
case .ReloadSections: return .Fade
case .InsertSections: return .None
case .DeleteSections: return .Fade
case .InsertRows: return .Fade
case .DeleteRows: return .None
case .ReloadRows: return .Fade
}
}
}
extension UITableView {
private func applyTableOperation(operation: TableUpdateOperation, withAnimation animation: UITableViewRowAnimation) {
switch operation {
case let .ReloadSections(sections):
self.reloadSections(sections, withRowAnimation: animation)
case let .InsertSections(sections):
self.insertSections(sections, withRowAnimation: animation)
case let .DeleteSections(sections):
self.deleteSections(sections, withRowAnimation: animation)
case let .InsertRows(indexPaths):
self.insertRowsAtIndexPaths(indexPaths, withRowAnimation: animation)
case let .DeleteRows(indexPaths):
self.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: animation)
case let .ReloadRows(indexPaths):
self.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: animation)
}
}
func applyTableOperations(tableOperations: [TableUpdateOperation], withAnimations animations: TableUpdateOperationAnimations = TableUpdateOperation.defaultAnimations, completion: (() -> ())? = nil) {
guard !tableOperations.isEmpty else { return }
if let completion = completion {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
}
self.beginUpdates()
tableOperations.forEach { operation in
let animation = animations(operation)
self.applyTableOperation(operation, withAnimation: animation)
}
self.endUpdates()
if completion != nil {
CATransaction.commit()
}
}
}
//
// TableSectionDataDiffingTableViewTests.swift
// Fabric
//
// Created by Javier Soto on 1/10/16.
// Copyright © 2016 Fabric. All rights reserved.
//
import Foundation
import Quick
import Nimble
@testable import FabricApp
private final class TestTableViewDataSource: NSObject, UITableViewDataSource {
var sections: [TestSection] = []
@objc private func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.sections.count
}
@objc private func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sections[section].rows.count
}
@objc private func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Doesn't really matter
return UITableViewCell(style: .Default, reuseIdentifier: nil)
}
}
class TableSectionDataDiffingTableViewTests: QuickSpec {
override func spec() {
describe("Applying table update operations") {
struct TableSectionDiff {
let old: [TestSection]
let new: [TestSection]
}
let diffs: [TableSectionDiff] = [
TableSectionDiff(
old: [.Section1([.Row1]), .Section2([])],
new: [.Section1([.Row1]), .Section2([])]
),
TableSectionDiff(
old: [],
new: [.Section1([])]
),
TableSectionDiff(
old: [.Section1([])],
new: []
),
TableSectionDiff(
old: [.Section1([])],
new: [.Section1([])]
),
TableSectionDiff(
old: [.Section1([])],
new: [.Section2([])]
),
TableSectionDiff(
old: [.Section1([])],
new: [.Section1([.Row1])]
),
TableSectionDiff(
old: [.Section1([])],
new: [.Section2([.Row1])]
),
TableSectionDiff(
old: [.Section1([.Row1])],
new: [.Section2([.Row1])]
),
TableSectionDiff(
old: [.Section1([.Row1])],
new: [.Section1([.Row1, .Row3])]
),
TableSectionDiff(
old: [.Section1([.Row1])],
new: [.Section1([])]
),
TableSectionDiff(
old: [.Section1([.Row1])],
new: [.Section1([.Row2])]
),
]
for (index, diff) in diffs.enumerate() {
it("TableView doesn't raise exception (check \(index + 1)") {
let tableViewController = UITableViewController()
let dataSource = TestTableViewDataSource()
let tableView = tableViewController.tableView
dataSource.sections = diff.old
tableView.dataSource = dataSource
tableView.reloadData()
dataSource.sections = diff.new
let operations = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: diff.old, toSections: diff.new)
expect(tableView.applyTableOperations(operations)).toNot(raiseException())
}
}
}
}
}
//
// TableSectionDataDiffingTests.swift
// Fabric
//
// Created by Javier Soto on 1/10/16.
// Copyright © 2016 Fabric. All rights reserved.
//
import Foundation
import Quick
import Nimble
@testable import FabricApp
class TableSectionDataDiffingTests: QuickSpec {
override func spec() {
describe("Section Updates") {
it("Returns empty array for equal sections") {
let oldSections: [TestSection] = [.Section1([.Row1]), .Section2([])]
let newSections: [TestSection] = [.Section1([.Row1]), .Section2([])]
let diff = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldSections, toSections: newSections)
expect(diff).to(beEmpty())
}
it("Inserts a new section") {
let oldSections: [TestSection] = []
let newSections: [TestSection] = [.Section1([])]
let diff = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldSections, toSections: newSections)
expect(diff).to(haveCount(1))
expect(diff.first).to(equal(TableUpdateOperation.InsertSections(NSIndexSet(index: 0))))
}
it("Deletes a section") {
let oldSections: [TestSection] = [.Section1([])]
let newSections: [TestSection] = []
let diff = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldSections, toSections: newSections)
expect(diff).to(haveCount(1))
expect(diff.first).to(equal(TableUpdateOperation.DeleteSections(NSIndexSet(index: 0))))
}
it("Doesn't update sections if there's the same amount") {
let oldSections: [TestSection] = [.Section1([])]
let newSections: [TestSection] = [.Section2([])]
let diff = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldSections, toSections: newSections)
expect(diff.filter { $0.isSectionUpdate }).to(beEmpty())
}
}
describe("Row Updates") {
it("Inserts a new row") {
let oldSections: [TestSection] = [.Section1([])]
let newSections: [TestSection] = [.Section1([.Row1])]
let diff = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldSections, toSections: newSections)
expect(diff).to(haveCount(1))
expect(diff.first).to(equal(TableUpdateOperation.InsertRows([NSIndexPath(forRow: 0, inSection: 0)])))
}
it("Deletes a row") {
let oldSections: [TestSection] = [.Section1([.Row1])]
let newSections: [TestSection] = [.Section1([])]
let diff = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldSections, toSections: newSections)
expect(diff).to(haveCount(1))
expect(diff.first).to(equal(TableUpdateOperation.DeleteRows([NSIndexPath(forRow: 0, inSection: 0)])))
}
it("Reloads a row") {
let oldSections: [TestSection] = [.Section1([.Row1])]
let newSections: [TestSection] = [.Section1([.Row2])]
let diff = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldSections, toSections: newSections)
expect(diff).to(haveCount(1))
expect(diff.first).to(equal(TableUpdateOperation.ReloadRows([NSIndexPath(forRow: 0, inSection: 0)])))
}
}
}
}
//
// TestTableSectionTypes.swift
// Fabric
//
// Created by Javier Soto on 1/10/16.
// Copyright © 2016 Fabric. All rights reserved.
//
import Foundation
@testable import FabricApp
enum TestSection: TableSectionType, Equatable {
case Section1([TestRow])
case Section2([TestRow])
case Section3([TestRow])
var rows: [TestRow] {
switch self {
case let .Section1(rows): return rows
case let .Section2(rows): return rows
case let .Section3(rows): return rows
}
}
}
enum TestRow: TableRowType, Equatable {
case Row1
case Row2
case Row3
}
func ==(lhs: TestSection, rhs: TestSection) -> Bool {
guard lhs.rows == rhs.rows else { return false }
switch (lhs, rhs) {
case (.Section1, .Section1): return true
case (.Section2, .Section2): return true
case (.Section3, .Section3): return true
default: return false
}
}
func ==(lhs: TestRow, rhs: TestRow) -> Bool {
switch (lhs, rhs) {
case (.Row1, .Row1): return true
case (.Row2, .Row2): return true
case (.Row3, .Row3): return true
default: return false
}
}
extension TableUpdateOperation {
var isSectionUpdate: Bool {
switch self {
case .ReloadSections, .InsertSections, .DeleteSections: return true
case .InsertRows, .DeleteRows, .ReloadRows: return false
}
}
}
extension TableUpdateOperation: Equatable { }
func ==(lhs: TableUpdateOperation, rhs: TableUpdateOperation) -> Bool {
switch (lhs, rhs) {
case let (.ReloadSections(sections1), .ReloadSections(sections2)) where sections1.isEqualToIndexSet(sections2): return true
case let (.InsertSections(sections1), .InsertSections(sections2)) where sections1.isEqualToIndexSet(sections2): return true
case let (.DeleteSections(sections1), .DeleteSections(sections2)) where sections1.isEqualToIndexSet(sections2): return true
case let (.InsertRows(indexPaths1), .InsertRows(indexPaths2)) where indexPaths1 == indexPaths2: return true
case let (.DeleteRows(indexPaths1), .DeleteRows(indexPaths2)) where indexPaths1 == indexPaths2: return true
case let (.ReloadRows(indexPaths1), .ReloadRows(indexPaths2)) where indexPaths1 == indexPaths2: return true
default: return false
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment