import Foundation import UIKit import AsyncDisplayKit public enum GridNodeVisibleContentOffset { case known(CGFloat) case unknown case none } 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 previousIndex: Int public let item: GridItem public init(index: Int, previousIndex: Int, item: GridItem) { self.index = index self.previousIndex = previousIndex self.item = item } } public enum GridNodeScrollToItemPosition { case top(CGFloat) case bottom(CGFloat) case center(CGFloat) case visible } public struct GridNodeScrollToItem { public let index: Int public let position: GridNodeScrollToItemPosition public let transition: ContainedViewLayoutTransition public let directionHint: GridNodePreviousItemsTransitionDirectionHint public let adjustForSection: Bool public let adjustForTopInset: Bool public init(index: Int, position: GridNodeScrollToItemPosition, transition: ContainedViewLayoutTransition, directionHint: GridNodePreviousItemsTransitionDirectionHint, adjustForSection: Bool, adjustForTopInset: Bool = false) { self.index = index self.position = position self.transition = transition self.directionHint = directionHint self.adjustForSection = adjustForSection self.adjustForTopInset = adjustForTopInset } } public enum GridNodeLayoutType: Equatable { case fixed(itemSize: CGSize, fillWidth: Bool?, lineSpacing: CGFloat, itemSpacing: CGFloat?) case balanced(idealHeight: CGFloat) } public struct GridNodeLayout: Equatable { public let size: CGSize public let insets: UIEdgeInsets public let scrollIndicatorInsets: UIEdgeInsets? public let preloadSize: CGFloat public let type: GridNodeLayoutType public init(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets? = nil, preloadSize: CGFloat, type: GridNodeLayoutType) { self.size = size self.insets = insets self.scrollIndicatorInsets = scrollIndicatorInsets self.preloadSize = preloadSize self.type = type } } public struct GridNodeUpdateLayout { public let layout: GridNodeLayout public let transition: ContainedViewLayoutTransition public init(layout: GridNodeLayout, transition: ContainedViewLayoutTransition) { self.layout = layout self.transition = transition } } public enum GridNodeStationaryItems { case none case all case indices(Set) } 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 itemTransition: ContainedViewLayoutTransition public let stationaryItems: GridNodeStationaryItems public let updateFirstIndexInSectionOffset: Int? public let updateOpaqueState: Any? public let synchronousLoads: Bool public init(deleteItems: [Int], insertItems: [GridNodeInsertItem], updateItems: [GridNodeUpdateItem], scrollToItem: GridNodeScrollToItem?, updateLayout: GridNodeUpdateLayout?, itemTransition: ContainedViewLayoutTransition, stationaryItems: GridNodeStationaryItems, updateFirstIndexInSectionOffset: Int?, updateOpaqueState: Any? = nil, synchronousLoads: Bool = false) { self.deleteItems = deleteItems self.insertItems = insertItems self.updateItems = updateItems self.scrollToItem = scrollToItem self.updateLayout = updateLayout self.itemTransition = itemTransition self.stationaryItems = stationaryItems self.updateFirstIndexInSectionOffset = updateFirstIndexInSectionOffset self.updateOpaqueState = updateOpaqueState self.synchronousLoads = synchronousLoads } } private struct GridNodePresentationItem { let index: Int let frame: CGRect } private struct GridNodePresentationSection { let section: GridSection let frame: CGRect } private struct GridNodePresentationLayout { let layout: GridNodeLayout let contentOffset: CGPoint let contentSize: CGSize let items: [GridNodePresentationItem] let sections: [GridNodePresentationSection] } public enum GridNodePreviousItemsTransitionDirectionHint { case up case down } private struct GridNodePresentationLayoutTransition { let layout: GridNodePresentationLayout let directionHint: GridNodePreviousItemsTransitionDirectionHint let transition: ContainedViewLayoutTransition } public struct GridNodeCurrentPresentationLayout { public let layout: GridNodeLayout public let contentOffset: CGPoint public let contentSize: CGSize } private final class GridNodeItemLayout { let contentSize: CGSize let items: [GridNodePresentationItem] let sections: [GridNodePresentationSection] init(contentSize: CGSize, items: [GridNodePresentationItem], sections: [GridNodePresentationSection]) { self.contentSize = contentSize self.items = items self.sections = sections } } public struct GridNodeDisplayedItemRange: Equatable { public let loadedRange: Range? public let visibleRange: Range? } private struct WrappedGridSection: Equatable, Hashable { let section: GridSection init(_ section: GridSection) { self.section = section } static func ==(lhs: WrappedGridSection, rhs: WrappedGridSection) -> Bool { return lhs.section.isEqual(to: rhs.section) } func hash(into hasher: inout Hasher) { hasher.combine(self.section.hashValue) } } public struct GridNodeVisibleItems { public let top: (Int, GridItem)? public let bottom: (Int, GridItem)? public let topVisible: (Int, GridItem)? public let bottomVisible: (Int, GridItem)? public let topSectionVisible: GridSection? public let count: Int } private struct WrappedGridItemNode: Hashable { let node: ASDisplayNode static func ==(lhs: WrappedGridItemNode, rhs: WrappedGridItemNode) -> Bool { return lhs.node === rhs.node } } open class GridNode: GridNodeScroller, UIScrollViewDelegate { private var gridLayout = GridNodeLayout(size: CGSize(), insets: UIEdgeInsets(), preloadSize: 0.0, type: .fixed(itemSize: CGSize(), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)) private var firstIndexInSectionOffset: Int = 0 public private(set) var items: [GridItem] = [] private var itemNodes: [Int: GridItemNode] = [:] private var sectionNodes: [WrappedGridSection: ASDisplayNode] = [:] private var itemLayout = GridNodeItemLayout(contentSize: CGSize(), items: [], sections: []) public var setupNode: ((GridItemNode) -> Void)? private var applyingContentOffset = false public var visibleItemsUpdated: ((GridNodeVisibleItems) -> Void)? public var presentationLayoutUpdated: ((GridNodeCurrentPresentationLayout, ContainedViewLayoutTransition) -> Void)? public var scrollingInitiated: (() -> Void)? public var scrollingCompleted: (() -> Void)? public var interactiveScrollingEnded: (() -> Void)? public var interactiveScrollingWillBeEnded: ((CGPoint, CGPoint) -> Void)? public var visibleContentOffsetChanged: (GridNodeVisibleContentOffset) -> Void = { _ in } public final var floatingSections = false public final var initialOffset: CGFloat = 0.0 public var showVerticalScrollIndicator: Bool = false { didSet { self.scrollView.showsVerticalScrollIndicator = self.showVerticalScrollIndicator } } public var indicatorStyle: UIScrollView.IndicatorStyle = .default { didSet { self.scrollView.indicatorStyle = self.indicatorStyle } } public private(set) var opaqueState: Any? 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 let updateOpaqueState = transaction.updateOpaqueState { self.opaqueState = updateOpaqueState } if transaction.deleteItems.isEmpty && transaction.insertItems.isEmpty && transaction.scrollToItem == nil && transaction.updateItems.isEmpty && (transaction.updateLayout == nil || transaction.updateLayout!.layout == self.gridLayout && (transaction.updateFirstIndexInSectionOffset == nil || transaction.updateFirstIndexInSectionOffset == self.firstIndexInSectionOffset)) { if let presentationLayoutUpdated = self.presentationLayoutUpdated { presentationLayoutUpdated(GridNodeCurrentPresentationLayout(layout: self.gridLayout, contentOffset: self.scrollView.contentOffset, contentSize: self.itemLayout.contentSize), transaction.updateLayout?.transition ?? .immediate) } completion(self.displayedItemRange()) return } if let updateFirstIndexInSectionOffset = transaction.updateFirstIndexInSectionOffset { self.firstIndexInSectionOffset = updateFirstIndexInSectionOffset } var layoutTransactionOffset: CGFloat = 0.0 if let updateLayout = transaction.updateLayout { layoutTransactionOffset += updateLayout.layout.insets.top - self.gridLayout.insets.top self.gridLayout = updateLayout.layout } for updatedItem in transaction.updateItems { self.items[updatedItem.previousIndex] = updatedItem.item if let itemNode = self.itemNodes[updatedItem.previousIndex] { updatedItem.item.update(node: itemNode) } } var removedNodes: [GridItemNode] = [] if !transaction.deleteItems.isEmpty || !transaction.insertItems.isEmpty { let deleteItems = transaction.deleteItems.sorted() for deleteItemIndex in deleteItems.reversed() { self.items.remove(at: deleteItemIndex) if let itemNode = self.itemNodes[deleteItemIndex] { removedNodes.append(itemNode) self.removeItemNodeWithIndex(deleteItemIndex, removeNode: false) } else { self.removeItemNodeWithIndex(deleteItemIndex, removeNode: true) } } 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) } let sortedInsertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) var remappedInsertionItemNodes: [Int: GridItemNode] = [:] for (index, itemNode) in remappedDeletionItemNodes { var indexOffset = 0 for insertedItem in sortedInsertItems { if insertedItem.index <= index + indexOffset { indexOffset += 1 } } remappedInsertionItemNodes[index + indexOffset] = itemNode } self.itemNodes = remappedInsertionItemNodes } let previousLayoutWasEmpty = self.itemLayout.items.isEmpty self.itemLayout = self.generateItemLayout() var updateLayoutTransition = transaction.updateLayout?.transition let generatedScrollToItem: GridNodeScrollToItem? if let scrollToItem = transaction.scrollToItem { generatedScrollToItem = scrollToItem if updateLayoutTransition == nil { updateLayoutTransition = scrollToItem.transition } } else if previousLayoutWasEmpty { generatedScrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .immediate, directionHint: .up, adjustForSection: true, adjustForTopInset: true) } else { generatedScrollToItem = nil } self.applyPresentationLayoutTransition(self.generatePresentationLayoutTransition(stationaryItems: transaction.stationaryItems, layoutTransactionOffset: layoutTransactionOffset, scrollToItem: generatedScrollToItem), removedNodes: removedNodes, updateLayoutTransition: updateLayoutTransition, customScrollToItem: transaction.scrollToItem != nil, itemTransition: transaction.itemTransition, synchronousLoads: transaction.synchronousLoads, updatingLayout: transaction.updateLayout != nil, completion: completion) } public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.updateItemNodeVisibilititesAndScrolling() self.updateVisibleContentOffset() self.scrollingInitiated?() } public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { self.interactiveScrollingWillBeEnded?(velocity, targetContentOffset.pointee) } public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { self.interactiveScrollingEnded?() if !decelerate { self.updateItemNodeVisibilititesAndScrolling() self.updateVisibleContentOffset() self.scrollingCompleted?() } } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { self.updateItemNodeVisibilititesAndScrolling() self.updateVisibleContentOffset() self.scrollingCompleted?() } public func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.applyingContentOffset { self.applyPresentationLayoutTransition(self.generatePresentationLayoutTransition(layoutTransactionOffset: 0.0), removedNodes: [], updateLayoutTransition: nil, customScrollToItem: false, itemTransition: .immediate, synchronousLoads: false, updatingLayout: false, completion: { _ in }) self.updateVisibleContentOffset() } } 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) { var contentSize = CGSize(width: gridLayout.size.width, height: 0.0) var items: [GridNodePresentationItem] = [] var sections: [GridNodePresentationSection] = [] switch gridLayout.type { case let .fixed(defaultItemSize, fillWidth, lineSpacing, defaultItemSpacing): let itemInsets = gridLayout.insets let effectiveWidth = gridLayout.size.width - itemInsets.left - itemInsets.right let itemsInRow = Int(effectiveWidth / defaultItemSize.width) let itemsInRowWidth = CGFloat(itemsInRow) * defaultItemSize.width let remainingWidth = max(0.0, effectiveWidth - itemsInRowWidth) let itemSpacing = defaultItemSpacing ?? floorToScreenPixels(remainingWidth / CGFloat(itemsInRow + 1)) let initialSpacing: CGFloat = (fillWidth ?? false) ? 0.0 : itemSpacing var incrementedCurrentRow = false var nextItemOrigin = CGPoint(x: initialSpacing + itemInsets.left, y: 0.0) var index = 0 var previousSection: GridSection? for item in self.items { var itemSize = defaultItemSize let section = item.section var keepSection = true if let previousSection = previousSection, let section = section { keepSection = previousSection.isEqual(to: section) } else if (previousSection != nil) != (section != nil) { keepSection = false } if !keepSection { if incrementedCurrentRow { nextItemOrigin.x = initialSpacing + itemInsets.left nextItemOrigin.y += itemSize.height + lineSpacing incrementedCurrentRow = false } if let section = section { sections.append(GridNodePresentationSection(section: section, frame: CGRect(origin: CGPoint(x: 0.0, y: nextItemOrigin.y), size: CGSize(width: gridLayout.size.width, height: section.height)))) nextItemOrigin.y += section.height contentSize.height += section.height } } previousSection = section if let height = item.fillsRowWithHeight { nextItemOrigin.x = 0.0 itemSize.width = gridLayout.size.width itemSize.height = height } else if let fillsRowWithDynamicHeight = item.fillsRowWithDynamicHeight { let height = fillsRowWithDynamicHeight(gridLayout.size.width) nextItemOrigin.x = 0.0 itemSize.width = gridLayout.size.width itemSize.height = height } else if index == 0 { let itemsInRow = max(1, Int(effectiveWidth) / Int(itemSize.width)) let normalizedIndexOffset = self.firstIndexInSectionOffset % itemsInRow nextItemOrigin.x += (itemSize.width + itemSpacing) * CGFloat(normalizedIndexOffset) } else if let fillWidth = fillWidth, fillWidth { let nextItemOriginX = nextItemOrigin.x + itemSize.width + itemSpacing let remainingWidth = remainingWidth - CGFloat(itemsInRow - 1) * itemSpacing if nextItemOriginX + itemSize.width > self.gridLayout.size.width - itemInsets.right && remainingWidth > 0.0 { itemSize.width += remainingWidth } } if !incrementedCurrentRow { incrementedCurrentRow = true contentSize.height += itemSize.height + lineSpacing } items.append(GridNodePresentationItem(index: index, frame: CGRect(origin: nextItemOrigin, size: itemSize))) index += 1 nextItemOrigin.x += itemSize.width + itemSpacing if nextItemOrigin.x + itemSize.width > gridLayout.size.width - itemInsets.right { nextItemOrigin.x = initialSpacing + itemInsets.left nextItemOrigin.y += itemSize.height + lineSpacing incrementedCurrentRow = false } } case let .balanced(idealHeight): var weights: [Int] = [] for item in self.items { weights.append(Int(item.aspectRatio * 100)) } var totalItemSize: CGFloat = 0.0 for i in 0 ..< self.items.count { totalItemSize += self.items[i].aspectRatio * idealHeight } let numberOfRows = max(Int(round(totalItemSize / gridLayout.size.width)), 1) let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows) var i = 0 var offset = CGPoint(x: 0.0, y: 0.0) var previousItemSize: CGFloat = 0.0 var contentMaxValueInScrollDirection: CGFloat = 0.0 let maxWidth = gridLayout.size.width let minimumInteritemSpacing: CGFloat = 1.0 let minimumLineSpacing: CGFloat = 1.0 let viewportWidth: CGFloat = gridLayout.size.width let preferredRowSize = idealHeight var rowIndex = -1 for row in partition { rowIndex += 1 var summedRatios: CGFloat = 0.0 var j = i var n = i + row.count while j < n { summedRatios += self.items[j].aspectRatio j += 1 } var rowSize = gridLayout.size.width - (CGFloat(row.count - 1) * minimumInteritemSpacing) if rowIndex == partition.count - 1 { if row.count < 2 { rowSize = floor(viewportWidth / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing) } else if row.count < 3 { rowSize = floor(viewportWidth * 2.0 / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing) } } j = i n = i + row.count while j < n { let preferredAspectRatio = self.items[j].aspectRatio let actualSize = CGSize(width: round(rowSize / summedRatios * (preferredAspectRatio)), height: preferredRowSize) var frame = CGRect(x: offset.x, y: offset.y, width: actualSize.width, height: actualSize.height) if frame.origin.x + frame.size.width >= maxWidth - 2.0 { frame.size.width = max(1.0, maxWidth - frame.origin.x) } items.append(GridNodePresentationItem(index: j, frame: frame)) offset.x += actualSize.width + minimumInteritemSpacing previousItemSize = actualSize.height contentMaxValueInScrollDirection = frame.maxY j += 1 } if row.count > 0 { offset = CGPoint(x: 0.0, y: offset.y + previousItemSize + minimumLineSpacing) } i += row.count } contentSize = CGSize(width: gridLayout.size.width, height: contentMaxValueInScrollDirection) } return GridNodeItemLayout(contentSize: contentSize, items: items, sections: sections) } else { return GridNodeItemLayout(contentSize: CGSize(), items: [], sections: []) } } private func generatePresentationLayoutTransition(stationaryItems: GridNodeStationaryItems = .none, layoutTransactionOffset: CGFloat, scrollToItem: GridNodeScrollToItem? = nil) -> GridNodePresentationLayoutTransition { if CGFloat(0.0).isLess(than: self.gridLayout.size.width) && CGFloat(0.0).isLess(than: self.gridLayout.size.height) { var transitionDirectionHint: GridNodePreviousItemsTransitionDirectionHint = .up var transition: ContainedViewLayoutTransition = .immediate let contentOffset: CGPoint var updatedStationaryItems = stationaryItems if let scrollToItem = scrollToItem { updatedStationaryItems = .none if case .immediate = transition { transition = scrollToItem.transition } } switch updatedStationaryItems { case .none: if let scrollToItem = scrollToItem { if self.itemLayout.items.isEmpty { transitionDirectionHint = scrollToItem.directionHint transition = scrollToItem.transition contentOffset = CGPoint(x: 0.0, y: -self.gridLayout.insets.top + self.initialOffset) } else { let itemFrame = self.itemLayout.items[scrollToItem.index] var additionalOffset: CGFloat = 0.0 if scrollToItem.adjustForSection { var adjustForSection: GridSection? if scrollToItem.index == 0 { if let itemSection = self.items[scrollToItem.index].section { adjustForSection = itemSection } } else { let itemSection = self.items[scrollToItem.index].section let previousSection = self.items[scrollToItem.index - 1].section if let itemSection = itemSection, let previousSection = previousSection { if !itemSection.isEqual(to: previousSection) { adjustForSection = itemSection } } else if let itemSection = itemSection { adjustForSection = itemSection } } if let adjustForSection = adjustForSection { additionalOffset = -adjustForSection.height } if scrollToItem.adjustForTopInset { additionalOffset += -gridLayout.insets.top// + self.initialOffset } } else if scrollToItem.adjustForTopInset { additionalOffset = -gridLayout.insets.top } let displayHeight = max(0.0, self.gridLayout.size.height - self.gridLayout.insets.top - self.gridLayout.insets.bottom) var verticalOffset: CGFloat = self.scrollView.contentOffset.y switch scrollToItem.position { case let .top(offset): verticalOffset = itemFrame.frame.minY + additionalOffset + offset case let .center(offset): verticalOffset = floor(itemFrame.frame.minY + itemFrame.frame.size.height / 2.0 - displayHeight / 2.0 - self.gridLayout.insets.top) + additionalOffset + offset case let .bottom(offset): verticalOffset = itemFrame.frame.maxY - displayHeight + additionalOffset + offset case .visible: if verticalOffset + self.gridLayout.insets.top > itemFrame.frame.minY { //verticalOffset = -self.gridLayout.insets.top + itemFrame.frame.minY } else if verticalOffset + self.gridLayout.insets.top + displayHeight < itemFrame.frame.maxY { verticalOffset = -self.gridLayout.insets.top - displayHeight + itemFrame.frame.maxY } } 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 } transitionDirectionHint = scrollToItem.directionHint transition = scrollToItem.transition contentOffset = CGPoint(x: 0.0, y: verticalOffset) } } else { if !layoutTransactionOffset.isZero { var verticalOffset = self.scrollView.contentOffset.y - layoutTransactionOffset 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 } } case let .indices(stationaryItemIndices): var selectedContentOffset: CGPoint? for (index, itemNode) in self.itemNodes { if stationaryItemIndices.contains(index) { //let currentScreenOffset = itemNode.frame.origin.y - self.scrollView.contentOffset.y selectedContentOffset = CGPoint(x: 0.0, y: self.itemLayout.items[index].frame.origin.y - itemNode.frame.origin.y + self.scrollView.contentOffset.y) break } } if let _ = selectedContentOffset, self.itemNodes.count > 0, let itemNode = self.itemNodes[0], self.scrollView.contentInset.top + self.scrollView.contentOffset.y <= itemNode.frame.maxY { selectedContentOffset = self.scrollView.contentOffset } if let selectedContentOffset = selectedContentOffset { contentOffset = selectedContentOffset } else { contentOffset = self.scrollView.contentOffset } case .all: var selectedContentOffset: CGPoint? for (index, itemNode) in self.itemNodes { //let currentScreenOffset = itemNode.frame.origin.y - self.scrollView.contentOffset.y selectedContentOffset = CGPoint(x: 0.0, y: self.itemLayout.items[index].frame.origin.y - itemNode.frame.origin.y + self.scrollView.contentOffset.y) break } if let selectedContentOffset = selectedContentOffset { contentOffset = selectedContentOffset } else { contentOffset = self.scrollView.contentOffset } } let lowerDisplayBound = contentOffset.y - self.gridLayout.insets.top - self.gridLayout.preloadSize let upperDisplayBound = contentOffset.y + self.gridLayout.insets.bottom + self.gridLayout.size.height + self.gridLayout.preloadSize var presentationItems: [GridNodePresentationItem] = [] var validSections = Set() for item in self.itemLayout.items { if item.frame.maxY < lowerDisplayBound { continue } if item.frame.minY > upperDisplayBound { break } presentationItems.append(item) if self.floatingSections { if let section = self.items[item.index].section { validSections.insert(WrappedGridSection(section)) } } } var presentationSections: [GridNodePresentationSection] = [] for section in self.itemLayout.sections { if section.frame.origin.y < lowerDisplayBound { if !validSections.contains(WrappedGridSection(section.section)) { continue } } if section.frame.origin.y + section.frame.size.height > upperDisplayBound { break } presentationSections.append(section) } return GridNodePresentationLayoutTransition(layout: GridNodePresentationLayout(layout: self.gridLayout, contentOffset: contentOffset, contentSize: self.itemLayout.contentSize, items: presentationItems, sections: presentationSections), directionHint: transitionDirectionHint, transition: transition) } else { return GridNodePresentationLayoutTransition(layout: GridNodePresentationLayout(layout: self.gridLayout, contentOffset: CGPoint(), contentSize: self.itemLayout.contentSize, items: [], sections: []), directionHint: .up, transition: .immediate) } } public func lowestSectionNode() -> ASDisplayNode? { var lowestHeaderNode: ASDisplayNode? var lowestHeaderNodeIndex: Int? for (_, headerNode) in self.sectionNodes { if let index = self.subnodes?.firstIndex(of: headerNode) { if lowestHeaderNodeIndex == nil || index < lowestHeaderNodeIndex! { lowestHeaderNodeIndex = index lowestHeaderNode = headerNode } } } return lowestHeaderNode } private func applyPresentationLayoutTransition(_ presentationLayoutTransition: GridNodePresentationLayoutTransition, removedNodes: [GridItemNode], updateLayoutTransition: ContainedViewLayoutTransition?, customScrollToItem: Bool, itemTransition: ContainedViewLayoutTransition, synchronousLoads: Bool, updatingLayout: Bool, completion: (GridNodeDisplayedItemRange) -> Void) { let boundsTransition: ContainedViewLayoutTransition = updateLayoutTransition ?? .immediate var addedNodes = false let verticalIndicator = self.scrollView.subviews.last as? UIImageView var previousItemFrames: [WrappedGridItemNode: CGRect]? var saveItemFrames = false switch presentationLayoutTransition.transition { case .animated: saveItemFrames = true case .immediate: break } if case .animated = itemTransition { saveItemFrames = true } if saveItemFrames { var itemFrames: [WrappedGridItemNode: CGRect] = [:] let contentOffset = self.scrollView.contentOffset for (_, itemNode) in self.itemNodes { itemFrames[WrappedGridItemNode(node: itemNode)] = itemNode.frame.offsetBy(dx: 0.0, dy: -contentOffset.y) } for (_, sectionNode) in self.sectionNodes { itemFrames[WrappedGridItemNode(node: sectionNode)] = sectionNode.frame.offsetBy(dx: 0.0, dy: -contentOffset.y) } for itemNode in removedNodes { itemFrames[WrappedGridItemNode(node: itemNode)] = itemNode.frame.offsetBy(dx: 0.0, dy: -contentOffset.y) } previousItemFrames = itemFrames } self.applyingContentOffset = true let previousBounds = self.bounds self.scrollView.contentSize = presentationLayoutTransition.layout.contentSize let layoutInsets = presentationLayoutTransition.layout.layout.insets self.scrollView.contentInset = UIEdgeInsets(top: layoutInsets.top, left: 0.0, bottom: layoutInsets.bottom, right: 0.0) if let scrollIndicatorInsets = presentationLayoutTransition.layout.layout.scrollIndicatorInsets { self.scrollView.scrollIndicatorInsets = scrollIndicatorInsets } else { self.scrollView.scrollIndicatorInsets = presentationLayoutTransition.layout.layout.insets } var boundsOffset: CGFloat = 0.0 var shouldAnimateBounds = false if !self.scrollView.contentOffset.equalTo(presentationLayoutTransition.layout.contentOffset) || self.bounds.size != presentationLayoutTransition.layout.layout.size { let updatedBounds = CGRect(origin: presentationLayoutTransition.layout.contentOffset, size: presentationLayoutTransition.layout.layout.size) boundsOffset = updatedBounds.origin.y - previousBounds.origin.y self.bounds = updatedBounds shouldAnimateBounds = true } self.applyingContentOffset = false let lowestSectionNode: ASDisplayNode? = self.lowestSectionNode() let bounds = self.bounds var existingItemIndices = Set() for item in presentationLayoutTransition.layout.items { existingItemIndices.insert(item.index) let itemInBounds = bounds.intersects(item.frame) if let itemNode = self.itemNodes[item.index] { if itemNode.frame != item.frame { itemNode.frame = item.frame } itemNode.updateLayout(item: self.items[item.index], size: item.frame.size, isVisible: bounds.intersects(item.frame), synchronousLoads: synchronousLoads && itemInBounds) } else { let itemNode = self.items[item.index].node(layout: presentationLayoutTransition.layout.layout, synchronousLoad: synchronousLoads && itemInBounds) itemNode.frame = item.frame self.addItemNode(index: item.index, itemNode: itemNode, lowestSectionNode: lowestSectionNode) addedNodes = true itemNode.updateLayout(item: self.items[item.index], size: item.frame.size, isVisible: bounds.intersects(item.frame), synchronousLoads: synchronousLoads) self.setupNode?(itemNode) } } var existingSections = Set() for i in 0 ..< presentationLayoutTransition.layout.sections.count { let section = presentationLayoutTransition.layout.sections[i] let wrappedSection = WrappedGridSection(section.section) existingSections.insert(wrappedSection) var sectionFrame = section.frame if self.floatingSections { var maxY = CGFloat.greatestFiniteMagnitude if i != presentationLayoutTransition.layout.sections.count - 1 { maxY = presentationLayoutTransition.layout.sections[i + 1].frame.minY - sectionFrame.height } sectionFrame.origin.y = max(sectionFrame.minY, min(maxY, presentationLayoutTransition.layout.contentOffset.y + presentationLayoutTransition.layout.layout.insets.top)) } if let sectionNode = self.sectionNodes[wrappedSection] { sectionNode.frame = sectionFrame } else { let sectionNode = section.section.node() sectionNode.frame = sectionFrame self.addSectionNode(section: wrappedSection, sectionNode: sectionNode) addedNodes = true } } if let previousItemFrames = previousItemFrames, case let .animated(duration, curve) = presentationLayoutTransition.transition { let contentOffset = presentationLayoutTransition.layout.contentOffset if !updatingLayout { boundsOffset = 0.0 shouldAnimateBounds = false } var offset: CGFloat? for (index, itemNode) in self.itemNodes { if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)], existingItemIndices.contains(index) { let currentFrame = itemNode.frame.offsetBy(dx: 0.0, dy: -presentationLayoutTransition.layout.contentOffset.y) offset = previousFrame.origin.y - currentFrame.origin.y - boundsOffset break } } if offset == nil { var previousUpperBound: CGFloat? var previousLowerBound: CGFloat? for (_, frame) in previousItemFrames { if previousUpperBound == nil || previousUpperBound! > frame.minY { previousUpperBound = frame.minY } if previousLowerBound == nil || previousLowerBound! < frame.maxY { previousLowerBound = frame.maxY } } var updatedUpperBound: CGFloat? var updatedLowerBound: CGFloat? for item in presentationLayoutTransition.layout.items { let frame = item.frame.offsetBy(dx: 0.0, dy: -contentOffset.y) if updatedUpperBound == nil || updatedUpperBound! > frame.minY { updatedUpperBound = frame.minY } if updatedLowerBound == nil || updatedLowerBound! < frame.maxY { updatedLowerBound = frame.maxY } } for section in presentationLayoutTransition.layout.sections { let frame = section.frame.offsetBy(dx: 0.0, dy: -contentOffset.y) if updatedUpperBound == nil || updatedUpperBound! > frame.minY { updatedUpperBound = frame.minY } if updatedLowerBound == nil || updatedLowerBound! < frame.maxY { updatedLowerBound = frame.maxY } } if let updatedUpperBound = updatedUpperBound, let updatedLowerBound = updatedLowerBound { switch presentationLayoutTransition.directionHint { case .up: offset = -(updatedLowerBound - (previousUpperBound ?? 0.0)) case .down: offset = -(updatedUpperBound - (previousLowerBound ?? presentationLayoutTransition.layout.layout.size.height)) } } } if let offset = offset { let timingFunction = curve.timingFunction let mediaTimingFunction = curve.mediaTimingFunction for (index, itemNode) in self.itemNodes where existingItemIndices.contains(index) { itemNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, additive: true) } for (wrappedSection, sectionNode) in self.sectionNodes where existingSections.contains(wrappedSection) { sectionNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, additive: true) } for index in self.itemNodes.keys { if !existingItemIndices.contains(index) { let itemNode = self.itemNodes[index]! if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)] { self.removeItemNodeWithIndex(index, removeNode: false) let position = CGPoint(x: previousFrame.midX, y: previousFrame.midY) itemNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y - boundsOffset), to: CGPoint(x: position.x, y: position.y + contentOffset.y - boundsOffset - offset), duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: false, force: true, completion: { [weak itemNode] _ in itemNode?.removeFromSupernode() }) } else { self.removeItemNodeWithIndex(index, removeNode: true) } } } for itemNode in removedNodes { if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)] { let position = CGPoint(x: previousFrame.midX, y: previousFrame.midY) itemNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y), to: CGPoint(x: position.x, y: position.y + contentOffset.y - offset), duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: false, force: true, completion: { [weak itemNode] _ in itemNode?.removeFromSupernode() }) } else { itemNode.removeFromSupernode() } } for wrappedSection in self.sectionNodes.keys { if !existingSections.contains(wrappedSection) { let sectionNode = self.sectionNodes[wrappedSection]! if let previousFrame = previousItemFrames[WrappedGridItemNode(node: sectionNode)] { self.removeSectionNodeWithSection(wrappedSection, removeNode: false) let position = CGPoint(x: previousFrame.midX, y: previousFrame.midY) sectionNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y), to: CGPoint(x: position.x, y: position.y + contentOffset.y - offset), duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: false, force: true, completion: { [weak sectionNode] _ in sectionNode?.removeFromSupernode() }) } else { self.removeSectionNodeWithSection(wrappedSection, removeNode: true) } } } } else { for index in self.itemNodes.keys { if !existingItemIndices.contains(index) { self.removeItemNodeWithIndex(index) } } for wrappedSection in self.sectionNodes.keys { if !existingSections.contains(wrappedSection) { self.removeSectionNodeWithSection(wrappedSection) } } for itemNode in removedNodes { itemNode.removeFromSupernode() } } } else if let previousItemFrames = previousItemFrames, case let .animated(duration, curve) = itemTransition { let timingFunction = curve.timingFunction let mediaTimingFunction = curve.mediaTimingFunction let contentOffset = self.scrollView.contentOffset for index in self.itemNodes.keys { let itemNode = self.itemNodes[index]! if !existingItemIndices.contains(index) { if let _ = previousItemFrames[WrappedGridItemNode(node: itemNode)] { self.removeItemNodeWithIndex(index, removeNode: false) itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false) itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false, completion: { [weak itemNode] _ in itemNode?.removeFromSupernode() }) } else { self.removeItemNodeWithIndex(index, removeNode: true) } } else if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)] { itemNode.layer.animatePosition(from: CGPoint(x: previousFrame.midX, y: previousFrame.midY + contentOffset.y), to: itemNode.layer.position, duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction) } else { itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) } } for itemNode in removedNodes { if let _ = previousItemFrames[WrappedGridItemNode(node: itemNode)] { itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false) itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.18, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false, completion: { [weak itemNode] _ in itemNode?.removeFromSupernode() }) } else { itemNode.removeFromSupernode() } } for wrappedSection in self.sectionNodes.keys { let sectionNode = self.sectionNodes[wrappedSection]! if !existingSections.contains(wrappedSection) { if let _ = previousItemFrames[WrappedGridItemNode(node: sectionNode)] { self.removeSectionNodeWithSection(wrappedSection, removeNode: false) sectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak sectionNode] _ in sectionNode?.removeFromSupernode() }) } else { self.removeSectionNodeWithSection(wrappedSection, removeNode: true) } } else if let previousFrame = previousItemFrames[WrappedGridItemNode(node: sectionNode)] { sectionNode.layer.animatePosition(from: CGPoint(x: previousFrame.midX, y: previousFrame.midY + contentOffset.y), to: sectionNode.layer.position, duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction) } else { sectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) } } } else { for index in self.itemNodes.keys { if !existingItemIndices.contains(index) { self.removeItemNodeWithIndex(index) } } for wrappedSection in self.sectionNodes.keys { if !existingSections.contains(wrappedSection) { self.removeSectionNodeWithSection(wrappedSection) } } for itemNode in removedNodes { itemNode.removeFromSupernode() } } if shouldAnimateBounds { boundsTransition.animateBounds(layer: self.layer, from: previousBounds) } completion(self.displayedItemRange()) self.updateItemNodeVisibilititesAndScrolling() self.updateVisibleContentOffset() if let visibleItemsUpdated = self.visibleItemsUpdated { if presentationLayoutTransition.layout.items.count != 0 { let topIndex = presentationLayoutTransition.layout.items.first!.index let bottomIndex = presentationLayoutTransition.layout.items.last!.index var topVisible: (Int, GridItem) = (topIndex, self.items[topIndex]) let bottomVisible: (Int, GridItem) = (bottomIndex, self.items[bottomIndex]) let lowerDisplayBound = presentationLayoutTransition.layout.contentOffset.y + presentationLayoutTransition.layout.layout.insets.top //let upperDisplayBound = presentationLayoutTransition.layout.contentOffset.y + self.gridLayout.size.height for item in presentationLayoutTransition.layout.items { if lowerDisplayBound.isLess(than: item.frame.maxY) { topVisible = (item.index, self.items[item.index]) break } } var topSectionVisible: GridSection? for section in presentationLayoutTransition.layout.sections { if lowerDisplayBound.isLess(than: section.frame.maxY) { if self.itemLayout.items[topVisible.0].frame.minY > section.frame.minY { topSectionVisible = section.section } break } } visibleItemsUpdated(GridNodeVisibleItems(top: (topIndex, self.items[topIndex]), bottom: (bottomIndex, self.items[bottomIndex]), topVisible: topVisible, bottomVisible: bottomVisible, topSectionVisible: topSectionVisible, count: self.items.count)) } else { visibleItemsUpdated(GridNodeVisibleItems(top: nil, bottom: nil, topVisible: nil, bottomVisible: nil, topSectionVisible: nil, count: self.items.count)) } } if addedNodes { if let verticalIndicator = verticalIndicator, self.scrollView.subviews.last !== verticalIndicator { verticalIndicator.superview?.bringSubviewToFront(verticalIndicator) } } if let presentationLayoutUpdated = self.presentationLayoutUpdated { presentationLayoutUpdated(GridNodeCurrentPresentationLayout(layout: presentationLayoutTransition.layout.layout, contentOffset: presentationLayoutTransition.layout.contentOffset, contentSize: presentationLayoutTransition.layout.contentSize), updateLayoutTransition ?? presentationLayoutTransition.transition) } } private func addItemNode(index: Int, itemNode: GridItemNode, lowestSectionNode: ASDisplayNode?) { assert(self.itemNodes[index] == nil) self.itemNodes[index] = itemNode if itemNode.supernode == nil { if let lowestSectionNode = lowestSectionNode { self.insertSubnode(itemNode, belowSubnode: lowestSectionNode) } else { self.addSubnode(itemNode) } } } private func addSectionNode(section: WrappedGridSection, sectionNode: ASDisplayNode) { assert(self.sectionNodes[section] == nil) self.sectionNodes[section] = sectionNode if sectionNode.supernode == nil { self.addSubnode(sectionNode) } } private func removeItemNodeWithIndex(_ index: Int, removeNode: Bool = true) { if let itemNode = self.itemNodes.removeValue(forKey: index) { if removeNode { itemNode.removeFromSupernode() } } } private func removeSectionNodeWithSection(_ section: WrappedGridSection, removeNode: Bool = true) { if let sectionNode = self.sectionNodes.removeValue(forKey: section) { if removeNode { sectionNode.removeFromSupernode() } } } private func updateItemNodeVisibilititesAndScrolling() { let visibleRect = self.scrollView.bounds let isScrolling = self.scrollView.isDragging || self.scrollView.isDecelerating for (_, itemNode) in self.itemNodes { let visible = itemNode.frame.intersects(visibleRect) if itemNode.isVisibleInGrid != visible { itemNode.isVisibleInGrid = visible } if itemNode.isGridScrolling != isScrolling { itemNode.isGridScrolling = isScrolling } } } public func visibleContentOffset() -> GridNodeVisibleContentOffset { var offset: GridNodeVisibleContentOffset = .unknown if let supernode = self.supernode { var topItemIndexAndFrame: (Int, CGRect) = (-1, CGRect()) for index in self.itemNodes.keys.sorted() { let itemNode = self.itemNodes[index]! topItemIndexAndFrame = (index, supernode.convert(itemNode.bounds, from: itemNode)) break } if topItemIndexAndFrame.0 == 0 { offset = .known(self.scrollView.contentOffset.y + self.scrollView.contentInset.top) } else if topItemIndexAndFrame.0 == -1 { offset = .none } } return offset } private func updateVisibleContentOffset() { self.visibleContentOffsetChanged(self.visibleContentOffset()) } public func forEachItemNode(_ f: (ASDisplayNode) -> Void) { for (_, node) in self.itemNodes { f(node) } } public func forEachRow(_ f: ([ASDisplayNode]) -> Void) { var row: [ASDisplayNode] = [] var previousMinY: CGFloat? for index in self.itemNodes.keys.sorted() { let itemNode = self.itemNodes[index]! if let previousMinY = previousMinY, !previousMinY.isEqual(to: itemNode.frame.minY) { if !row.isEmpty { f(row) row.removeAll() } } previousMinY = itemNode.frame.minY row.append(itemNode) } if !row.isEmpty { f(row) } } public func itemNodeAtPoint(_ point: CGPoint) -> ASDisplayNode? { for (_, node) in self.itemNodes { if node.frame.contains(point) { return node } } return nil } } private func NH_LP_TABLE_LOOKUP(_ table: inout [Int], _ i: Int, _ j: Int, _ rowsize: Int) -> Int { return table[i * rowsize + j] } private func NH_LP_TABLE_LOOKUP_SET(_ table: inout [Int], _ i: Int, _ j: Int, _ rowsize: Int, _ value: Int) { table[i * rowsize + j] = value } private func linearPartitionTable(_ weights: [Int], numberOfPartitions: Int) -> [Int] { let n = weights.count let k = numberOfPartitions let tableSize = n * k; var tmpTable = Array(repeatElement(0, count: tableSize)) let solutionSize = (n - 1) * (k - 1) var solution = Array(repeatElement(0, count: solutionSize)) for i in 0 ..< n { let offset = i != 0 ? NH_LP_TABLE_LOOKUP(&tmpTable, i - 1, 0, k) : 0 NH_LP_TABLE_LOOKUP_SET(&tmpTable, i, 0, k, Int(weights[i]) + offset) } for j in 0 ..< k { NH_LP_TABLE_LOOKUP_SET(&tmpTable, 0, j, k, Int(weights[0])) } for i in 1 ..< n { for j in 1 ..< k { var currentMin = 0 var minX = Int.max for x in 0 ..< i { let c1 = NH_LP_TABLE_LOOKUP(&tmpTable, x, j - 1, k) let c2 = NH_LP_TABLE_LOOKUP(&tmpTable, i, 0, k) - NH_LP_TABLE_LOOKUP(&tmpTable, x, 0, k) let cost = max(c1, c2) if x == 0 || cost < currentMin { currentMin = cost; minX = x } } NH_LP_TABLE_LOOKUP_SET(&tmpTable, i, j, k, currentMin) NH_LP_TABLE_LOOKUP_SET(&solution, i - 1, j - 1, k - 1, minX) } } return solution } private func linearPartitionForWeights(_ weights: [Int], numberOfPartitions: Int) -> [[Int]] { var n = weights.count var k = numberOfPartitions if k <= 0 { return [] } if k >= n { var partition: [[Int]] = [] for weight in weights { partition.append([weight]) } return partition } if n == 1 { return [weights] } var solution = linearPartitionTable(weights, numberOfPartitions: numberOfPartitions) let solutionRowSize = numberOfPartitions - 1 k = k - 2; n = n - 1; var answer: [[Int]] = [] while k >= 0 { if n < 1 { answer.insert([], at: 0) } else { var currentAnswer: [Int] = [] var i = NH_LP_TABLE_LOOKUP(&solution, n - 1, k, solutionRowSize) + 1 let range = n + 1 while i < range { currentAnswer.append(weights[i]) i += 1 } answer.insert(currentAnswer, at: 0) n = NH_LP_TABLE_LOOKUP(&solution, n - 1, k, solutionRowSize) } k = k - 1 } var currentAnswer: [Int] = [] var i = 0 let range = n + 1 while i < range { currentAnswer.append(weights[i]) i += 1 } answer.insert(currentAnswer, at: 0) return answer }