diff --git a/Display.xcodeproj/project.pbxproj b/Display.xcodeproj/project.pbxproj index b81f97ac7b..f73fb852a4 100644 --- a/Display.xcodeproj/project.pbxproj +++ b/Display.xcodeproj/project.pbxproj @@ -13,6 +13,10 @@ D015F7541D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7531D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift */; }; D015F7581D1B467200E269B5 /* ActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7571D1B467200E269B5 /* ActionSheetController.swift */; }; D015F75A1D1B46B600E269B5 /* ActionSheetControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7591D1B46B600E269B5 /* ActionSheetControllerNode.swift */; }; + D01E2BDE1D9049620066BF65 /* GridNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BDD1D9049620066BF65 /* GridNode.swift */; }; + D01E2BE01D90498E0066BF65 /* GridNodeScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */; }; + D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BE11D9049F60066BF65 /* GridItemNode.swift */; }; + D01E2BE41D904A000066BF65 /* GridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BE31D904A000066BF65 /* GridItem.swift */; }; D02958001D6F096000360E5E /* ContextMenuContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02957FF1D6F096000360E5E /* ContextMenuContainerNode.swift */; }; D02BDB021B6AC703008AFAD2 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */; }; D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C01D6DF594007FC290 /* ContextMenuNode.swift */; }; @@ -120,6 +124,10 @@ D015F7531D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemContainedControllerTransitionCoordinator.swift; sourceTree = ""; }; D015F7571D1B467200E269B5 /* ActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetController.swift; sourceTree = ""; }; D015F7591D1B46B600E269B5 /* ActionSheetControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetControllerNode.swift; sourceTree = ""; }; + D01E2BDD1D9049620066BF65 /* GridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridNode.swift; sourceTree = ""; }; + D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridNodeScroller.swift; sourceTree = ""; }; + D01E2BE11D9049F60066BF65 /* GridItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItemNode.swift; sourceTree = ""; }; + D01E2BE31D904A000066BF65 /* GridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItem.swift; sourceTree = ""; }; D02957FF1D6F096000360E5E /* ContextMenuContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuContainerNode.swift; sourceTree = ""; }; D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuntimeUtils.swift; sourceTree = ""; }; D03725C01D6DF594007FC290 /* ContextMenuNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuNode.swift; sourceTree = ""; }; @@ -269,6 +277,26 @@ name = "Action Sheet"; sourceTree = ""; }; + D01E2BDC1D90494A0066BF65 /* Grid Node */ = { + isa = PBXGroup; + children = ( + D01E2BDD1D9049620066BF65 /* GridNode.swift */, + D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */, + D01E2BE31D904A000066BF65 /* GridItem.swift */, + D01E2BE11D9049F60066BF65 /* GridItemNode.swift */, + ); + name = "Grid Node"; + sourceTree = ""; + }; + D01E2BE51D904A530066BF65 /* Collection Nodes */ = { + isa = PBXGroup; + children = ( + D0C2DFBA1CC443080044FF83 /* List Node */, + D01E2BDC1D90494A0066BF65 /* Grid Node */, + ); + name = "Collection Nodes"; + sourceTree = ""; + }; D02BDAEC1B6A7053008AFAD2 /* Nodes */ = { isa = PBXGroup; children = ( @@ -340,7 +368,7 @@ isa = PBXGroup; children = ( D08122991D19A9E0005F7395 /* User Interface */, - D0C2DFBA1CC443080044FF83 /* List View */, + D01E2BE51D904A530066BF65 /* Collection Nodes */, D03BCCE91C72AE4B0097A291 /* Theme */, D05CC3001B6955D500E235A3 /* Utils */, D07921AA1B6FC911005C23D9 /* Status Bar */, @@ -483,7 +511,7 @@ name = Controllers; sourceTree = ""; }; - D0C2DFBA1CC443080044FF83 /* List View */ = { + D0C2DFBA1CC443080044FF83 /* List Node */ = { isa = PBXGroup; children = ( D0C2DFBB1CC4431D0044FF83 /* ASTransformLayerNode.swift */, @@ -497,7 +525,7 @@ D0C2DFC41CC4431D0044FF83 /* ListViewScroller.swift */, D0C2DFC51CC4431D0044FF83 /* ListViewAccessoryItemNode.swift */, ); - name = "List View"; + name = "List Node"; sourceTree = ""; }; D0DC48521BF93D7C00F672FD /* Tabs */ = { @@ -661,6 +689,8 @@ D03E7DFF1C96F7B400C07816 /* StatusBarManager.swift in Sources */, D05CC3161B695A9600E235A3 /* NavigationBar.swift in Sources */, D05CC31D1B695A9600E235A3 /* UIBarButtonItem+Proxy.m in Sources */, + D01E2BDE1D9049620066BF65 /* GridNode.swift in Sources */, + D01E2BE01D90498E0066BF65 /* GridNodeScroller.swift in Sources */, D0C85DD61D1C600D00124894 /* ActionSheetButtonNode.swift in Sources */, D0C2DFD01CC4431D0044FF83 /* ListViewAccessoryItemNode.swift in Sources */, D0D94A171D3814F900740E02 /* UniversalTapRecognizer.swift in Sources */, @@ -691,8 +721,10 @@ D015F75A1D1B46B600E269B5 /* ActionSheetControllerNode.swift in Sources */, D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */, D053CB611D22B4F200DD41DF /* CATracingLayer.m in Sources */, + D01E2BE41D904A000066BF65 /* GridItem.swift in Sources */, D081229D1D19AA1C005F7395 /* ContainerViewLayout.swift in Sources */, D0C2DFC71CC4431D0044FF83 /* ListViewItemNode.swift in Sources */, + D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */, D08E903A1D24159200533158 /* ActionSheetItem.swift in Sources */, D0AE2CA61C94548900F2FD3C /* GenerateImage.swift in Sources */, D05CC2EC1B69558A00E235A3 /* RuntimeUtils.m in Sources */, diff --git a/Display/CAAnimationUtils.swift b/Display/CAAnimationUtils.swift index 74c9a54705..95a5cff473 100644 --- a/Display/CAAnimationUtils.swift +++ b/Display/CAAnimationUtils.swift @@ -83,6 +83,28 @@ public extension CALayer { self.add(animation, forKey: keyPath) } } + + public func animateSpring(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, initialVelocity: CGFloat = 0.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + let animation = makeSpringBounceAnimation(keyPath, initialVelocity) + animation.fromValue = from + animation.toValue = to + animation.isRemovedOnCompletion = removeOnCompletion + animation.fillMode = kCAFillModeForwards + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(completion: completion) + } + + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + animation.speed = speed * Float(animation.duration / duration) + animation.isAdditive = additive + + self.add(animation, forKey: keyPath) + } public func animateAdditive(from: NSValue, to: NSValue, keyPath: String, key: String, timingFunction: String, duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { let k = Float(UIView.animationDurationFactor()) @@ -115,7 +137,7 @@ public extension CALayer { self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, removeOnCompletion: removeOnCompletion, completion: completion) } - func animatePosition(from: CGPoint, to: CGPoint, duration: Double, timingFunction: String, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + func animatePosition(from: CGPoint, to: CGPoint, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { if from == to { if let completion = completion { completion(true) @@ -146,7 +168,29 @@ public extension CALayer { } return } - self.animatePosition(from: CGPoint(x: from.midX, y: from.midY), to: CGPoint(x: to.midX, y: to.midY), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: nil) - self.animateBounds(from: CGRect(origin: self.bounds.origin, size: from.size), to: CGRect(origin: self.bounds.origin, size: to.size), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + var interrupted = false + var completedPosition = false + var completedBounds = false + var partialCompletion: () -> Void = { + if interrupted || (completedPosition && completedBounds) { + if let completion = completion { + completion(!interrupted) + } + } + } + self.animatePosition(from: CGPoint(x: from.midX, y: from.midY), to: CGPoint(x: to.midX, y: to.midY), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { value in + if !value { + interrupted = true + } + completedPosition = true + partialCompletion() + }) + self.animateBounds(from: CGRect(origin: self.bounds.origin, size: from.size), to: CGRect(origin: self.bounds.origin, size: to.size), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { value in + if !value { + interrupted = true + } + completedBounds = true + partialCompletion() + }) } } diff --git a/Display/ContextMenuNode.swift b/Display/ContextMenuNode.swift index 40b82270ff..8f8aabcbad 100644 --- a/Display/ContextMenuNode.swift +++ b/Display/ContextMenuNode.swift @@ -10,6 +10,7 @@ final class ContextMenuNode: ASDisplayNode { private let actionNodes: [ContextMenuActionNode] var sourceRect: CGRect? + var arrowOnBottom: Bool = true private var dismissedByTouchOutside = false @@ -59,6 +60,7 @@ final class ContextMenuNode: ASDisplayNode { verticalOrigin = min(layout.size.height - insets.bottom - 54.0, sourceRect.maxY) arrowOnBottom = false } + self.arrowOnBottom = arrowOnBottom let horizontalOrigin: CGFloat = floor(min(max(8.0, sourceRect.midX - actionsWidth / 2.0), layout.size.width - actionsWidth - 8.0)) @@ -69,7 +71,12 @@ final class ContextMenuNode: ASDisplayNode { } func animateIn() { - self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.2)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4) + + let containerPosition = self.containerNode.layer.position + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: containerPosition.x, y: containerPosition.y + (self.arrowOnBottom ? 1.0 : -1.0) * self.containerNode.bounds.size.height / 2.0)), to: NSValue(cgPoint: containerPosition), keyPath: "position", duration: 0.4) + + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) } func animateOut(completion: @escaping () -> Void) { diff --git a/Display/GridItem.swift b/Display/GridItem.swift new file mode 100644 index 0000000000..bcbe1e9533 --- /dev/null +++ b/Display/GridItem.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol GridItem { + func node(layout: GridNodeLayout) -> GridItemNode +} diff --git a/Display/GridItemNode.swift b/Display/GridItemNode.swift new file mode 100644 index 0000000000..8a4de56269 --- /dev/null +++ b/Display/GridItemNode.swift @@ -0,0 +1,6 @@ +import Foundation +import AsyncDisplayKit + +open class GridItemNode: ASDisplayNode { + +} diff --git a/Display/GridNode.swift b/Display/GridNode.swift new file mode 100644 index 0000000000..f89d9a24f1 --- /dev/null +++ b/Display/GridNode.swift @@ -0,0 +1,388 @@ +import Foundation +import AsyncDisplayKit + +public struct GridNodeInsertItem { + public let index: Int + public let item: GridItem + public let previousIndex: Int? + + public init(index: Int, item: GridItem, previousIndex: Int?) { + self.index = index + self.item = item + self.previousIndex = previousIndex + } +} + +public struct GridNodeUpdateItem { + public let index: Int + public let item: GridItem + + public init(index: Int, item: GridItem) { + self.index = index + self.item = item + } +} + +public enum GridNodeScrollToItemPosition { + case top + case bottom + case center +} + +public struct GridNodeScrollToItem { + public let index: Int + public let position: GridNodeScrollToItemPosition + + public init(index: Int, position: GridNodeScrollToItemPosition) { + self.index = index + self.position = position + } +} + +public struct GridNodeLayout: Equatable { + public let size: CGSize + public let insets: UIEdgeInsets + public let preloadSize: CGFloat + public let itemSize: CGSize + public let indexOffset: Int + + public init(size: CGSize, insets: UIEdgeInsets, preloadSize: CGFloat, itemSize: CGSize, indexOffset: Int) { + self.size = size + self.insets = insets + self.preloadSize = preloadSize + self.itemSize = itemSize + self.indexOffset = indexOffset + } + + public static func ==(lhs: GridNodeLayout, rhs: GridNodeLayout) -> Bool { + return lhs.size.equalTo(rhs.size) && lhs.insets == rhs.insets && lhs.preloadSize.isEqual(to: rhs.preloadSize) && lhs.itemSize.equalTo(rhs.itemSize) && lhs.indexOffset == rhs.indexOffset + } +} + +public struct GridNodeUpdateLayout { + public let layout: GridNodeLayout + public let transition: ContainedViewLayoutTransition + + public init(layout: GridNodeLayout, transition: ContainedViewLayoutTransition) { + self.layout = layout + self.transition = transition + } +} + +/*private func binarySearch(_ inputArr: [GridNodePresentationItem], searchItem: CGFloat) -> Int? { + if inputArr.isEmpty { + return nil + } + + var lowerPosition = inputArr[0].frame.origin.y + inputArr[0].frame.size.height + var upperPosition = inputArr[inputArr.count - 1].frame.origin.y + + if lowerPosition > upperPosition { + return nil + } + + while (true) { + let currentPosition = (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 + } + } + } +}*/ + +public struct GridNodeTransaction { + public let deleteItems: [Int] + public let insertItems: [GridNodeInsertItem] + public let updateItems: [GridNodeUpdateItem] + public let scrollToItem: GridNodeScrollToItem? + public let updateLayout: GridNodeUpdateLayout? + public let stationaryItemRange: (Int, Int)? + + public init(deleteItems: [Int], insertItems: [GridNodeInsertItem], updateItems: [GridNodeUpdateItem], scrollToItem: GridNodeScrollToItem?, updateLayout: GridNodeUpdateLayout?, stationaryItemRange: (Int, Int)?) { + self.deleteItems = deleteItems + self.insertItems = insertItems + self.updateItems = updateItems + self.scrollToItem = scrollToItem + self.updateLayout = updateLayout + self.stationaryItemRange = stationaryItemRange + } +} + +private struct GridNodePresentationItem { + let index: Int + let frame: CGRect +} + +private struct GridNodePresentationLayout { + let layout: GridNodeLayout + let contentOffset: CGPoint + let contentSize: CGSize + let items: [GridNodePresentationItem] +} + +private final class GridNodeItemLayout { + let contentSize: CGSize + let items: [GridNodePresentationItem] + + init(contentSize: CGSize, items: [GridNodePresentationItem]) { + self.contentSize = contentSize + self.items = items + } +} + +public struct GridNodeDisplayedItemRange: Equatable { + public let loadedRange: Range? + public let visibleRange: Range? + + public static func ==(lhs: GridNodeDisplayedItemRange, rhs: GridNodeDisplayedItemRange) -> Bool { + return lhs.loadedRange == rhs.loadedRange && lhs.visibleRange == rhs.visibleRange + } +} + +open class GridNode: GridNodeScroller, UIScrollViewDelegate { + private var gridLayout = GridNodeLayout(size: CGSize(), insets: UIEdgeInsets(), preloadSize: 0.0, itemSize: CGSize(), indexOffset: 0) + private var items: [GridItem] = [] + private var itemNodes: [Int: GridItemNode] = [:] + private var itemLayout = GridNodeItemLayout(contentSize: CGSize(), items: []) + + private var applyingContentOffset = false + + public override init() { + super.init() + + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func transaction(_ transaction: GridNodeTransaction, completion: (GridNodeDisplayedItemRange) -> Void) { + if transaction.deleteItems.isEmpty && transaction.insertItems.isEmpty && transaction.scrollToItem == nil && transaction.updateItems.isEmpty && (transaction.updateLayout == nil || transaction.updateLayout!.layout == self.gridLayout) { + completion(self.displayedItemRange()) + return + } + + if let updateLayout = transaction.updateLayout { + self.gridLayout = updateLayout.layout + } + + for updatedItem in transaction.updateItems { + self.items[updatedItem.index] = updatedItem.item + if let itemNode = self.itemNodes[updatedItem.index] { + //update node + } + } + + if !transaction.deleteItems.isEmpty || !transaction.insertItems.isEmpty { + let deleteItems = transaction.deleteItems.sorted() + + for deleteItemIndex in deleteItems.reversed() { + self.items.remove(at: deleteItemIndex) + self.removeItemNodeWithIndex(deleteItemIndex) + } + + var remappedDeletionItemNodes: [Int: GridItemNode] = [:] + + for (index, itemNode) in self.itemNodes { + var indexOffset = 0 + for deleteIndex in deleteItems { + if deleteIndex < index { + indexOffset += 1 + } else { + break + } + } + + remappedDeletionItemNodes[index - indexOffset] = itemNode + } + + let insertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) + if self.items.count == 0 && !insertItems.isEmpty { + if insertItems[0].index != 0 { + fatalError("transaction: invalid insert into empty list") + } + } + + for insertedItem in insertItems { + self.items.insert(insertedItem.item, at: insertedItem.index) + } + + var remappedInsertionItemNodes: [Int: GridItemNode] = [:] + for (index, itemNode) in remappedDeletionItemNodes { + var indexOffset = 0 + for insertedItem in transaction.insertItems { + if insertedItem.index <= index + indexOffset { + indexOffset += 1 + } + } + + remappedInsertionItemNodes[index + indexOffset] = itemNode + } + + self.itemNodes = remappedInsertionItemNodes + } + + self.itemLayout = self.generateItemLayout() + + self.applyPresentaionLayout(self.generatePresentationLayout(scrollToItemIndex: 0)) + + completion(self.displayedItemRange()) + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.applyingContentOffset { + self.applyPresentaionLayout(self.generatePresentationLayout()) + } + } + + private func displayedItemRange() -> GridNodeDisplayedItemRange { + var minIndex: Int? + var maxIndex: Int? + for index in self.itemNodes.keys { + if minIndex == nil || minIndex! > index { + minIndex = index + } + if maxIndex == nil || maxIndex! < index { + maxIndex = index + } + } + + if let minIndex = minIndex, let maxIndex = maxIndex { + return GridNodeDisplayedItemRange(loadedRange: minIndex ..< maxIndex, visibleRange: minIndex ..< maxIndex) + } else { + return GridNodeDisplayedItemRange(loadedRange: nil, visibleRange: nil) + } + } + + private func generateItemLayout() -> GridNodeItemLayout { + if CGFloat(0.0).isLess(than: gridLayout.size.width) && CGFloat(0.0).isLess(than: gridLayout.size.height) && !self.items.isEmpty { + var contentSize = CGSize(width: gridLayout.size.width, height: 0.0) + var items: [GridNodePresentationItem] = [] + + var incrementedCurrentRow = false + var nextItemOrigin = CGPoint(x: 0.0, y: 0.0) + var index = 0 + for item in self.items { + if !incrementedCurrentRow { + incrementedCurrentRow = true + contentSize.height += gridLayout.itemSize.height + } + + items.append(GridNodePresentationItem(index: index, frame: CGRect(origin: nextItemOrigin, size: gridLayout.itemSize))) + index += 1 + + nextItemOrigin.x += gridLayout.itemSize.width + if nextItemOrigin.x + gridLayout.itemSize.width > gridLayout.size.width { + nextItemOrigin.x = 0.0 + nextItemOrigin.y += gridLayout.itemSize.height + incrementedCurrentRow = false + } + } + + return GridNodeItemLayout(contentSize: contentSize, items: items) + } else { + return GridNodeItemLayout(contentSize: CGSize(), items: []) + } + } + + private func generatePresentationLayout(scrollToItemIndex: Int? = nil) -> GridNodePresentationLayout { + if CGFloat(0.0).isLess(than: gridLayout.size.width) && CGFloat(0.0).isLess(than: gridLayout.size.height) && !self.itemLayout.items.isEmpty { + let contentOffset: CGPoint + if let scrollToItemIndex = scrollToItemIndex { + let itemFrame = self.itemLayout.items[scrollToItemIndex] + + let displayHeight = max(0.0, self.gridLayout.size.height - self.gridLayout.insets.top - self.gridLayout.insets.bottom) + var verticalOffset = floor(itemFrame.frame.minY + itemFrame.frame.size.height / 2.0 - displayHeight / 2.0 - self.gridLayout.insets.top) + + if verticalOffset > self.itemLayout.contentSize.height + self.gridLayout.insets.bottom - self.gridLayout.size.height { + verticalOffset = self.itemLayout.contentSize.height + self.gridLayout.insets.bottom - self.gridLayout.size.height + } + if verticalOffset < -self.gridLayout.insets.top { + verticalOffset = -self.gridLayout.insets.top + } + + contentOffset = CGPoint(x: 0.0, y: verticalOffset) + } else { + contentOffset = self.scrollView.contentOffset + } + + let lowerDisplayBound = contentOffset.y - self.gridLayout.preloadSize + let upperDisplayBound = contentOffset.y + self.gridLayout.size.height + self.gridLayout.preloadSize + + var presentationItems: [GridNodePresentationItem] = [] + for item in self.itemLayout.items { + if item.frame.origin.y < lowerDisplayBound { + continue + } + if item.frame.origin.y + item.frame.size.height > upperDisplayBound { + break + } + presentationItems.append(item) + } + + return GridNodePresentationLayout(layout: self.gridLayout, contentOffset: contentOffset, contentSize: self.itemLayout.contentSize, items: presentationItems) + } else { + return GridNodePresentationLayout(layout: self.gridLayout, contentOffset: CGPoint(), contentSize: self.itemLayout.contentSize, items: []) + } + } + + private func applyPresentaionLayout(_ presentationLayout: GridNodePresentationLayout) { + applyingContentOffset = true + self.scrollView.contentSize = presentationLayout.contentSize + self.scrollView.contentInset = presentationLayout.layout.insets + if !self.scrollView.contentOffset.equalTo(presentationLayout.contentOffset) { + self.scrollView.setContentOffset(presentationLayout.contentOffset, animated: false) + } + applyingContentOffset = false + + var existingItemIndices = Set() + for item in presentationLayout.items { + existingItemIndices.insert(item.index) + + if let itemNode = self.itemNodes[item.index] { + itemNode.frame = item.frame + } else { + let itemNode = self.items[item.index].node(layout: presentationLayout.layout) + itemNode.frame = item.frame + self.addItemNode(index: item.index, itemNode: itemNode) + } + } + + for index in self.itemNodes.keys { + if !existingItemIndices.contains(index) { + self.removeItemNodeWithIndex(index) + } + } + } + + private func addItemNode(index: Int, itemNode: GridItemNode) { + assert(self.itemNodes[index] == nil) + self.itemNodes[index] = itemNode + if itemNode.supernode == nil { + self.addSubnode(itemNode) + } + } + + private func removeItemNodeWithIndex(_ index: Int) { + if let itemNode = self.itemNodes.removeValue(forKey: index) { + itemNode.removeFromSupernode() + } + } + + public func forEachItemNode(_ f: @noescape(ASDisplayNode) -> Void) { + for (_, node) in self.itemNodes { + f(node) + } + } +} diff --git a/Display/GridNodeScroller.swift b/Display/GridNodeScroller.swift new file mode 100644 index 0000000000..6ad5e93348 --- /dev/null +++ b/Display/GridNodeScroller.swift @@ -0,0 +1,29 @@ +import UIKit + +private class GridNodeScrollerView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + + @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } +} + +open class GridNodeScroller: ASDisplayNode, UIGestureRecognizerDelegate { + var scrollView: UIScrollView { + return self.view as! UIScrollView + } + + override init() { + super.init(viewBlock: { + return GridNodeScrollerView() + }, didLoad: nil) + + self.scrollView.scrollsToTop = false + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Display/ListView.swift b/Display/ListView.swift index ded97be467..72aa8f8fa8 100644 --- a/Display/ListView.swift +++ b/Display/ListView.swift @@ -99,11 +99,13 @@ public struct ListViewInsertItem { public struct ListViewUpdateItem { public let index: Int + public let previousIndex: Int public let item: ListViewItem public let directionHint: ListViewItemOperationDirectionHint? - public init(index: Int, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?) { + public init(index: Int, previousIndex: Int, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?) { self.index = index + self.previousIndex = previousIndex self.item = item self.directionHint = directionHint } @@ -579,8 +581,9 @@ private struct ListViewState { while i >= 0 { let itemNode = self.nodes[i] let frame = itemNode.frame + //print("node \(i) frame \(frame)") if frame.maxY < -self.invisibleInset || frame.origin.y > self.visibleSize.height + self.invisibleInset { - //print("remove \(i)") + //print("remove invisible 1 \(i) frame \(frame)") operations.append(.Remove(index: i, offsetDirection: frame.maxY < -self.invisibleInset ? .Down : .Up)) self.nodes.remove(at: i) } @@ -599,7 +602,7 @@ private struct ListViewState { if self.nodes[j].frame.maxY < upperBound { if let index = self.nodes[j].index { if index != previousIndex - 1 { - print("remove monotonity \(j) (\(index))") + //print("remove monotonity \(j) (\(index))") operations.append(.Remove(index: j, offsetDirection: .Down)) self.nodes.remove(at: j) } else { @@ -633,7 +636,7 @@ private struct ListViewState { } if !removeIndices.isEmpty { for i in removeIndices.reversed() { - print("remove monotonity \(i) (\(self.nodes[i].index!))") + //print("remove monotonity \(i) (\(self.nodes[i].index!))") operations.append(.Remove(index: i, offsetDirection: .Up)) self.nodes.remove(at: i) } @@ -776,7 +779,7 @@ private struct ListViewState { if let referenceNode = referenceNode , animated { self.nodes.insert(.Placeholder(frame: nodeFrame), at: index) - operations.append(.InsertPlaceholder(index: index, referenceNode: referenceNode, offsetDirection: offsetDirection.inverted())) + operations.append(.InsertDisappearingPlaceholder(index: index, referenceNode: referenceNode, offsetDirection: offsetDirection.inverted())) } else { if nodeFrame.maxY > self.insets.top - CGFloat(FLT_EPSILON) { if let direction = direction , direction == .Down && node.frame.minY < self.visibleSize.height - self.insets.bottom + CGFloat(FLT_EPSILON) { @@ -849,9 +852,9 @@ private struct ListViewState { } } - operations.append(.UpdateLayout(index: itemIndex, layout: layout, apply: apply)) + operations.append(.UpdateLayout(index: i, layout: layout, apply: apply)) case .System: - operations.append(.UpdateLayout(index: itemIndex, layout: layout, apply: apply)) + operations.append(.UpdateLayout(index: i, layout: layout, apply: apply)) } break @@ -862,7 +865,7 @@ private struct ListViewState { private enum ListViewStateOperation { case InsertNode(index: Int, offsetDirection: ListViewInsertionOffsetDirection, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> ()) - case InsertPlaceholder(index: Int, referenceNode: ListViewItemNode, offsetDirection: ListViewInsertionOffsetDirection) + case InsertDisappearingPlaceholder(index: Int, referenceNode: ListViewItemNode, offsetDirection: ListViewInsertionOffsetDirection) case Remove(index: Int, offsetDirection: ListViewInsertionOffsetDirection) case Remap([Int: Int]) case UpdateLayout(index: Int, layout: ListViewItemNodeLayout, apply: () -> ()) @@ -928,7 +931,7 @@ public enum ListViewVisibleContentOffset { case none } -public final class ListView: ASDisplayNode, UIScrollViewDelegate { +open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private final let scroller: ListViewScroller private final var visibleSize: CGSize = CGSize() private final var insets = UIEdgeInsets() @@ -1038,6 +1041,10 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { self.scroller.panGestureRecognizer.cancelsTouchesInView = true self.view.addGestureRecognizer(self.scroller.panGestureRecognizer) + let trackingRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.trackingGesture(_:))) + trackingRecognizer.delegate = self + self.view.addGestureRecognizer(trackingRecognizer) + self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes) if #available(iOS 10.0, *) { @@ -1181,17 +1188,18 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { var useScrollDynamics = false + let anchor: CGFloat + if self.isTracking { + anchor = self.touchesPosition.y + } else if deltaY < 0.0 { + anchor = self.visibleSize.height + } else { + anchor = 0.0 + } + 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 @@ -1443,11 +1451,19 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { } private func deleteAndInsertItemsTransaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem?, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemRange: (Int, Int)?, completion: @escaping (Void) -> Void) { - if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && scrollToItem == nil { + if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && updateIndicesAndItems.isEmpty && scrollToItem == nil { if let updateSizeAndInsets = updateSizeAndInsets , self.items.count == 0 || (updateSizeAndInsets.size == self.visibleSize && updateSizeAndInsets.insets == self.insets) { self.visibleSize = updateSizeAndInsets.size self.insets = updateSizeAndInsets.insets + if useDynamicTuning { + let size = updateSizeAndInsets.size + 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) + } + self.ignoreScrollingEvents = true self.scroller.frame = CGRect(origin: CGPoint(), size: updateSizeAndInsets.size) self.scroller.contentSize = CGSize(width: updateSizeAndInsets.size.width, height: infiniteScrollSize * 2.0) @@ -1491,17 +1507,6 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { } } - if self.debugInfo { - //print("deleteAndInsertItemsTransaction deleteIndices: \(deleteIndices.map({$0.index})) insertIndicesAndItems: \(insertIndicesAndItems.map({"\($0.index) <- \($0.previousIndex)"}))") - } - - /*if scrollToItem != nil { - print("Current indices:") - for itemNode in self.itemNodes { - print(" \(itemNode.index)") - } - }*/ - var previousNodes: [Int: ListViewItemNode] = [:] for insertedItem in sortedIndicesAndItems { self.items.insert(insertedItem.item, at: insertedItem.index) @@ -1517,8 +1522,9 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { for updatedItem in updateIndicesAndItems { self.items[updatedItem.index] = updatedItem.item for itemNode in self.itemNodes { - if itemNode.index == updatedItem.index { + if itemNode.index == updatedItem.previousIndex { previousNodes[updatedItem.index] = itemNode + break } } } @@ -1750,12 +1756,23 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { var updatedState = state var updatedOperations = operations + let heightDelta = layout.size.height - updatedState.nodes[i].frame.size.height + updatedOperations.append(.UpdateLayout(index: i, layout: layout, apply: apply)) - if nodeIndex + 1 < updatedState.nodes.count { - 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) + if !animated { + let previousFrame = updatedState.nodes[i].frame + updatedState.nodes[i].frame = CGRect(origin: previousFrame.origin, size: layout.size) + if previousFrame.minY < updatedState.insets.top { + for j in 0 ... i { + updatedState.nodes[j].frame = updatedState.nodes[j].frame.offsetBy(dx: 0.0, dy: -heightDelta) + } + } else { + if i != updatedState.nodes.count { + for j in i + 1 ..< updatedState.nodes.count { + updatedState.nodes[j].frame = updatedState.nodes[j].frame.offsetBy(dx: 0.0, dy: heightDelta) + } + } } } @@ -1998,9 +2015,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { previousApparentFrames.append((itemNode, itemNode.apparentFrame)) } - if self.debugInfo { - //print("replay before \(self.itemNodes.map({"\($0.index) \(unsafeAddressOf($0))"}))") - } + //var takenPreviousNodes = Set() for operation in operations { switch operation { @@ -2014,7 +2029,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { } self.insertNodeAtIndex(animated: animated, animateAlpha: animateAlpha, previousFrame: previousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp) self.addSubnode(node) - case let .InsertPlaceholder(index, referenceNode, offsetDirection): + case let .InsertDisappearingPlaceholder(index, referenceNode, offsetDirection): var height: CGFloat? for (node, previousFrame) in previousApparentFrames { @@ -2666,143 +2681,6 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { return ListViewDisplayedItemRange(loadedRange: loadedRange, visibleRange: visibleRange) } - 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: size, insets: insets, duration: duration, options: options, completion: { [weak self] in - if let strongSelf = self { - strongSelf.transactionOffset = 0.0 - strongSelf.updateVisibleItemsTransaction(completion: 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: @escaping (Void) -> Void) { - if size.equalTo(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 = CGSize(width: size.width, height: infiniteScrollSize * 2.0) - self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) - self.scroller.contentOffset = self.lastContentOffset - - self.updateScroller() - self.updateVisibleItemRange() - - let completion = { [weak self] (_: Bool) -> Void in - if let strongSelf = self { - strongSelf.updateVisibleItemsTransaction(completion: completion) - strongSelf.ignoreScrollingEvents = false - } - } - - if duration > DBL_EPSILON { - let animation: CABasicAnimation - if (options.rawValue & UInt(7 << 16)) != 0 { - let springAnimation = makeSpringAnimation("sublayerTransform") - springAnimation.duration = duration * UIView.animationDurationFactor() - springAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -completeOffset, 0.0)) - springAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) - springAnimation.isRemovedOnCompletion = 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.isRemovedOnCompletion = true - animation = basicAnimation - } - - animation.completion = completion - self.layer.add(animation, forKey: "sublayerTransform") - } else { - completion(true) - } - } - } - private func updateAnimations() { self.inVSync = true let actionsForVSync = self.actionsForVSync @@ -2893,10 +2771,9 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { } } - override public func touchesBegan(_ touches: Set, with event: UIEvent?) { - self.isTracking = true - self.touchesPosition = (touches.first!).location(in: self.view) - self.selectionTouchLocation = self.touchesPosition + override open func touchesBegan(_ touches: Set, with event: UIEvent?) { + self.touchesPosition = touches.first!.location(in: self.view) + self.selectionTouchLocation = touches.first!.location(in: self.view) self.selectionTouchDelayTimer?.invalidate() let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in @@ -2948,7 +2825,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { return nil } - public func forEachItemNode(_ f: @noescape(ListViewItemNode) -> Void) { + public func forEachItemNode(_ f: @noescape(ASDisplayNode) -> Void) { for itemNode in self.itemNodes { if itemNode.index != nil { f(itemNode) @@ -2956,10 +2833,10 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { } } - override public func touchesMoved(_ touches: Set, with event: UIEvent?) { - self.touchesPosition = touches.first!.location(in: self.view) + override open func touchesMoved(_ touches: Set, with event: UIEvent?) { if let selectionTouchLocation = self.selectionTouchLocation { - let distance = CGPoint(x: selectionTouchLocation.x - self.touchesPosition.x, y: selectionTouchLocation.y - self.touchesPosition.y) + let location = touches.first!.location(in: self.view) + let distance = CGPoint(x: selectionTouchLocation.x - location.x, y: selectionTouchLocation.y - location.y) let maxMovementDistance: CGFloat = 4.0 if distance.x * distance.x + distance.y * distance.y > maxMovementDistance * maxMovementDistance { self.selectionTouchLocation = nil @@ -2972,9 +2849,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { super.touchesMoved(touches, with: event) } - override public func touchesEnded(_ touches: Set, with event: UIEvent?) { - self.isTracking = false - + override open func touchesEnded(_ touches: Set, with event: UIEvent?) { if let selectionTouchLocation = self.selectionTouchLocation { let index = self.itemIndexAtPoint(selectionTouchLocation) if index != self.highlightedItemIndex { @@ -2998,16 +2873,14 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { } if let highlightedItemIndex = self.highlightedItemIndex { - self.items[highlightedItemIndex].selected() + self.items[highlightedItemIndex].selected(listView: self) } self.selectionTouchLocation = nil super.touchesEnded(touches, with: event) } - override public func touchesCancelled(_ touches: Set?, with event: UIEvent?) { - self.isTracking = false - + override open func touchesCancelled(_ touches: Set?, with event: UIEvent?) { self.selectionTouchLocation = nil self.selectionTouchDelayTimer?.invalidate() self.selectionTouchDelayTimer = nil @@ -3015,4 +2888,22 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { super.touchesCancelled(touches, with: event) } + + @objc func trackingGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.isTracking = true + break + case .changed: + self.touchesPosition = recognizer.location(in: self.view) + case .ended, .cancelled: + self.isTracking = false + default: + break + } + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } } diff --git a/Display/ListViewItem.swift b/Display/ListViewItem.swift index 6c22519549..cd9278fe2d 100644 --- a/Display/ListViewItem.swift +++ b/Display/ListViewItem.swift @@ -15,7 +15,7 @@ public protocol ListViewItem { var floatingAccessoryItem: ListViewAccessoryItem? { get } var selectable: Bool { get } - func selected() + func selected(listView: ListView) } public extension ListViewItem { @@ -35,7 +35,7 @@ public extension ListViewItem { return false } - func selected() { + func selected(listView: ListView) { } func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { diff --git a/Display/ListViewItemNode.swift b/Display/ListViewItemNode.swift index a4e3480e2d..6aa5ba6822 100644 --- a/Display/ListViewItemNode.swift +++ b/Display/ListViewItemNode.swift @@ -2,16 +2,16 @@ import Foundation import AsyncDisplayKit var testSpringFrictionLimits: (CGFloat, CGFloat) = (3.0, 60.0) -var testSpringFriction: CGFloat = 33.26 +var testSpringFriction: CGFloat = 29.3323 var testSpringConstantLimits: (CGFloat, CGFloat) = (3.0, 450.0) -var testSpringConstant: CGFloat = 450.0 +var testSpringConstant: CGFloat = 353.6746 var testSpringResistanceFreeLimits: (CGFloat, CGFloat) = (0.05, 1.0) -var testSpringFreeResistance: CGFloat = 1.0 +var testSpringFreeResistance: CGFloat = 0.6721 var testSpringResistanceScrollingLimits: (CGFloat, CGFloat) = (0.1, 1.0) -var testSpringScrollingResistance: CGFloat = 0.4 +var testSpringScrollingResistance: CGFloat = 0.6721 struct ListViewItemSpring { let stiffness: CGFloat diff --git a/Display/ListViewTransactionQueue.swift b/Display/ListViewTransactionQueue.swift index 0316299fa7..39a510c9cb 100644 --- a/Display/ListViewTransactionQueue.swift +++ b/Display/ListViewTransactionQueue.swift @@ -1,7 +1,7 @@ import Foundation import SwiftSignalKit -public typealias ListViewTransaction = @escaping (@escaping (Void) -> Void) -> Void +public typealias ListViewTransaction = (@escaping (Void) -> Void) -> Void public final class ListViewTransactionQueue { private var transactions: [ListViewTransaction] = [] @@ -10,7 +10,7 @@ public final class ListViewTransactionQueue { public init() { } - public func addTransaction(_ transaction: ListViewTransaction) { + public func addTransaction(_ transaction: @escaping ListViewTransaction) { let beginTransaction = self.transactions.count == 0 self.transactions.append(transaction) diff --git a/Display/NavigationBar.swift b/Display/NavigationBar.swift index 47458f96d7..f5a0b3f6ce 100644 --- a/Display/NavigationBar.swift +++ b/Display/NavigationBar.swift @@ -176,7 +176,7 @@ public class NavigationBar: ASDisplayNode { } self._previousItem = value - if let previousItem = value { + if let previousItem = value, previousItem.backBarButtonItem == nil { self.previousItemListenerKey = previousItem.addSetTitleListener { [weak self] text in if let strongSelf = self { strongSelf.backButtonNode.text = text ?? "Back" @@ -204,7 +204,11 @@ public class NavigationBar: ASDisplayNode { self.leftButtonNode.removeFromSupernode() if let previousItem = self.previousItem { - self.backButtonNode.text = previousItem.title ?? "Back" + if let backBarButtonItem = previousItem.backBarButtonItem { + self.backButtonNode.text = backBarButtonItem.title ?? "Back" + } else { + self.backButtonNode.text = previousItem.title ?? "Back" + } if self.backButtonNode.supernode == nil { self.clippingNode.addSubnode(self.backButtonNode) diff --git a/Display/SystemContainedControllerTransitionCoordinator.swift b/Display/SystemContainedControllerTransitionCoordinator.swift index 723d5e36b5..b861536e24 100644 --- a/Display/SystemContainedControllerTransitionCoordinator.swift +++ b/Display/SystemContainedControllerTransitionCoordinator.swift @@ -55,11 +55,11 @@ final class SystemContainedControllerTransitionCoordinator: NSObject, UIViewCont return CGAffineTransform.identity } - public func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)?, completion: (@escaping (UIViewControllerTransitionCoordinatorContext) -> Swift.Void)? = nil) -> Bool { + public func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)? = nil) -> Bool { return false } - public func animateAlongsideTransition(in view: UIView?, animation: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)?, completion: (@escaping (UIViewControllerTransitionCoordinatorContext) -> Swift.Void)? = nil) -> Bool { + public func animateAlongsideTransition(in view: UIView?, animation: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)? = nil) -> Bool { return false } diff --git a/Display/UIKitUtils.h b/Display/UIKitUtils.h index 6278f5f316..dd2807b5ed 100644 --- a/Display/UIKitUtils.h +++ b/Display/UIKitUtils.h @@ -8,5 +8,6 @@ @end CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath); +CABasicAnimation * _Nonnull makeSpringBounceAnimation(NSString * _Nonnull keyPath, CGFloat initialVelocity); CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t); diff --git a/Display/UIKitUtils.m b/Display/UIKitUtils.m index 76acce5862..acae5e4427 100644 --- a/Display/UIKitUtils.m +++ b/Display/UIKitUtils.m @@ -42,6 +42,17 @@ CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath) { return springAnimation; } +CABasicAnimation * _Nonnull makeSpringBounceAnimation(NSString * _Nonnull keyPath, CGFloat initialVelocity) { + CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:keyPath]; + springAnimation.mass = 5.0f; + springAnimation.stiffness = 900.0f; + springAnimation.damping = 88.0f; + springAnimation.initialVelocity = initialVelocity; + springAnimation.duration = springAnimation.settlingDuration; + springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + return springAnimation; +} + CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t) { return [(CASpringAnimation *)animation _solveForInput:t]; }