-
-
Save karabatov/d411dd02421ab0f53dcb5d51e082c750 to your computer and use it in GitHub Desktop.
Revisions
-
JaviSoto revised this gist
May 12, 2016 . 1 changed file with 92 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,92 @@ // // 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) } } -
JaviSoto revised this gist
May 9, 2016 . 1 changed file with 105 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,105 @@ // // 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()) } } } } } -
JaviSoto created this gist
May 9, 2016 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,7 @@ var sections: [MySectionType] { didSet { let operations = TableSectionDataDiffing.tableOperationsToUpdateFromSections(sections: oldValue, toSections: sections) self.tableView.applyTableOperations(operations, withAnimations: updateAnimations)} } } This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,182 @@ // // 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 hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,88 @@ // // 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 hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,72 @@ // // 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 } }