<NME> Table.swift
<BEF> //
//  Table.swift
//  TemplateKit
//
//  Created by Matias Cudich on 10/29/16.
//  Copyright © 2016 Matias Cudich. All rights reserved.
//

import Foundation
import CSSLayout

public struct TableSection: Hashable {
  public let items: [AnyHashable]
  public var hashValue: Int

  public init(items: [AnyHashable], hashValue: Int) {
    self.items = items
    self.hashValue = hashValue
  public var isEditing: Bool?

  // This is used to know when the underlying table view rows should be inserted, deleted or moved.
  // By modifying this dictionary, the table is notified that it should perform the operations
  // necessary to reflect the changes in the underlying data source.
  public var itemKeys: [IndexPath: AnyHashable]?

  public var onEndReached: Selector?
  public var onEndReachedThreshold: CGFloat?
  weak public var tableViewDelegate: TableViewDelegate?
  weak public var tableViewDataSource: TableViewDataSource?
  weak public var eventTarget: Node?

  public var isEditing: Bool?

  // This is used to know when the underlying table view rows should be inserted, deleted or moved.
    tableViewDataSource = properties["tableViewDataSource"] as? TableViewDataSource
    eventTarget = properties["eventTarget"] as? Node
    isEditing = properties.cast("isEditing")
    itemKeys = properties["itemKeys"] as? [IndexPath: AnyHashable]
    onEndReached = properties.cast("onEndReached")
    onEndReachedThreshold = properties.cast("onEndReachedThreshold")
  }

  public init() {}

  public init(_ properties: [String : Any]) {
    core = CoreProperties(properties)
    merge(&tableViewDataSource, other.tableViewDataSource)
    merge(&eventTarget, other.eventTarget)
    merge(&isEditing, other.isEditing)
    merge(&itemKeys, other.itemKeys)
    merge(&onEndReached, other.onEndReached)
    merge(&onEndReachedThreshold, other.onEndReachedThreshold)
  }
}

public func ==(lhs: TableProperties, rhs: TableProperties) -> Bool {
  return lhs.tableViewDelegate === rhs.tableViewDelegate && lhs.tableViewDataSource === rhs.tableViewDataSource && lhs.eventTarget === rhs.eventTarget && lhs.isEditing == rhs.isEditing && lhs.itemKeys == rhs.itemKeys && lhs.onEndReached == rhs.onEndReached && lhs.onEndReachedThreshold == rhs.onEndReachedThreshold && lhs.equals(otherProperties: rhs)
}

protocol ScrollProxyDelegate: class {
    merge(&tableViewDataSource, other.tableViewDataSource)
    merge(&eventTarget, other.eventTarget)
    merge(&isEditing, other.isEditing)
    merge(&items, other.items)
    merge(&onEndReached, other.onEndReached)
    merge(&onEndReachedThreshold, other.onEndReachedThreshold)
  }
}

public func ==(lhs: TableProperties, rhs: TableProperties) -> Bool {
  return lhs.tableViewDelegate === rhs.tableViewDelegate && lhs.tableViewDataSource === rhs.tableViewDataSource && lhs.eventTarget === rhs.eventTarget && lhs.isEditing == rhs.isEditing && lhs.items == rhs.items && lhs.onEndReached == rhs.onEndReached && lhs.onEndReachedThreshold == rhs.onEndReachedThreshold && lhs.equals(otherProperties: rhs)
}

public class Table: PropertyNode {
  public weak var parent: Node?

  public var properties: TableProperties {
    didSet {
    }
  }
  public var children: [Node]?
    }
  }
  public var children: [Node]?
  public var element: ElementData<TableProperties>
  public var cssNode: CSSNode?

  lazy public var view: View = {
    return TableView(frame: CGRect.zero, style: .plain, context: self.getContext())
  }()

  private var tableView: TableView? {
    return view as? TableView
  }

  private lazy var scrollProxy: ScrollProxy = {
    let proxy = ScrollProxy()
    proxy.delegate = self
    return proxy
  }()

  private var delegateProxy: DelegateProxy?
  fileprivate var lastReportedEndLength: CGFloat = 0

  public required init(element: ElementData<TableProperties>, children: [Node]? = nil, owner: Node? = nil, context: Context? = nil) {
    self.element = element
    self.properties = element.properties
    self.children = children
    self.owner = owner
    self.context = context

    if let items = properties.items {
      precondition(items.count > 0, "Items must contain at least one section.")
    }

    updateParent()
  }

  public func build() -> View {
    if delegateProxy == nil || delegateProxy?.target !== properties.tableViewDelegate {
      delegateProxy = DelegateProxy(target: properties.tableViewDelegate, interceptor: scrollProxy)
      delegateProxy?.registerInterceptable(selector: #selector(UIScrollViewDelegate.scrollViewDidScroll(_:)))
    }

    if tableView?.tableViewDelegate !== delegateProxy {
      tableView?.tableViewDelegate = delegateProxy as? TableViewDelegate

    return view
  }
}

extension Table: ScrollProxyDelegate {
    var insertions = [IndexPath]()
    var updates = [IndexPath]()

    for (sectionIndex, section) in new.enumerated() {
      let oldItems = old.count > sectionIndex ? old[sectionIndex].items : []
      let result = diff(oldItems, section.items)
      for step in result {
        switch step {
        case .delete(let index):
          deletions.append(IndexPath(row: index, section: sectionIndex))
        case .insert(let index):
          insertions.append(IndexPath(row: index, section: sectionIndex))
        case .update(let index):
          updates.append(IndexPath(row: index, section: sectionIndex))
        default:
          break
        }
      }
    }

    tableView?.deleteRows(at: deletions, with: .none)
    tableView?.insertRows(at: insertions, with: .none)
    tableView?.reloadRows(at: updates, with: .none)

    tableView?.endUpdates()
  }
}

extension Table: ScrollProxyDelegate {
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    properties.tableViewDelegate?.scrollViewDidScroll?(scrollView)

    let threshold = properties.onEndReachedThreshold ?? 0
    if let onEndReached = properties.onEndReached, scrollView.contentSize.height != lastReportedEndLength, distanceFromEnd(of: scrollView) < threshold, let owner = owner {
      lastReportedEndLength = scrollView.contentSize.height
      _ = (owner as AnyObject).perform(onEndReached)
    }
  }

  private func distanceFromEnd(of scrollView: UIScrollView) -> CGFloat {
    return scrollView.contentSize.height - (scrollView.bounds.height + scrollView.contentOffset.y)
  }
}

<MSG> Use new diff utility

AsyncDataList is still broken for moves

<DFF> @@ -19,9 +19,10 @@ public struct TableProperties: Properties {
   public var isEditing: Bool?
 
   // This is used to know when the underlying table view rows should be inserted, deleted or moved.
-  // By modifying this dictionary, the table is notified that it should perform the operations
-  // necessary to reflect the changes in the underlying data source.
-  public var itemKeys: [IndexPath: AnyHashable]?
+  // This 2-d array should follow the list of sections and rows provided by the data source. When
+  // this value changes, the table is automatically updated for you using the minimal set of
+  // operations required.
+  public var items: [[AnyHashable]]?
 
   public var onEndReached: Selector?
   public var onEndReachedThreshold: CGFloat?
@@ -35,7 +36,7 @@ public struct TableProperties: Properties {
     tableViewDataSource = properties["tableViewDataSource"] as? TableViewDataSource
     eventTarget = properties["eventTarget"] as? Node
     isEditing = properties.cast("isEditing")
-    itemKeys = properties["itemKeys"] as? [IndexPath: AnyHashable]
+    items = properties["items"] as? [[AnyHashable]]
     onEndReached = properties.cast("onEndReached")
     onEndReachedThreshold = properties.cast("onEndReachedThreshold")
   }
@@ -47,14 +48,14 @@ public struct TableProperties: Properties {
     merge(&tableViewDataSource, other.tableViewDataSource)
     merge(&eventTarget, other.eventTarget)
     merge(&isEditing, other.isEditing)
-    merge(&itemKeys, other.itemKeys)
+    merge(&items, other.items)
     merge(&onEndReached, other.onEndReached)
     merge(&onEndReachedThreshold, other.onEndReachedThreshold)
   }
 }
 
 public func ==(lhs: TableProperties, rhs: TableProperties) -> Bool {
-  return lhs.tableViewDelegate === rhs.tableViewDelegate && lhs.tableViewDataSource === rhs.tableViewDataSource && lhs.eventTarget === rhs.eventTarget && lhs.isEditing == rhs.isEditing && lhs.itemKeys == rhs.itemKeys && lhs.onEndReached == rhs.onEndReached && lhs.onEndReachedThreshold == rhs.onEndReachedThreshold && lhs.equals(otherProperties: rhs)
+  return lhs.tableViewDelegate === rhs.tableViewDelegate && lhs.tableViewDataSource === rhs.tableViewDataSource && lhs.eventTarget === rhs.eventTarget && lhs.isEditing == rhs.isEditing && lhs.items == rhs.items && lhs.onEndReached == rhs.onEndReached && lhs.onEndReachedThreshold == rhs.onEndReachedThreshold && lhs.equals(otherProperties: rhs)
 }
 
 protocol ScrollProxyDelegate: class {
@@ -76,6 +77,9 @@ public class Table: PropertyNode {
 
   public var properties: TableProperties {
     didSet {
+      if let oldItems = oldValue.items, let newItems = properties.items, oldItems != newItems {
+        updateRows(old: oldItems, new: newItems)
+      }
     }
   }
   public var children: [Node]?
@@ -127,6 +131,32 @@ public class Table: PropertyNode {
 
     return view
   }
+
+  private func updateRows(old: [[AnyHashable]], new: [[AnyHashable]]) {
+    tableView?.beginUpdates()
+    for (index, section) in new.enumerated() {
+      let result = diff(old[index], section)
+      if !result.hasChanges {
+        continue
+      }
+      if result.insert.count > 0 {
+        let indexPaths = result.insert.map { IndexPath(row: $0, section: index) }
+        tableView?.insertRows(at: indexPaths, with: .none)
+      }
+      if result.remove.count > 0 {
+        let indexPaths = result.remove.map { IndexPath(row: $0, section: index) }
+        tableView?.deleteRows(at: indexPaths, with: .none)
+      }
+      for move in result.move {
+        tableView?.moveRow(at: IndexPath(row: move.from, section: index), to: IndexPath(row: move.to, section: index))
+      }
+      if result.update.count > 0 {
+        let indexPaths = result.update.map { IndexPath(row: $0, section: index) }
+        tableView?.reloadRows(at: indexPaths, with: .none)
+      }
+    }
+    tableView?.endUpdates()
+  }
 }
 
 extension Table: ScrollProxyDelegate {

Use new diff utility