Last active
July 17, 2017 06:16
-
-
Save JaviSoto/ede392c04615cfcc6d61b733ae12e3a2 to your computer and use it in GitHub Desktop.
TableSectionDataDiffing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var sections: [MySectionType] { | |
didSet { | |
let operations = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldValue, toSections: sections) | |
self.tableView.applyTableOperations(operations, withAnimations: updateAnimations)} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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() | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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()) | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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)]))) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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