diff --git a/CollectionKitTests/FlowLayoutSpec.swift b/CollectionKitTests/FlowLayoutSpec.swift index ba52eac..d7f66fe 100644 --- a/CollectionKitTests/FlowLayoutSpec.swift +++ b/CollectionKitTests/FlowLayoutSpec.swift @@ -49,7 +49,7 @@ class FlowLayoutSpec: QuickSpec { it("should not display cells outside of the visible area") { let layout = FlowLayout().transposed() layout.mockLayout(parentSize: (100, 50), (50, 50), (50, 50), (50, 50), (50, 50)) - let visible = layout.visibleIndexes(visibleFrame: CGRect(x: 50, y: 0, width: 100, height: 50)) + let visible = layout.visible(in: CGRect(x: 50, y: 0, width: 100, height: 50)).indexes expect(visible).to(equal([1, 2])) } } diff --git a/CollectionKitTests/WaterfallLayoutSpec.swift b/CollectionKitTests/WaterfallLayoutSpec.swift index 62c409e..fd68e29 100644 --- a/CollectionKitTests/WaterfallLayoutSpec.swift +++ b/CollectionKitTests/WaterfallLayoutSpec.swift @@ -46,7 +46,7 @@ class WaterfallLayoutSpec: QuickSpec { it("should not display cells outside of the visible area") { let layout = WaterfallLayout() layout.mockLayout(parentSize: (100, 50), (50, 50), (50, 50), (50, 50), (50, 50)) - let visible = layout.visibleIndexes(visibleFrame: CGRect(x: 0, y: 50, width: 100, height: 50)) + let visible = layout.visible(in: CGRect(x: 0, y: 50, width: 100, height: 50)).indexes expect(visible).to(equal([2, 3])) } } diff --git a/Sources/CollectionView.swift b/Sources/CollectionView.swift index dbe0fbe..b45db33 100644 --- a/Sources/CollectionView.swift +++ b/Sources/CollectionView.swift @@ -158,7 +158,7 @@ open class CollectionView: UIScrollView { } private func _loadCells(forceReload: Bool) { - let newIndexes = flattenedProvider.visibleIndexes(visibleFrame: visibleFrame) + let newIndexes = flattenedProvider.visible(in: visibleFrame).indexes // optimization: we assume that corresponding identifier for each index doesnt change unless forceReload is true. guard forceReload || diff --git a/Sources/Layout/InsetLayout.swift b/Sources/Layout/InsetLayout.swift index 71f21df..893bdb9 100644 --- a/Sources/Layout/InsetLayout.swift +++ b/Sources/Layout/InsetLayout.swift @@ -55,8 +55,8 @@ open class InsetLayout: WrapperLayout { rootLayout.layout(context: InsetLayoutContext(original: context, insets: insets)) } - open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { - return rootLayout.visibleIndexes(visibleFrame: visibleFrame.inset(by: -insets)) + open override func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { + return rootLayout.visible(in: visibleFrame.inset(by: -insets)) } open override func frame(at: Int) -> CGRect { diff --git a/Sources/Layout/Layout.swift b/Sources/Layout/Layout.swift index 7211c7a..ecb3543 100644 --- a/Sources/Layout/Layout.swift +++ b/Sources/Layout/Layout.swift @@ -22,7 +22,7 @@ open class Layout { fatalError("Subclass should provide its own layout") } - open func visibleIndexes(visibleFrame: CGRect) -> [Int] { + open func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { fatalError("Subclass should provide its own layout") } @@ -38,6 +38,7 @@ extension Layout { return InsetLayout(self, insets: insets) } + /// Visible insets in the opposite of the layout direction are doubled public func insetVisibleFrame(by insets: UIEdgeInsets) -> VisibleFrameInsetLayout { return VisibleFrameInsetLayout(self, insets: insets) } diff --git a/Sources/Layout/SimpleLayout.swift b/Sources/Layout/SimpleLayout.swift index bba9152..07df7a8 100644 --- a/Sources/Layout/SimpleLayout.swift +++ b/Sources/Layout/SimpleLayout.swift @@ -36,14 +36,14 @@ open class SimpleLayout: Layout { return frames[at] } - open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { + open override func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { var result = [Int]() for (i, frame) in frames.enumerated() { if frame.intersects(visibleFrame) { result.append(i) } } - return result + return (result, visibleFrame) } } @@ -54,7 +54,14 @@ open class VerticalSimpleLayout: SimpleLayout { maxFrameLength = frames.max { $0.height < $1.height }?.height ?? 0 } - open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { + open override func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { + guard !visibleFrame.isEmptyOrNegative else { + // When this vertical layout gets called in a + // section provider with horizontal layout we need + // to guard here because the optimised index search + // here doesn't take the X axes into account. + return ([], visibleFrame) + } var index = frames.binarySearch { $0.minY < visibleFrame.minY - maxFrameLength } var visibleIndexes = [Int]() while index < frames.count { @@ -67,7 +74,7 @@ open class VerticalSimpleLayout: SimpleLayout { } index += 1 } - return visibleIndexes + return (visibleIndexes, visibleFrame) } } @@ -78,7 +85,10 @@ open class HorizontalSimpleLayout: SimpleLayout { maxFrameLength = frames.max { $0.width < $1.width }?.width ?? 0 } - open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { + open override func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { + guard !visibleFrame.isEmptyOrNegative else { + return ([], visibleFrame) + } var index = frames.binarySearch { $0.minX < visibleFrame.minX - maxFrameLength } var visibleIndexes = [Int]() while index < frames.count { @@ -91,6 +101,14 @@ open class HorizontalSimpleLayout: SimpleLayout { } index += 1 } - return visibleIndexes + return (visibleIndexes, visibleFrame) + } +} + +extension CGRect { + /// Returns whether a rectangle has zero or less + /// width or height, or is a null rectangle. + var isEmptyOrNegative: Bool { + return isEmpty || size.width < 0 || size.height < 0 } } diff --git a/Sources/Layout/StickyLayout.swift b/Sources/Layout/StickyLayout.swift index 8f86cb2..39b5e6a 100644 --- a/Sources/Layout/StickyLayout.swift +++ b/Sources/Layout/StickyLayout.swift @@ -34,17 +34,19 @@ public class StickyLayout: WrapperLayout { } } - public override func visibleIndexes(visibleFrame: CGRect) -> [Int] { + // TODO: Fix for the new FlattenedProvider.visible(in:) + public override func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { self.visibleFrame = visibleFrame topFrameIndex = stickyFrames.binarySearch { $0.frame.minY < visibleFrame.minY } - 1 if let index = stickyFrames.get(topFrameIndex)?.index, index >= 0 { - var oldVisible = rootLayout.visibleIndexes(visibleFrame: visibleFrame) - if let index = oldVisible.index(of: index) { - oldVisible.remove(at: index) + var oldVisible = rootLayout.visible(in: visibleFrame) + if let index = oldVisible.indexes.index(of: index) { + oldVisible.indexes.remove(at: index) } - return oldVisible + [index] + oldVisible.indexes += [index] + return oldVisible } - return rootLayout.visibleIndexes(visibleFrame: visibleFrame) + return rootLayout.visible(in: visibleFrame) } public override func frame(at: Int) -> CGRect { diff --git a/Sources/Layout/TransposeLayout.swift b/Sources/Layout/TransposeLayout.swift index 59bcf07..5825404 100644 --- a/Sources/Layout/TransposeLayout.swift +++ b/Sources/Layout/TransposeLayout.swift @@ -37,8 +37,9 @@ open class TransposeLayout: WrapperLayout { rootLayout.layout(context: TransposeLayoutContext(original: context)) } - open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { - return rootLayout.visibleIndexes(visibleFrame: visibleFrame.transposed) + open override func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { + let visible = rootLayout.visible(in: visibleFrame.transposed) + return (visible.indexes, visible.frame.transposed) } open override func frame(at: Int) -> CGRect { diff --git a/Sources/Layout/VisibleFrameInsetLayout.swift b/Sources/Layout/VisibleFrameInsetLayout.swift index cafd98c..6ab3755 100644 --- a/Sources/Layout/VisibleFrameInsetLayout.swift +++ b/Sources/Layout/VisibleFrameInsetLayout.swift @@ -30,7 +30,7 @@ open class VisibleFrameInsetLayout: WrapperLayout { super.layout(context: context) } - open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { - return rootLayout.visibleIndexes(visibleFrame: visibleFrame.inset(by: insets)) + open override func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { + return rootLayout.visible(in: visibleFrame.inset(by: insets)) } } diff --git a/Sources/Layout/WrapperLayout.swift b/Sources/Layout/WrapperLayout.swift index 1163058..a7a40d1 100644 --- a/Sources/Layout/WrapperLayout.swift +++ b/Sources/Layout/WrapperLayout.swift @@ -23,8 +23,8 @@ open class WrapperLayout: Layout { rootLayout.layout(context: context) } - open override func visibleIndexes(visibleFrame: CGRect) -> [Int] { - return rootLayout.visibleIndexes(visibleFrame: visibleFrame) + open override func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { + return rootLayout.visible(in: visibleFrame) } open override func frame(at: Int) -> CGRect { diff --git a/Sources/Protocol/LayoutableProvider.swift b/Sources/Protocol/LayoutableProvider.swift index e57ca19..c890536 100644 --- a/Sources/Protocol/LayoutableProvider.swift +++ b/Sources/Protocol/LayoutableProvider.swift @@ -21,8 +21,8 @@ extension LayoutableProvider where Self: Provider { public func layout(collectionSize: CGSize) { internalLayout.layout(context: layoutContext(collectionSize: collectionSize)) } - public func visibleIndexes(visibleFrame: CGRect) -> [Int] { - return internalLayout.visibleIndexes(visibleFrame: visibleFrame) + public func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { + return internalLayout.visible(in: visibleFrame) } public var contentSize: CGSize { return internalLayout.contentSize diff --git a/Sources/Protocol/Provider.swift b/Sources/Protocol/Provider.swift index eefd073..6013cc7 100644 --- a/Sources/Protocol/Provider.swift +++ b/Sources/Protocol/Provider.swift @@ -17,7 +17,7 @@ public protocol Provider { // layout func layout(collectionSize: CGSize) - func visibleIndexes(visibleFrame: CGRect) -> [Int] + func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) var contentSize: CGSize { get } func frame(at: Int) -> CGRect diff --git a/Sources/Provider/EmptyCollectionProvider.swift b/Sources/Provider/EmptyCollectionProvider.swift index 9554bfb..a7b3c41 100644 --- a/Sources/Provider/EmptyCollectionProvider.swift +++ b/Sources/Provider/EmptyCollectionProvider.swift @@ -33,8 +33,8 @@ open class EmptyCollectionProvider: ItemProvider, CollectionReloadable { open func frame(at: Int) -> CGRect { return .zero } - open func visibleIndexes(visibleFrame: CGRect) -> [Int] { - return [Int]() + open func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { + return ([Int](), visibleFrame) } open func animator(at: Int) -> Animator? { diff --git a/Sources/Provider/FlattenedProvider.swift b/Sources/Provider/FlattenedProvider.swift index a5d36f4..d726c0c 100644 --- a/Sources/Provider/FlattenedProvider.swift +++ b/Sources/Provider/FlattenedProvider.swift @@ -84,23 +84,33 @@ struct FlattenedProvider: ItemProvider { return provider.contentSize } - func visibleIndexes(visibleFrame: CGRect) -> [Int] { - var visible = [Int]() - for sectionIndex in provider.visibleIndexes(visibleFrame: visibleFrame) { - let beginIndex = childSections[sectionIndex].beginIndex - if let sectionData = childSections[sectionIndex].sectionData { - let sectionFrame = provider.frame(at: sectionIndex) - let intersectFrame = visibleFrame.intersection(sectionFrame) - let visibleFrameForCell = CGRect(origin: intersectFrame.origin - sectionFrame.origin, size: intersectFrame.size) - let sectionVisible = sectionData.visibleIndexes(visibleFrame: visibleFrameForCell) - for item in sectionVisible { - visible.append(item + beginIndex) - } - } else { - visible.append(beginIndex) - } + func visible(in visibleFrame: CGRect) -> (indexes: [Int], frame: CGRect) { + let visible = provider.visible(in: visibleFrame) + // sort child sections by the indexes from layout + let sections = Array(0.. [Int] in + let section = provider.frame(at: index) + // intersection that doesn't return invalid frame when the + // rects don't intersect but rather returns a rect spanning + // the part of the border of the visible frame where the + // section frame would enter the visible frame if it + // were closer. This allows for the section to add its + // visible inset to that rect and show according to that. + // Calculation source https://math.stackexchange.com/a/99576 + let x = max(0, visible.frame.origin.x - section.origin.x) + let y = max(0, visible.frame.origin.y - section.origin.y) + let maxTop = max(visible.frame.origin.y, section.origin.y) + let maxLeft = max(visible.frame.origin.x, section.origin.x) + let minBottom = min(visible.frame.maxY, section.maxY) + let minRight = min(visible.frame.maxX, section.maxX) + let visibleFrameForCell = CGRect(x: x, y: y, width: minRight - maxLeft, height: minBottom - maxTop) + let child = childSections[index] + let childVisible = child.sectionData?.visible(in: visibleFrameForCell) + let childIndexes = childVisible?.indexes ?? [0] + return childIndexes.map { $0 + child.beginIndex } } - return visible + return (indexes, visible.frame) } func frame(at: Int) -> CGRect { @@ -138,3 +148,18 @@ struct FlattenedProvider: ItemProvider { return provider.hasReloadable(reloadable) } } + +extension CGPoint { + static var infinity: CGPoint = CGPoint(x: CGFloat.infinity, y: CGFloat.infinity) +} + +extension Array where Element: Equatable { + + /// Returns an array sorted based on another array + /// + /// - Parameter array: The array with items that we sort bt + /// - Returns: An array sorted by elements in parameter array. + func sorted(by array: [Element]) -> [Element] { + return array.filter { contains($0) } + filter { !array.contains($0) } + } +}