Skip to content

Instantly share code, notes, and snippets.

@karabatov
Forked from JaviSoto/Example.swift
Created May 26, 2016 09:27
Show Gist options
  • Select an option

  • Save karabatov/d411dd02421ab0f53dcb5d51e082c750 to your computer and use it in GitHub Desktop.

Select an option

Save karabatov/d411dd02421ab0f53dcb5d51e082c750 to your computer and use it in GitHub Desktop.

Revisions

  1. @JaviSoto JaviSoto revised this gist May 12, 2016. 1 changed file with 92 additions and 0 deletions.
    92 changes: 92 additions & 0 deletions GenericTableViewDataSource.swift
    Original 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)
    }
    }
  2. @JaviSoto JaviSoto revised this gist May 9, 2016. 1 changed file with 105 additions and 0 deletions.
    105 changes: 105 additions & 0 deletions TableSectionDataDiffingTableViewTests.swift
    Original 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())
    }
    }
    }
    }
    }
  3. @JaviSoto JaviSoto created this gist May 9, 2016.
    7 changes: 7 additions & 0 deletions Example.swift
    Original 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)}
    }
    }
    182 changes: 182 additions & 0 deletions TableSectionDataDiffing.swift
    Original 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()
    }
    }
    }
    88 changes: 88 additions & 0 deletions TableSectionDataDiffingTests.swift
    Original 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)])))
    }
    }
    }
    }
    72 changes: 72 additions & 0 deletions TestTableSectionTypes.swift
    Original 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
    }
    }