<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
  }
}
  // later to intelligently add/remove only the items that changed.
  public var itemKeys: [AnyHashable]?

  public init() {}

  public init(_ properties: [String : Any]) {
  weak public var tableViewDataSource: TableViewDataSource?
  weak public var eventTarget: Node?

    tableViewDataSource = properties["tableViewDataSource"] as? TableViewDataSource
    eventTarget = properties["eventTarget"] as? Node
    itemKeys = properties["itemKeys"] as? [AnyHashable]
  }

  public mutating func merge(_ other: TableProperties) {
  public var onEndReached: Selector?
  public var onEndReachedThreshold: CGFloat?

    merge(&tableViewDataSource, other.tableViewDataSource)
    merge(&eventTarget, other.eventTarget)
    merge(&itemKeys, other.itemKeys)
  }
}

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

public class Table: PropertyNode {
}

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 weak var owner: Node?
  public var context: Context?

  public var properties: TableProperties {
    didSet {
      if let oldItems = oldValue.items, let newItems = properties.items {
        precondition(newItems.count > 0, "Items must contain at least one section.")
        updateRows(old: oldItems, new: newItems)
      }
    }
  }
  public var children: [Node]?
  public var element: ElementData<TableProperties>
  public var cssNode: CSSNode?
    return view as? TableView
  }

  public required init(element: ElementData<TableProperties>, children: [Node]? = nil, owner: Node? = nil, context: Context? = nil) {
    self.element = element
    self.properties = element.properties
  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
  }

  public func build() -> View {
    tableView?.tableViewDelegate = properties.tableViewDelegate
    tableView?.tableViewDataSource = properties.tableViewDataSource
    tableView?.eventTarget = properties.eventTarget

    return view
  }
}

    return view
  }

  private func updateRows(old: [TableSection], new: [TableSection]) {
    tableView?.beginUpdates()

    let sectionResult = diff(old, new)
    for step in sectionResult {
      switch step {
      case .delete(let index):
        tableView?.deleteSections(IndexSet(integer: index), with: .none)
      case .insert(let index):
        tableView?.insertSections(IndexSet(integer: index), with: .none)
      case .update(let index):
        // Updates are handled below.
        break
      default:
        break
      }
    }

    var deletions = [IndexPath]()
    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> Merge pull request #164 from mcudich/table-component

Add callback for table component that notifies scrolling to end of list
<DFF> @@ -21,6 +21,9 @@ public struct TableProperties: Properties {
   // later to intelligently add/remove only the items that changed.
   public var itemKeys: [AnyHashable]?
 
+  public var onEndReached: Selector?
+  public var onEndReachedThreshold: CGFloat?
+
   public init() {}
 
   public init(_ properties: [String : Any]) {
@@ -30,6 +33,8 @@ public struct TableProperties: Properties {
     tableViewDataSource = properties["tableViewDataSource"] as? TableViewDataSource
     eventTarget = properties["eventTarget"] as? Node
     itemKeys = properties["itemKeys"] as? [AnyHashable]
+    onEndReached = properties.cast("onEndReached")
+    onEndReachedThreshold = properties.cast("onEndReachedThreshold")
   }
 
   public mutating func merge(_ other: TableProperties) {
@@ -39,11 +44,25 @@ public struct TableProperties: Properties {
     merge(&tableViewDataSource, other.tableViewDataSource)
     merge(&eventTarget, other.eventTarget)
     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.itemKeys == rhs.itemKeys && lhs.equals(otherProperties: rhs)
+  return lhs.tableViewDelegate === rhs.tableViewDelegate && lhs.tableViewDataSource === rhs.tableViewDataSource && lhs.eventTarget === rhs.eventTarget && lhs.itemKeys == rhs.itemKeys && lhs.onEndReached == rhs.onEndReached  && lhs.onEndReachedThreshold == rhs.onEndReachedThreshold && lhs.equals(otherProperties: rhs)
+}
+
+protocol ScrollProxyDelegate: class {
+  func scrollViewDidScroll(_ scrollView: UIScrollView)
+}
+
+public class ScrollProxy: NSObject {
+  weak var delegate: ScrollProxyDelegate?
+
+  func scrollViewDidScroll(_ scrollView: UIScrollView) {
+    delegate?.scrollViewDidScroll(scrollView)
+  }
 }
 
 public class Table: PropertyNode {
@@ -72,6 +91,15 @@ public class Table: PropertyNode {
     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
@@ -83,10 +111,29 @@ public class Table: PropertyNode {
   }
 
   public func build() -> View {
-    tableView?.tableViewDelegate = properties.tableViewDelegate
+    delegateProxy = DelegateProxy(target: properties.tableViewDelegate, interceptor: scrollProxy)
+    delegateProxy?.registerInterceptable(selector: #selector(UIScrollViewDelegate.scrollViewDidScroll(_:)))
+
+    tableView?.tableViewDelegate = delegateProxy as? TableViewDelegate
     tableView?.tableViewDataSource = properties.tableViewDataSource
     tableView?.eventTarget = properties.eventTarget
 
     return view
   }
 }
+
+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)
+  }
+}

Merge pull request #164 from mcudich/table-component