Swiftgram/Display/ListView.swift
2016-04-18 01:30:55 +03:00

1808 lines
75 KiB
Swift

import UIKit
import AsyncDisplayKit
import SwiftSignalKit
private let usePerformanceTracker = false
private let useDynamicTuning = false
public enum ListViewScrollPosition {
case Top
case Bottom
case Center
}
public struct ListViewDeleteAndInsertOptions: OptionSetType {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let AnimateInsertion = ListViewDeleteAndInsertOptions(rawValue: 1)
public static let AnimateAlpha = ListViewDeleteAndInsertOptions(rawValue: 2)
}
public struct ListViewVisibleRange: Equatable {
public let firstIndex: Int
public let lastIndex: Int
}
public func ==(lhs: ListViewVisibleRange, rhs: ListViewVisibleRange) -> Bool {
return lhs.firstIndex == rhs.firstIndex && lhs.lastIndex == rhs.lastIndex
}
private struct IndexRange {
let first: Int
let last: Int
func contains(index: Int) -> Bool {
return index >= first && index <= last
}
var empty: Bool {
return first > last
}
}
private struct OffsetRanges {
var offsets: [(IndexRange, CGFloat)] = []
mutating func append(other: OffsetRanges) {
self.offsets.appendContentsOf(other.offsets)
}
mutating func offset(indexRange: IndexRange, offset: CGFloat) {
self.offsets.append((indexRange, offset))
}
func offsetForIndex(index: Int) -> CGFloat {
var result: CGFloat = 0.0
for offset in self.offsets {
if offset.0.contains(index) {
result += offset.1
}
}
return result
}
}
private func binarySearch(inputArr: [Int], searchItem: Int) -> Int? {
var lowerIndex = 0;
var upperIndex = inputArr.count - 1
if lowerIndex > upperIndex {
return nil
}
while (true) {
let currentIndex = (lowerIndex + upperIndex) / 2
if (inputArr[currentIndex] == searchItem) {
return currentIndex
} else if (lowerIndex > upperIndex) {
return nil
} else {
if (inputArr[currentIndex] > searchItem) {
upperIndex = currentIndex - 1
} else {
lowerIndex = currentIndex + 1
}
}
}
}
private struct TransactionState {
let visibleSize: CGSize
let items: [ListViewItem]
}
private struct PendingNode {
let index: Int
let node: ListViewItemNode
let apply: () -> ()
let frame: CGRect
let apparentHeight: CGFloat
}
private enum ListViewStateNode {
case Node(index: Int, frame: CGRect, referenceNode: ListViewItemNode?)
case Placeholder(frame: CGRect)
var index: Int? {
switch self {
case .Node(let index, _, _):
return index
case .Placeholder(_):
return nil
}
}
var frame: CGRect {
get {
switch self {
case .Node(_, let frame, _):
return frame
case .Placeholder(let frame):
return frame
}
} set(value) {
switch self {
case let .Node(index, _, referenceNode):
self = .Node(index: index, frame: value, referenceNode: referenceNode)
case .Placeholder(_):
self = .Placeholder(frame: value)
}
}
}
}
private enum ListViewInsertionOffsetDirection {
case Up
case Down
}
private struct ListViewState {
let insets: UIEdgeInsets
let visibleSize: CGSize
let invisibleInset: CGFloat
var nodes: [ListViewStateNode]
func nodeInsertionPointAndIndex(itemIndex: Int) -> (CGPoint, Int) {
if self.nodes.count == 0 {
return (CGPoint(x: 0.0, y: self.insets.top), 0)
} else {
var index = 0
var lastNodeWithIndex = -1
for node in self.nodes {
if let nodeItemIndex = node.index {
if nodeItemIndex > itemIndex {
break
}
lastNodeWithIndex = index
}
index += 1
}
lastNodeWithIndex += 1
return (CGPoint(x: 0.0, y: lastNodeWithIndex == 0 ? self.nodes[0].frame.minY : self.nodes[lastNodeWithIndex - 1].frame.maxY), lastNodeWithIndex)
}
}
mutating func insertNode(itemIndex: Int, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> (), offsetDirection: ListViewInsertionOffsetDirection, animated: Bool, inout operations: [ListViewStateOperation]) {
let (insertionOrigin, insertionIndex) = self.nodeInsertionPointAndIndex(itemIndex)
let nodeOrigin: CGPoint
switch offsetDirection {
case .Up:
nodeOrigin = CGPoint(x: insertionOrigin.x, y: insertionOrigin.y - (animated ? 0.0 : layout.size.height))
case .Down:
nodeOrigin = insertionOrigin
}
let nodeFrame = CGRect(origin: nodeOrigin, size: CGSize(width: layout.size.width, height: animated ? 0.0 : layout.size.height))
operations.append(.InsertNode(index: insertionIndex, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply))
self.nodes.insert(.Node(index: node.index!, frame: nodeFrame, referenceNode: nil), atIndex: insertionIndex)
if !animated {
switch offsetDirection {
case .Up:
var i = insertionIndex - 1
while i >= 0 {
var frame = self.nodes[i].frame
frame.origin.y -= nodeFrame.size.height
self.nodes[i].frame = frame
i -= 1
}
case .Down:
var i = insertionIndex + 1
while i < self.nodes.count {
var frame = self.nodes[i].frame
frame.origin.y += nodeFrame.size.height
self.nodes[i].frame = frame
i += 1
}
}
}
}
mutating func removeNodeAtIndex(index: Int, animated: Bool, inout operations: [ListViewStateOperation]) {
let node = self.nodes[index]
if case let .Node(_, _, referenceNode) = node {
let nodeFrame = node.frame
self.nodes.removeAtIndex(index)
operations.append(.Remove(index: index))
if let referenceNode = referenceNode where animated {
self.nodes.insert(.Placeholder(frame: nodeFrame), atIndex: index)
operations.append(.InsertPlaceholder(index: index, referenceNode: referenceNode))
} else {
for i in index ..< self.nodes.count {
var frame = self.nodes[i].frame
frame.origin.y -= nodeFrame.size.height
self.nodes[i].frame = frame
}
}
} else {
assertionFailure()
}
}
}
private enum ListViewStateOperation {
case InsertNode(index: Int, offsetDirection: ListViewInsertionOffsetDirection, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> ())
case InsertPlaceholder(index: Int, referenceNode: ListViewItemNode)
case Remove(index: Int)
case Remap([Int: Int])
case UpdateLayout(index: Int, layout: ListViewItemNodeLayout, apply: () -> ())
}
private let infiniteScrollSize: CGFloat = 10000.0
private let insertionAnimationDuration: Double = 0.4
private final class ListViewBackingLayer: CALayer {
override func setNeedsLayout() {
}
override func layoutSublayers() {
}
}
private final class ListViewBackingView: UIView {
weak var target: ASDisplayNode?
override class func layerClass() -> AnyClass {
return ListViewBackingLayer.self
}
override func setNeedsLayout() {
}
override func layoutSubviews() {
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.target?.touchesBegan(touches, withEvent: event)
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.target?.touchesCancelled(touches, withEvent: event)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.target?.touchesMoved(touches, withEvent: event)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.target?.touchesEnded(touches, withEvent: event)
}
}
private final class ListViewTimerProxy: NSObject {
private let action: () -> ()
init(_ action: () -> ()) {
self.action = action
super.init()
}
@objc func timerEvent() {
self.action()
}
}
public final class ListView: ASDisplayNode, UIScrollViewDelegate {
private final let scroller: ListViewScroller
private final var visibleSize: CGSize = CGSize()
private final var insets = UIEdgeInsets()
private final var lastContentOffset: CGPoint = CGPoint()
private final var lastContentOffsetTimestamp: CFAbsoluteTime = 0.0
private final var ignoreScrollingEvents: Bool = false
private final var displayLink: CADisplayLink!
private final var needsAnimations = false
private final var invisibleInset: CGFloat = 500.0
public var preloadPages: Bool = true {
didSet {
if self.preloadPages != oldValue {
self.invisibleInset = self.preloadPages ? 500.0 : 20.0
self.enqueueUpdateVisibleItems()
}
}
}
private var touchesPosition = CGPoint()
private var isTracking = false
private final var transactionQueue: ListViewTransactionQueue
private final var transactionOffset: CGFloat = 0.0
private final var enqueuedUpdateVisibleItems = false
private final var createdItemNodes = 0
public final var synchronousNodes = false
public final var debugInfo = false
private final var items: [ListViewItem] = []
private final var itemNodes: [ListViewItemNode] = []
public final var visibleItemRangeChanged: ListViewVisibleRange? -> Void = { _ in }
public final var visibleItemRange: ListViewVisibleRange?
private final var animations: [ListViewAnimation] = []
private final var actionsForVSync: [() -> ()] = []
private final var inVSync = false
private let frictionSlider = UISlider()
private let springSlider = UISlider()
private let freeResistanceSlider = UISlider()
private let scrollingResistanceSlider = UISlider()
//let performanceTracker: FBAnimationPerformanceTracker
private var selectionTouchLocation: CGPoint?
private var selectionTouchDelayTimer: NSTimer?
private var highlightedItemIndex: Int?
public func reportDurationInMS(duration: Int, smallDropEvent: Double, largeDropEvent: Double) {
print("reportDurationInMS duration: \(duration), smallDropEvent: \(smallDropEvent), largeDropEvent: \(largeDropEvent)")
}
public func reportStackTrace(stack: String!, withSlide slide: String!) {
NSLog("reportStackTrace stack: \(stack)\n\nslide: \(slide)")
}
override public init() {
class DisplayLinkProxy: NSObject {
weak var target: ListView?
init(target: ListView) {
self.target = target
}
@objc func displayLinkEvent() {
self.target?.displayLinkEvent()
}
}
self.transactionQueue = ListViewTransactionQueue()
self.scroller = ListViewScroller()
/*var performanceTrackerConfig = FBAnimationPerformanceTracker.standardConfig()
performanceTrackerConfig.reportStackTraces = true
self.performanceTracker = FBAnimationPerformanceTracker(config: performanceTrackerConfig)*/
super.init(viewBlock: { Void -> UIView in
return ListViewBackingView()
}, didLoadBlock: nil)
(self.view as! ListViewBackingView).target = self
self.transactionQueue.transactionCompleted = { [weak self] in
if let strongSelf = self {
strongSelf.updateVisibleItemRange()
}
}
//self.performanceTracker.delegate = self
self.scroller.alwaysBounceVertical = true
self.scroller.contentSize = CGSize(width: 0.0, height: infiniteScrollSize * 2.0)
self.scroller.hidden = true
self.scroller.delegate = self
self.view.addSubview(self.scroller)
self.scroller.panGestureRecognizer.cancelsTouchesInView = false
self.view.addGestureRecognizer(self.scroller.panGestureRecognizer)
self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
self.displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
self.displayLink.paused = true
if useDynamicTuning {
self.frictionSlider.addTarget(self, action: #selector(self.frictionSliderChanged(_:)), forControlEvents: .ValueChanged)
self.springSlider.addTarget(self, action: #selector(self.springSliderChanged(_:)), forControlEvents: .ValueChanged)
self.freeResistanceSlider.addTarget(self, action: #selector(self.freeResistanceSliderChanged(_:)), forControlEvents: .ValueChanged)
self.scrollingResistanceSlider.addTarget(self, action: #selector(self.scrollingResistanceSliderChanged(_:)), forControlEvents: .ValueChanged)
self.frictionSlider.minimumValue = Float(testSpringFrictionLimits.0)
self.frictionSlider.maximumValue = Float(testSpringFrictionLimits.1)
self.frictionSlider.value = Float(testSpringFriction)
self.springSlider.minimumValue = Float(testSpringConstantLimits.0)
self.springSlider.maximumValue = Float(testSpringConstantLimits.1)
self.springSlider.value = Float(testSpringConstant)
self.freeResistanceSlider.minimumValue = Float(testSpringResistanceFreeLimits.0)
self.freeResistanceSlider.maximumValue = Float(testSpringResistanceFreeLimits.1)
self.freeResistanceSlider.value = Float(testSpringFreeResistance)
self.scrollingResistanceSlider.minimumValue = Float(testSpringResistanceScrollingLimits.0)
self.scrollingResistanceSlider.maximumValue = Float(testSpringResistanceScrollingLimits.1)
self.scrollingResistanceSlider.value = Float(testSpringScrollingResistance)
self.view.addSubview(self.frictionSlider)
self.view.addSubview(self.springSlider)
self.view.addSubview(self.freeResistanceSlider)
self.view.addSubview(self.scrollingResistanceSlider)
}
}
deinit {
self.pauseAnimations()
}
@objc func frictionSliderChanged(slider: UISlider) {
testSpringFriction = CGFloat(slider.value)
print("friction: \(testSpringFriction)")
}
@objc func springSliderChanged(slider: UISlider) {
testSpringConstant = CGFloat(slider.value)
print("spring: \(testSpringConstant)")
}
@objc func freeResistanceSliderChanged(slider: UISlider) {
testSpringFreeResistance = CGFloat(slider.value)
print("free resistance: \(testSpringFreeResistance)")
}
@objc func scrollingResistanceSliderChanged(slider: UISlider) {
testSpringScrollingResistance = CGFloat(slider.value)
print("free resistance: \(testSpringScrollingResistance)")
}
private func displayLinkEvent() {
self.updateAnimations()
}
private func setNeedsAnimations() {
if !self.needsAnimations {
self.needsAnimations = true
self.displayLink.paused = false
}
}
private func pauseAnimations() {
if self.needsAnimations {
self.needsAnimations = false
self.displayLink.paused = true
}
}
private func dispatchOnVSync(forceNext: Bool = false, action: () -> ()) {
Queue.mainQueue().dispatch {
if !forceNext && self.inVSync {
action()
} else {
self.actionsForVSync.append(action)
self.setNeedsAnimations()
}
}
}
public func scrollViewWillBeginDragging(scrollView: UIScrollView) {
self.lastContentOffsetTimestamp = 0.0
/*if usePerformanceTracker {
self.performanceTracker.start()
}*/
}
public func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if decelerate {
self.lastContentOffsetTimestamp = CACurrentMediaTime()
} else {
self.lastContentOffsetTimestamp = 0.0
/*if usePerformanceTracker {
self.performanceTracker.stop()
}*/
}
}
public func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
self.lastContentOffsetTimestamp = 0.0
/*if usePerformanceTracker {
self.performanceTracker.stop()
}*/
}
public func scrollViewDidScroll(scrollView: UIScrollView) {
if self.ignoreScrollingEvents || scroller !== self.scroller {
return
}
CATransaction.begin()
CATransaction.setDisableActions(true)
let deltaY = scrollView.contentOffset.y - self.lastContentOffset.y
self.lastContentOffset = scrollView.contentOffset
if self.lastContentOffsetTimestamp > DBL_EPSILON {
self.lastContentOffsetTimestamp = CACurrentMediaTime()
}
for itemNode in self.itemNodes {
let position = itemNode.position
itemNode.position = CGPoint(x: position.x, y: position.y - deltaY)
}
self.transactionOffset += -deltaY
self.enqueueUpdateVisibleItems()
self.updateScroller()
var useScrollDynamics = false
for itemNode in self.itemNodes {
if itemNode.wantsScrollDynamics {
useScrollDynamics = true
let anchor: CGFloat
if self.isTracking {
anchor = self.touchesPosition.y
} else if deltaY < 0.0 {
anchor = self.visibleSize.height
} else {
anchor = 0.0
}
var distance: CGFloat
let itemFrame = itemNode.apparentFrame
if anchor < itemFrame.origin.y {
distance = abs(itemFrame.origin.y - anchor)
} else if anchor > itemFrame.origin.y + itemFrame.size.height {
distance = abs(anchor - (itemFrame.origin.y + itemFrame.size.height))
} else {
distance = 0.0
}
let factor: CGFloat = max(0.08, abs(distance) / self.visibleSize.height)
let resistance: CGFloat = testSpringFreeResistance
itemNode.addScrollingOffset(deltaY * factor * resistance)
}
}
if useScrollDynamics {
self.setNeedsAnimations()
}
self.updateVisibleNodes()
CATransaction.commit()
}
private func snapToBounds() {
if self.itemNodes.count == 0 {
return
}
var overscroll: CGFloat = 0.0
if self.scroller.contentOffset.y < 0.0 {
overscroll = self.scroller.contentOffset.y
} else if self.scroller.contentOffset.y > max(0.0, self.scroller.contentSize.height - self.scroller.bounds.size.height) {
overscroll = self.scroller.contentOffset.y - max(0.0, (self.scroller.contentSize.height - self.scroller.bounds.size.height))
}
var completeHeight: CGFloat = 0.0
var topItemFound = false
var bottomItemFound = false
var topItemEdge: CGFloat = 0.0
var bottomItemEdge: CGFloat = 0.0
if itemNodes[0].index == 0 {
topItemFound = true
topItemEdge = itemNodes[0].apparentFrame.origin.y
}
if itemNodes[itemNodes.count - 1].index == self.items.count - 1 {
bottomItemFound = true
bottomItemEdge = itemNodes[itemNodes.count - 1].apparentFrame.maxY
}
if topItemFound && bottomItemFound {
for itemNode in self.itemNodes {
completeHeight += itemNode.apparentBounds.height
}
}
var offset: CGFloat = 0.0
if topItemFound && bottomItemFound {
let areaHeight = min(completeHeight, self.visibleSize.height - self.insets.bottom - self.insets.top)
if bottomItemEdge < self.insets.top + areaHeight - overscroll {
offset = self.insets.top + areaHeight - overscroll - bottomItemEdge
} else if topItemEdge > self.insets.top - overscroll {
//offset = topItemEdge - (self.insets.top - overscroll)
}
} else if topItemFound {
if topItemEdge > self.insets.top - overscroll {
//offset = topItemEdge - (self.insets.top - overscroll)
}
} else if bottomItemFound {
if bottomItemEdge < self.visibleSize.height - self.insets.bottom - overscroll {
offset = self.visibleSize.height - self.insets.bottom - overscroll - bottomItemEdge
}
}
if abs(offset) > CGFloat(FLT_EPSILON) {
for itemNode in self.itemNodes {
var position = itemNode.position
position.y += offset
itemNode.position = position
}
}
}
private func updateScroller() {
if itemNodes.count == 0 {
return
}
var completeHeight = self.insets.top + self.insets.bottom
var topItemFound = false
var bottomItemFound = false
var topItemEdge: CGFloat = 0.0
var bottomItemEdge: CGFloat = 0.0
if itemNodes[0].index == 0 {
topItemFound = true
topItemEdge = itemNodes[0].apparentFrame.origin.y
}
if itemNodes[itemNodes.count - 1].index == self.items.count - 1 {
bottomItemFound = true
bottomItemEdge = itemNodes[itemNodes.count - 1].apparentFrame.maxY
}
if topItemFound && bottomItemFound {
for itemNode in self.itemNodes {
completeHeight += itemNode.apparentBounds.height
}
}
topItemEdge -= self.insets.top
bottomItemEdge += self.insets.bottom
self.ignoreScrollingEvents = true
if topItemFound && bottomItemFound {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight)
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
self.scroller.contentOffset = self.lastContentOffset;
} else if topItemFound {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
self.scroller.contentOffset = self.lastContentOffset
} else if bottomItemFound {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge)
self.scroller.contentOffset = self.lastContentOffset
}
else
{
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize)
self.scroller.contentOffset = self.lastContentOffset
}
self.ignoreScrollingEvents = false
}
private func nodeForItem(item: ListViewItem, previousNode: ListViewItemNode?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, width: CGFloat, completion: (ListViewItemNode, ListViewItemNodeLayout, () -> Void) -> Void) {
if let previousNode = previousNode {
item.updateNode(previousNode, width: width, previousItem: previousItem, nextItem: nextItem, completion: { (layout, apply) in
previousNode.index = index
completion(previousNode, layout, apply)
})
} else {
let startTime = CACurrentMediaTime()
item.nodeConfiguredForWidth(width, previousItem: previousItem, nextItem: nextItem, completion: { itemNode, apply in
itemNode.index = index
if self.debugInfo {
print("[ListView] nodeConfiguredForWidth \((CACurrentMediaTime() - startTime) * 1000.0) ms")
}
completion(itemNode, ListViewItemNodeLayout(contentSize: itemNode.contentSize, insets: itemNode.insets), apply)
})
}
}
private func currentState() -> ListViewState {
var nodes: [ListViewStateNode] = []
nodes.reserveCapacity(self.itemNodes.count)
for node in self.itemNodes {
if let index = node.index {
nodes.append(.Node(index: index, frame: node.apparentFrame, referenceNode: node))
} else {
nodes.append(.Placeholder(frame: node.apparentFrame))
}
}
return ListViewState(insets: self.insets, visibleSize: self.visibleSize, invisibleInset: self.invisibleInset, nodes: nodes)
}
public func deleteAndInsertItems(deleteIndices: [Int], insertIndicesAndItems: [(Int, ListViewItem, Int?)], offsetTopInsertedItems: Bool, options: ListViewDeleteAndInsertOptions, completion: Void -> Void = {}) {
if deleteIndices.count == 0 && insertIndicesAndItems.count == 0 {
completion()
return
}
self.transactionQueue.addTransaction({ [weak self] transactionCompletion in
if let strongSelf = self {
strongSelf.transactionOffset = 0.0
strongSelf.deleteAndInsertItemsTransaction(deleteIndices, insertIndicesAndItems: insertIndicesAndItems, offsetTopInsertedItems: offsetTopInsertedItems, options: options, completion: {
completion()
transactionCompletion()
})
}
})
}
private func deleteAndInsertItemsTransaction(deleteIndices: [Int], insertIndicesAndItems: [(Int, ListViewItem, Int?)], offsetTopInsertedItems: Bool, options: ListViewDeleteAndInsertOptions, completion: Void -> Void) {
var state = self.currentState()
let sortedDeleteIndices = deleteIndices.sort()
for index in sortedDeleteIndices.reverse() {
self.items.removeAtIndex(index)
}
let sortedIndicesAndItems = insertIndicesAndItems.sort { $0.0 < $1.0 }
if self.items.count == 0 {
if sortedIndicesAndItems[0].0 != 0 {
fatalError("deleteAndInsertItems: invalid insert into empty list")
}
}
var previousNodes: [Int: ListViewItemNode] = [:]
for (index, item, previousIndex) in sortedIndicesAndItems {
self.items.insert(item, atIndex: index)
if let previousIndex = previousIndex {
for itemNode in self.itemNodes {
if itemNode.index == previousIndex {
previousNodes[index] = itemNode
}
}
}
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
var operations: [ListViewStateOperation] = []
let deleteIndexSet = Set(deleteIndices)
var insertedIndexSet = Set<Int>()
var moveMapping: [Int: Int] = [:]
for (index, _, previousIndex) in sortedIndicesAndItems {
insertedIndexSet.insert(index)
if let previousIndex = previousIndex {
moveMapping[previousIndex] = index
}
}
let animated = options.contains(.AnimateInsertion)
var remapDeletion: [Int: Int] = [:]
var updateAdjacentItemsIndices = Set<Int>()
var i = 0
while i < state.nodes.count {
if let index = state.nodes[i].index {
var indexOffset = 0
for deleteIndex in sortedDeleteIndices {
if deleteIndex < index {
indexOffset += 1
} else {
break
}
}
if deleteIndexSet.contains(index) {
state.removeNodeAtIndex(i, animated: animated, operations: &operations)
} else {
let updatedIndex = index - indexOffset
remapDeletion[index] = updatedIndex
if deleteIndexSet.contains(index - 1) || deleteIndexSet.contains(index + 1) {
updateAdjacentItemsIndices.insert(updatedIndex)
}
switch state.nodes[i] {
case let .Node(_, frame, referenceNode):
state.nodes[i] = .Node(index: updatedIndex, frame: frame, referenceNode: referenceNode)
case .Placeholder:
break
}
i += 1
}
} else {
i += 1
}
}
if !remapDeletion.isEmpty {
operations.append(.Remap(remapDeletion))
}
var remapInsertion: [Int: Int] = [:]
for i in 0 ..< state.nodes.count {
if let index = state.nodes[i].index {
var indexOffset = 0
for (insertIndex, _, _) in sortedIndicesAndItems {
if insertIndex <= index + indexOffset {
indexOffset += 1
}
}
if indexOffset != 0 {
let updatedIndex = index + indexOffset
remapInsertion[index] = updatedIndex
switch state.nodes[i] {
case let .Node(_, frame, referenceNode):
state.nodes[i] = .Node(index: updatedIndex, frame: frame, referenceNode: referenceNode)
case .Placeholder:
break
}
}
}
}
for node in state.nodes {
if let index = node.index {
if insertedIndexSet.contains(index - 1) || insertedIndexSet.contains(index + 1) {
updateAdjacentItemsIndices.insert(index)
}
}
}
if !remapInsertion.isEmpty {
operations.append(.Remap(remapInsertion))
}
let startTime = CACurrentMediaTime()
self.fillMissingNodes(animated, offsetTopInsertedItems: offsetTopInsertedItems, animatedInsertIndices: animated ? insertedIndexSet : Set<Int>(), state: state, previousNodes: previousNodes, operations: operations, completion: { updatedState, operations in
var fixedUpdateAdjacentItemsIndices = updateAdjacentItemsIndices
let maxIndex = updatedState.nodes.count - 1
for nodeIndex in updateAdjacentItemsIndices {
if nodeIndex < 0 || nodeIndex > maxIndex {
fixedUpdateAdjacentItemsIndices.remove(nodeIndex)
}
}
if self.debugInfo {
print("fillMissingNodes completion \((CACurrentMediaTime() - startTime) * 1000.0) ms")
}
self.updateAdjacent(animated, state: updatedState, updateAdjacentItemsIndices: fixedUpdateAdjacentItemsIndices, operations: operations, completion: { operations in
if self.debugInfo {
print("updateAdjacent completion \((CACurrentMediaTime() - startTime) * 1000.0) ms")
}
let next = {
self.replayOperations(animated, operations: operations, completion: completion)
}
self.dispatchOnVSync {
next()
}
})
})
})
}
private func updateAdjacent(animated: Bool, state: ListViewState, updateAdjacentItemsIndices: Set<Int>, operations: [ListViewStateOperation], completion: [ListViewStateOperation] -> Void) {
if updateAdjacentItemsIndices.isEmpty {
completion(operations)
} else {
var updatedUpdateAdjacentItemsIndices = updateAdjacentItemsIndices
let nodeIndex = updateAdjacentItemsIndices.first!
updatedUpdateAdjacentItemsIndices.remove(nodeIndex)
var actualIndex = nodeIndex
/*for node in state.nodes {
if case let .Node(index, _, _) = node where index == nodeIndex {
break
}
actualIndex += 1
}*/
var continueWithoutNode = true
if actualIndex < state.nodes.count {
if case let .Node(index, _, referenceNode) = state.nodes[actualIndex] {
if let referenceNode = referenceNode {
continueWithoutNode = false
self.items[index].updateNode(referenceNode, width: state.visibleSize.width, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: index == self.items.count - 1 ? nil : self.items[index + 1], completion: { layout, apply in
var updatedState = state
var updatedOperations = operations
for i in nodeIndex + 1 ..< updatedState.nodes.count {
let frame = updatedState.nodes[i].frame
updatedState.nodes[i].frame = frame.offsetBy(dx: 0.0, dy: frame.size.height)
updatedOperations.append(.UpdateLayout(index: nodeIndex, layout: layout, apply: apply))
}
self.updateAdjacent(animated, state: updatedState, updateAdjacentItemsIndices: updatedUpdateAdjacentItemsIndices, operations: updatedOperations, completion: completion)
})
}
}
}
if continueWithoutNode {
updateAdjacent(animated, state: state, updateAdjacentItemsIndices: updatedUpdateAdjacentItemsIndices, operations: operations, completion: completion)
}
}
}
private func fillMissingNodes(animated: Bool, offsetTopInsertedItems: Bool, animatedInsertIndices: Set<Int>, state: ListViewState, previousNodes: [Int: ListViewItemNode], operations: [ListViewStateOperation], completion: (ListViewState, [ListViewStateOperation]) -> Void) {
if self.items.count == 0 {
completion(state, operations)
} else {
var insertionItemIndexAndDirection: (Int, ListViewInsertionOffsetDirection)?
if state.nodes.count == 0 {
insertionItemIndexAndDirection = (0, .Down)
} else {
var previousIndex: Int?
for node in state.nodes {
if let index = node.index {
if let previousIndex = previousIndex {
if previousIndex + 1 != index {
if state.nodeInsertionPointAndIndex(index - 1).0.y < state.insets.top {
insertionItemIndexAndDirection = (index - 1, .Up)
} else {
insertionItemIndexAndDirection = (previousIndex + 1, .Down)
}
break
}
} else if index != 0 {
let insertionPoint = state.nodeInsertionPointAndIndex(index - 1).0
if insertionPoint.y >= -state.invisibleInset {
if !offsetTopInsertedItems || insertionPoint.y < state.insets.top {
insertionItemIndexAndDirection = (index - 1, .Up)
} else {
insertionItemIndexAndDirection = (0, .Down)
}
break
}
}
previousIndex = index
}
}
if let previousIndex = previousIndex where insertionItemIndexAndDirection == nil && previousIndex != self.items.count - 1 {
let insertionPoint = state.nodeInsertionPointAndIndex(previousIndex + 1).0
if insertionPoint.y < state.visibleSize.height + state.invisibleInset {
insertionItemIndexAndDirection = (previousIndex + 1, .Down)
}
}
}
if let insertionItemIndexAndDirection = insertionItemIndexAndDirection {
let index = insertionItemIndexAndDirection.0
self.nodeForItem(self.items[index], previousNode: previousNodes[index], index: index, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: self.items.count == index + 1 ? nil : self.items[index + 1], width: state.visibleSize.width, completion: { (node, layout, apply) in
var updatedState = state
var updatedOperations = operations
updatedState.insertNode(index, node: node, layout: layout, apply: apply, offsetDirection: insertionItemIndexAndDirection.1, animated: animated && animatedInsertIndices.contains(index), operations: &updatedOperations)
self.fillMissingNodes(animated, offsetTopInsertedItems: offsetTopInsertedItems, animatedInsertIndices: animatedInsertIndices, state: updatedState, previousNodes: previousNodes, operations: updatedOperations, completion: completion)
})
} else {
completion(state, operations)
}
}
}
private func referencePointForInsertionAtIndex(nodeIndex: Int) -> CGPoint {
var index = 0
for itemNode in self.itemNodes {
if index == nodeIndex {
return itemNode.apparentFrame.origin
}
index += 1
}
if self.itemNodes.count == 0 {
return CGPoint(x: 0.0, y: self.insets.top)
} else {
return CGPoint(x: 0.0, y: self.itemNodes[self.itemNodes.count - 1].apparentFrame.maxY)
}
}
private func updateVisibleNodes() {
/*let visibleRect = CGRect(origin: CGPoint(x: 0.0, y: -10.0), size: CGSize(width: self.visibleSize.width, height: self.visibleSize.height + 20))
for itemNode in self.itemNodes {
if CGRectIntersectsRect(itemNode.apparentFrame, visibleRect) {
if useDynamicTuning {
self.insertSubnode(itemNode, atIndex: 0)
} else {
self.addSubnode(itemNode)
}
} else if itemNode.supernode != nil {
itemNode.removeFromSupernode()
}
}*/
}
private func insertNodeAtIndex(animated: Bool, previousFrame: CGRect?, nodeIndex: Int, offsetDirection: ListViewInsertionOffsetDirection, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> (), timestamp: Double) {
let insertionOrigin = self.referencePointForInsertionAtIndex(nodeIndex)
let nodeOrigin: CGPoint
switch offsetDirection {
case .Up:
nodeOrigin = CGPoint(x: insertionOrigin.x, y: insertionOrigin.y - (animated ? 0.0 : layout.size.height))
case .Down:
nodeOrigin = insertionOrigin
}
let nodeFrame = CGRect(origin: nodeOrigin, size: CGSize(width: layout.size.width, height: layout.size.height))
let previousApparentHeight = node.apparentHeight
let previousInsets = node.insets
node.contentSize = layout.contentSize
node.insets = layout.insets
node.apparentHeight = animated ? 0.0 : layout.size.height
node.frame = nodeFrame
apply()
self.itemNodes.insert(node, atIndex: nodeIndex)
if useDynamicTuning {
self.insertSubnode(node, atIndex: 0)
} else {
//self.addSubnode(node)
}
if previousFrame == nil {
node.setupGestures()
}
var offsetHeight = node.apparentHeight
var takenAnimation = false
if let _ = previousFrame where animated && node.index != nil && nodeIndex != self.itemNodes.count - 1 {
let nextNode = self.itemNodes[nodeIndex + 1]
if nextNode.index == nil {
let nextHeight = nextNode.apparentHeight
if abs(nextHeight - previousApparentHeight) < CGFloat(FLT_EPSILON) {
if let animation = node.animationForKey("apparentHeight") where abs(animation.to as! CGFloat - layout.size.height) < CGFloat(FLT_EPSILON) {
node.apparentHeight = previousApparentHeight
offsetHeight = 0.0
var offsetPosition = nextNode.position
offsetPosition.y += nextHeight
nextNode.position = offsetPosition
nextNode.apparentHeight = 0.0
nextNode.removeApparentHeightAnimation()
takenAnimation = true
}
}
}
}
if node.index == nil {
node.addApparentHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
} else if animated {
if !takenAnimation {
node.addApparentHeightAnimation(nodeFrame.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
if let previousFrame = previousFrame {
node.transitionOffset += nodeFrame.origin.y - previousFrame.origin.y
node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
if previousInsets != layout.insets {
node.insets = previousInsets
node.addInsetsAnimationToValue(layout.insets, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
}
} else {
node.animateInsertion(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor())
}
}
}
if node.apparentHeight > CGFloat(FLT_EPSILON) {
switch offsetDirection {
case .Up:
var i = nodeIndex - 1
while i >= 0 {
var frame = self.itemNodes[i].frame
frame.origin.y -= offsetHeight
self.itemNodes[i].frame = frame
i -= 1
}
case .Down:
var i = nodeIndex + 1
while i < self.itemNodes.count {
var frame = self.itemNodes[i].frame
frame.origin.y += offsetHeight
self.itemNodes[i].frame = frame
i += 1
}
}
}
}
private func replayOperations(animated: Bool, operations: [ListViewStateOperation], completion: () -> Void) {
let timestamp = CACurrentMediaTime()
var previousApparentFrames: [(ListViewItemNode, CGRect)] = []
for itemNode in self.itemNodes {
previousApparentFrames.append((itemNode, itemNode.apparentFrame))
}
var insertedNodes: [ASDisplayNode] = []
for operation in operations {
switch operation {
case let .InsertNode(index, offsetDirection, node, layout, apply):
var previousFrame: CGRect?
for (previousNode, frame) in previousApparentFrames {
if previousNode === node {
previousFrame = frame
break
}
}
self.insertNodeAtIndex(animated, previousFrame: previousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp)
insertedNodes.append(node)
case let .InsertPlaceholder(index, referenceNode):
var height: CGFloat?
for (node, previousFrame) in previousApparentFrames {
if node === referenceNode {
height = previousFrame.size.height
break
}
}
if let height = height {
self.insertNodeAtIndex(false, previousFrame: nil, nodeIndex: index, offsetDirection: .Down, node: ListViewItemNode(layerBacked: true), layout: ListViewItemNodeLayout(contentSize: CGSize(width: self.visibleSize.width, height: height), insets: UIEdgeInsets()), apply: { }, timestamp: timestamp)
} else {
assertionFailure()
}
case let .Remap(mapping):
for node in self.itemNodes {
if let index = node.index {
if let mapped = mapping[index] {
node.index = mapped
}
}
}
case let .Remove(index):
let height = self.itemNodes[index].apparentHeight
if index != self.itemNodes.count - 1 {
for i in index + 1 ..< self.itemNodes.count {
var frame = self.itemNodes[i].frame
frame.origin.y -= height
self.itemNodes[i].frame = frame
}
}
self.removeItemNodeAtIndex(index)
case let .UpdateLayout(index, layout, apply):
let node = self.itemNodes[index]
let previousApparentHeight = node.apparentHeight
let previousInsets = node.insets
node.contentSize = layout.contentSize
node.insets = layout.insets
apply()
let updatedApparentHeight = node.bounds.size.height
let updatedInsets = node.insets
var offsetRanges = OffsetRanges()
if animated {
if updatedInsets != previousInsets {
node.insets = previousInsets
node.addInsetsAnimationToValue(updatedInsets, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
}
if abs(updatedApparentHeight - previousApparentHeight) > CGFloat(FLT_EPSILON) {
node.apparentHeight = previousApparentHeight
node.addApparentHeightAnimation(updatedApparentHeight, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
}
} else {
node.apparentHeight = updatedApparentHeight
let apparentHeightDelta = updatedApparentHeight - previousApparentHeight
if apparentHeightDelta != 0.0 {
var apparentFrame = node.apparentFrame
apparentFrame.origin.y += offsetRanges.offsetForIndex(index)
if apparentFrame.maxY < self.insets.top {
offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta)
} else {
offsetRanges.offset(IndexRange(first: index + 1, last: Int.max), offset: apparentHeightDelta)
}
}
}
var index = 0
for itemNode in self.itemNodes {
let offset = offsetRanges.offsetForIndex(index)
if offset != 0.0 {
var position = itemNode.position
position.y += offset
itemNode.position = position
}
index += 1
}
}
}
self.insertNodesInBatches(insertedNodes, completion: {
self.debugCheckMonotonity()
self.removeInvisibleNodes()
self.updateAccessoryNodes(animated, currentTimestamp: timestamp)
self.snapToBounds()
self.updateVisibleNodes()
if animated {
self.setNeedsAnimations()
}
completion()
})
/*let delta = CACurrentMediaTime() - timestamp
if delta > 1.0 / 60.0 {
print("replayOperations \(delta * 1000.0) ms \(nodeCreationDurations)")
}*/
}
private func insertNodesInBatches(nodes: [ASDisplayNode], completion: () -> Void) {
if nodes.count == 0 {
completion()
} else {
for node in nodes {
self.addSubnode(node)
}
completion()
/*self.dispatchOnVSync(true, action: {
self.addSubnode(nodes[0])
var updatedNodes = nodes
updatedNodes.removeAtIndex(0)
self.insertNodesInBatches(updatedNodes, completion: completion)
})*/
}
}
private func debugCheckMonotonity() {
if self.debugInfo {
var previousMaxY: CGFloat?
for node in self.itemNodes {
if let previousMaxY = previousMaxY where abs(previousMaxY - node.apparentFrame.minY) > CGFloat(FLT_EPSILON) {
print("monotonity violated")
break
}
previousMaxY = node.apparentFrame.maxY
}
}
}
private func removeItemNodeAtIndex(index: Int) {
let node = self.itemNodes[index]
self.itemNodes.removeAtIndex(index)
node.removeFromSupernode()
node.accessoryItemNode?.removeFromSupernode()
node.accessoryItemNode = nil
node.accessoryHeaderItemNode?.removeFromSupernode()
node.accessoryHeaderItemNode = nil
}
private func updateAccessoryNodes(animated: Bool, currentTimestamp: Double) {
var index = -1
let count = self.itemNodes.count
for itemNode in self.itemNodes {
index += 1
if let itemNodeIndex = itemNode.index {
if let accessoryItem = self.items[itemNodeIndex].accessoryItem {
let previousItem: ListViewItem? = itemNodeIndex == 0 ? nil : self.items[itemNodeIndex - 1]
let previousAccessoryItem = previousItem?.accessoryItem
if (previousAccessoryItem == nil || !previousAccessoryItem!.isEqualToItem(accessoryItem)) {
if itemNode.accessoryItemNode == nil {
var didStealAccessoryNode = false
if index != count - 1 {
for i in index + 1 ..< count {
let nextItemNode = self.itemNodes[i]
if let nextItemNodeIndex = nextItemNode.index {
let nextItem = self.items[nextItemNodeIndex]
if let nextAccessoryItem = nextItem.accessoryItem where nextAccessoryItem.isEqualToItem(accessoryItem) {
if let nextAccessoryItemNode = nextItemNode.accessoryItemNode {
didStealAccessoryNode = true
var previousAccessoryItemNodeOrigin = nextAccessoryItemNode.frame.origin
let previousParentOrigin = nextItemNode.frame.origin
previousAccessoryItemNodeOrigin.x += previousParentOrigin.x
previousAccessoryItemNodeOrigin.y += previousParentOrigin.y
previousAccessoryItemNodeOrigin.y -= nextItemNode.bounds.origin.y
previousAccessoryItemNodeOrigin.y -= nextAccessoryItemNode.transitionOffset.y
nextAccessoryItemNode.transitionOffset = CGPoint()
nextAccessoryItemNode.removeFromSupernode()
itemNode.addSubnode(nextAccessoryItemNode)
itemNode.accessoryItemNode = nextAccessoryItemNode
self.itemNodes[i].accessoryItemNode = nil
var updatedAccessoryItemNodeOrigin = nextAccessoryItemNode.frame.origin
let updatedParentOrigin = itemNode.frame.origin
updatedAccessoryItemNodeOrigin.x += updatedParentOrigin.x
updatedAccessoryItemNodeOrigin.y += updatedParentOrigin.y
updatedAccessoryItemNodeOrigin.y -= itemNode.bounds.origin.y
nextAccessoryItemNode.animateTransitionOffset(CGPoint(x: 0.0, y: updatedAccessoryItemNodeOrigin.y - previousAccessoryItemNodeOrigin.y), beginAt: currentTimestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: listViewAnimationCurveSystem)
}
} else {
break
}
}
}
}
if !didStealAccessoryNode {
let accessoryNode = accessoryItem.node()
itemNode.addSubnode(accessoryNode)
itemNode.accessoryItemNode = accessoryNode
}
}
} else {
itemNode.accessoryItemNode?.removeFromSupernode()
itemNode.accessoryItemNode = nil
}
}
}
}
}
private func enqueueUpdateVisibleItems() {
if !self.enqueuedUpdateVisibleItems {
self.enqueuedUpdateVisibleItems = true
self.transactionQueue.addTransaction({ [weak self] completion in
if let strongSelf = self {
strongSelf.transactionOffset = 0.0
strongSelf.updateVisibleItemsTransaction({
var repeatUpdate = false
if let strongSelf = self {
repeatUpdate = abs(strongSelf.transactionOffset) > 0.00001
strongSelf.transactionOffset = 0.0
strongSelf.enqueuedUpdateVisibleItems = false
}
//dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(2.0 * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), {
completion()
if repeatUpdate {
strongSelf.enqueueUpdateVisibleItems()
}
//})
})
}
})
}
}
private func updateVisibleItemsTransaction(completion: Void -> Void) {
var i = 0
while i < self.itemNodes.count {
let node = self.itemNodes[i]
if node.index == nil && node.apparentHeight <= CGFloat(FLT_EPSILON) {
self.removeItemNodeAtIndex(i)
} else {
i += 1
}
}
self.fillMissingNodes(false, offsetTopInsertedItems: false, animatedInsertIndices: [], state: self.currentState(), previousNodes: [:], operations: []) { _, operations in
self.dispatchOnVSync {
self.replayOperations(false, operations: operations, completion: completion)
}
}
}
private func removeInvisibleNodes() {
var i = 0
var visibleItemNodeHeight: CGFloat = 0.0
while i < self.itemNodes.count {
visibleItemNodeHeight += self.itemNodes[i].apparentBounds.height
i += 1
}
if visibleItemNodeHeight > (self.visibleSize.height + self.invisibleInset + self.invisibleInset) {
i = self.itemNodes.count - 1
while i >= 0 {
let itemNode = self.itemNodes[i]
let apparentFrame = itemNode.apparentFrame
if apparentFrame.maxY < -self.invisibleInset || apparentFrame.origin.y > self.visibleSize.height + self.invisibleInset {
self.removeItemNodeAtIndex(i)
}
i -= 1
}
}
}
private func updateVisibleItemRange(force: Bool = false) {
let currentRange: ListViewVisibleRange?
if self.itemNodes.count != 0 {
var firstIndex: Int?
var lastIndex: Int?
var i = 0
while i < self.itemNodes.count {
if let index = self.itemNodes[i].index {
firstIndex = index
break
}
i += 1
}
i = self.itemNodes.count - 1
while i >= 0 {
if let index = self.itemNodes[i].index {
lastIndex = index
break
}
i -= 1
}
if let firstIndex = firstIndex, lastIndex = lastIndex {
currentRange = ListViewVisibleRange(firstIndex: firstIndex, lastIndex: lastIndex)
} else {
currentRange = nil
}
} else {
currentRange = nil
}
if currentRange != self.visibleItemRange || force {
self.visibleItemRange = currentRange
self.visibleItemRangeChanged(currentRange)
}
}
public func updateSizeAndInsets(size: CGSize, insets: UIEdgeInsets, duration: Double = 0.0, options: UIViewAnimationOptions = UIViewAnimationOptions()) {
self.transactionQueue.addTransaction({ [weak self] completion in
if let strongSelf = self {
strongSelf.transactionOffset = 0.0
strongSelf.updateSizeAndInsetsTransaction(size, insets: insets, duration: duration, options: options, completion: { [weak self] in
if let strongSelf = self {
strongSelf.transactionOffset = 0.0
strongSelf.updateVisibleItemsTransaction(completion)
}
})
}
})
if useDynamicTuning {
self.frictionSlider.frame = CGRect(x: 10.0, y: size.height - insets.bottom - 10.0 - self.frictionSlider.bounds.height, width: size.width - 20.0, height: self.frictionSlider.bounds.height)
self.springSlider.frame = CGRect(x: 10.0, y: self.frictionSlider.frame.minY - self.springSlider.bounds.height, width: size.width - 20.0, height: self.springSlider.bounds.height)
self.freeResistanceSlider.frame = CGRect(x: 10.0, y: self.springSlider.frame.minY - self.freeResistanceSlider.bounds.height, width: size.width - 20.0, height: self.freeResistanceSlider.bounds.height)
self.scrollingResistanceSlider.frame = CGRect(x: 10.0, y: self.freeResistanceSlider.frame.minY - self.scrollingResistanceSlider.bounds.height, width: size.width - 20.0, height: self.scrollingResistanceSlider.bounds.height)
}
}
private func updateSizeAndInsetsTransaction(size: CGSize, insets: UIEdgeInsets, duration: Double, options: UIViewAnimationOptions, completion: Void -> Void) {
if CGSizeEqualToSize(size, self.visibleSize) && UIEdgeInsetsEqualToEdgeInsets(self.insets, insets) {
completion()
} else {
if abs(size.width - self.visibleSize.width) > CGFloat(FLT_EPSILON) {
let itemNodes = self.itemNodes
for itemNode in itemNodes {
itemNode.removeAllAnimations()
itemNode.transitionOffset = 0.0
if let index = itemNode.index {
itemNode.layoutForWidth(size.width, item: self.items[index], previousItem: index == 0 ? nil : self.items[index - 1], nextItem: index == self.items.count - 1 ? nil : self.items[index + 1])
}
itemNode.apparentHeight = itemNode.bounds.height
}
if itemNodes.count != 0 {
for i in 0 ..< itemNodes.count - 1 {
var nextFrame = itemNodes[i + 1].frame
nextFrame.origin.y = itemNodes[i].apparentFrame.maxY
itemNodes[i + 1].frame = nextFrame
}
}
}
var offsetFix = insets.top - self.insets.top
self.visibleSize = size
self.insets = insets
var completeOffset = offsetFix
for itemNode in self.itemNodes {
let position = itemNode.position
itemNode.position = CGPoint(x: position.x, y: position.y + offsetFix)
}
let completeDeltaHeight = offsetFix
offsetFix = 0.0
if Double(completeDeltaHeight) < DBL_EPSILON && self.itemNodes.count != 0 {
let firstItemNode = self.itemNodes[0]
let lastItemNode = self.itemNodes[self.itemNodes.count - 1]
if lastItemNode.index == self.items.count - 1 {
if firstItemNode.index == 0 {
let topGap = firstItemNode.apparentFrame.origin.y - self.insets.top
let bottomGap = self.visibleSize.height - lastItemNode.apparentFrame.maxY - self.insets.bottom
if Double(bottomGap) > DBL_EPSILON {
offsetFix = -bottomGap
if topGap + bottomGap > 0.0 {
offsetFix = topGap
}
let absOffsetFix = abs(offsetFix)
let absCompleteDeltaHeight = abs(completeDeltaHeight)
offsetFix = min(absOffsetFix, absCompleteDeltaHeight) * (offsetFix < 0 ? -1.0 : 1.0)
}
} else {
offsetFix = completeDeltaHeight
}
}
}
if Double(abs(offsetFix)) > DBL_EPSILON {
completeOffset -= offsetFix
for itemNode in self.itemNodes {
let position = itemNode.position
itemNode.position = CGPoint(x: position.x, y: position.y - offsetFix)
}
}
self.snapToBounds()
self.ignoreScrollingEvents = true
self.scroller.frame = CGRect(origin: CGPoint(), size: size)
self.scroller.contentSize = CGSizeMake(size.width, infiniteScrollSize * 2.0)
self.lastContentOffset = CGPointMake(0.0, infiniteScrollSize)
self.scroller.contentOffset = self.lastContentOffset
self.updateScroller()
self.updateVisibleItemRange()
let completion = { [weak self] (_: Bool) -> Void in
if let strongSelf = self {
strongSelf.updateVisibleItemsTransaction(completion)
strongSelf.ignoreScrollingEvents = false
}
}
if duration > DBL_EPSILON {
let animation: CABasicAnimation
if (options.rawValue & UInt(7 << 16)) != 0 {
let springAnimation = CASpringAnimation(keyPath: "sublayerTransform")
springAnimation.mass = 3.0
springAnimation.stiffness = 1000.0
springAnimation.damping = 500.0
springAnimation.initialVelocity = 0.0
springAnimation.duration = duration * UIView.animationDurationFactor()
springAnimation.fromValue = NSValue(CATransform3D: CATransform3DMakeTranslation(0.0, -completeOffset, 0.0))
springAnimation.toValue = NSValue(CATransform3D: CATransform3DIdentity)
springAnimation.removedOnCompletion = true
animation = springAnimation
} else {
let basicAnimation = CABasicAnimation(keyPath: "sublayerTransform")
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
basicAnimation.duration = duration * UIView.animationDurationFactor()
basicAnimation.fromValue = NSValue(CATransform3D: CATransform3DMakeTranslation(0.0, -completeOffset, 0.0))
basicAnimation.toValue = NSValue(CATransform3D: CATransform3DIdentity)
basicAnimation.removedOnCompletion = true
animation = basicAnimation
}
animation.completion = completion
self.layer.addAnimation(animation, forKey: "sublayerTransform")
} else {
completion(true)
}
}
}
private func updateAnimations() {
self.inVSync = true
let actionsForVSync = self.actionsForVSync
self.actionsForVSync.removeAll()
for action in actionsForVSync {
action()
}
self.inVSync = false
let timestamp: Double = CACurrentMediaTime()
var continueAnimations = false
if !self.actionsForVSync.isEmpty {
continueAnimations = true
}
var i = 0
var animationCount = self.animations.count
while i < animationCount {
let animation = self.animations[i]
animation.applyAt(timestamp)
if animation.completeAt(timestamp) {
animations.removeAtIndex(i)
animationCount -= 1
i -= 1
} else {
continueAnimations = true
}
i += 1
}
var offsetRanges = OffsetRanges()
var requestUpdateVisibleItems = false
var index = 0
while index < self.itemNodes.count {
let itemNode = self.itemNodes[index]
let previousApparentHeight = itemNode.apparentHeight
if itemNode.animate(timestamp) {
continueAnimations = true
}
let updatedApparentHeight = itemNode.apparentHeight
let apparentHeightDelta = updatedApparentHeight - previousApparentHeight
if abs(apparentHeightDelta) > CGFloat(FLT_EPSILON) {
if itemNode.apparentFrame.maxY < self.insets.top + CGFloat(FLT_EPSILON) {
offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta)
} else {
offsetRanges.offset(IndexRange(first: index + 1, last: Int.max), offset: apparentHeightDelta)
}
}
if itemNode.index == nil && updatedApparentHeight <= CGFloat(FLT_EPSILON) {
requestUpdateVisibleItems = true
}
index += 1
}
if !offsetRanges.offsets.isEmpty {
requestUpdateVisibleItems = true
var index = 0
for itemNode in self.itemNodes {
let offset = offsetRanges.offsetForIndex(index)
if offset != 0.0 {
var position = itemNode.position
position.y += offset
itemNode.position = position
}
index += 1
}
self.snapToBounds()
}
self.debugCheckMonotonity()
if !continueAnimations {
self.pauseAnimations()
}
if requestUpdateVisibleItems {
self.updateVisibleNodes()
self.enqueueUpdateVisibleItems()
}
}
override public func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.isTracking = true
self.touchesPosition = (touches.first!).locationInView(self.view)
self.selectionTouchLocation = self.touchesPosition
self.selectionTouchDelayTimer?.invalidate()
let timer = NSTimer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in
if let strongSelf = self where strongSelf.selectionTouchLocation != nil {
strongSelf.clearHighlightAnimated(false)
let index = strongSelf.itemIndexAtPoint(strongSelf.touchesPosition)
if let index = index {
if strongSelf.items[index].selectable {
strongSelf.highlightedItemIndex = index
for itemNode in strongSelf.itemNodes {
if itemNode.index == index {
if !itemNode.layerBacked {
strongSelf.view.bringSubviewToFront(itemNode.view)
}
itemNode.setHighlighted(true, animated: false)
break
}
}
}
}
}
}, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false)
self.selectionTouchDelayTimer = timer
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
super.touchesBegan(touches, withEvent: event)
self.updateScroller()
}
public func clearHighlightAnimated(animated: Bool) {
if let highlightedItemIndex = self.highlightedItemIndex {
for itemNode in self.itemNodes {
if itemNode.index == highlightedItemIndex {
itemNode.setHighlighted(false, animated: animated)
break
}
}
}
self.highlightedItemIndex = nil
}
private func itemIndexAtPoint(point: CGPoint) -> Int? {
for itemNode in self.itemNodes {
if itemNode.apparentFrame.contains(point) {
return itemNode.index
}
}
return nil
}
override public func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.touchesPosition = touches.first!.locationInView(self.view)
if let selectionTouchLocation = self.selectionTouchLocation {
let distance = CGPoint(x: selectionTouchLocation.x - self.touchesPosition.x, y: selectionTouchLocation.y - self.touchesPosition.y)
let maxMovementDistance: CGFloat = 4.0
if distance.x * distance.x + distance.y * distance.y > maxMovementDistance * maxMovementDistance {
self.selectionTouchLocation = nil
self.selectionTouchDelayTimer?.invalidate()
self.selectionTouchDelayTimer = nil
self.clearHighlightAnimated(false)
}
}
super.touchesMoved(touches, withEvent: event)
}
override public func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.isTracking = false
if let selectionTouchLocation = self.selectionTouchLocation {
let index = self.itemIndexAtPoint(selectionTouchLocation)
if index != self.highlightedItemIndex {
self.clearHighlightAnimated(false)
}
if let index = index {
if self.items[index].selectable {
self.highlightedItemIndex = index
for itemNode in self.itemNodes {
if itemNode.index == index {
if !itemNode.layerBacked {
self.view.bringSubviewToFront(itemNode.view)
}
itemNode.setHighlighted(true, animated: false)
break
}
}
}
}
}
if let highlightedItemIndex = self.highlightedItemIndex {
self.items[highlightedItemIndex].selected()
}
self.selectionTouchLocation = nil
super.touchesEnded(touches, withEvent: event)
}
override public func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.isTracking = false
self.selectionTouchLocation = nil
self.selectionTouchDelayTimer?.invalidate()
self.selectionTouchDelayTimer = nil
self.clearHighlightAnimated(false)
super.touchesCancelled(touches, withEvent: event)
}
}