diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bca331e..7b0d3199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag - Added `-[IGListSectionController didDeselectItemAtIndex:]` API to support default `UICollectionView` cell deselection. [Ryan Nystrom](https://github.com/rnystrom) (tbd) - Added `-[IGListCollectionContext selectItemAtIndex:]` Select an item through IGListCollectionContext like `-[IGListCollectionContext deselectItemAtIndex:]`. [Marvin Nazari](https://github.com/MarvinNazari) (tbd) +- Added horizontal scrolling support to `IGListCollectionViewLayout`. [Peter Edmonston](https://github.com/edmonston) [(#857)](https://github.com/Instagram/IGListKit/pull/857) + 3.0.0 ----- diff --git a/Source/IGListCollectionViewLayout.h b/Source/IGListCollectionViewLayout.h index b77913ba..558003a8 100644 --- a/Source/IGListCollectionViewLayout.h +++ b/Source/IGListCollectionViewLayout.h @@ -14,8 +14,8 @@ NS_ASSUME_NONNULL_BEGIN /** - This UICollectionViewLayout subclass is for vertically-scrolling lists of data with variable widths and heights. It - supports an infinite number of sections and items. All work is done on the main thread, and while extremely efficient, + This UICollectionViewLayout subclass is for vertically or horizontally scrolling lists of data with variable widths and + heights. It supports an infinite number of sections and items. All work is done on the main thread, and while extremely efficient, care must be taken not to stall the main thread in sizing delegate methods. This layout piggybacks on the mechanics of UICollectionViewFlowLayout in that: @@ -33,9 +33,9 @@ NS_ASSUME_NONNULL_BEGIN - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section; ``` - Sections and items are put into the same horizontal row until the max-x position of an item extends beyond the width - of the collection view. When that happens, the item is "newlined" to the next row. The y position of that row is - determined by the maximum height (including section insets) of the section/item of the previous row. + In a vertically scrolling layout, sections and items are put into the same horizontal row until the max-x position + of an item extends beyond the width of the collection view. When that happens, the item is "newlined" to the next row. + The y position of that row is determined by the maximum height (including section insets) of the section/item of the previous row. Ex. of a section (2,0) with a large width causing a newline. ``` @@ -64,6 +64,9 @@ NS_ASSUME_NONNULL_BEGIN Interitem spacing applies to items and sections within the same row. Line spacing only applies to items within the same section. + + In a horizontally scrolling layout, sections and items are flowed vertically until they need to be "newlined" to the + next column. Headers, if used, are stretched to the height of the collection view, minus the section insets. Please see the unit tests for more configuration examples and expected output. */ @@ -71,27 +74,49 @@ NS_SWIFT_NAME(ListCollectionViewLayout) @interface IGListCollectionViewLayout : UICollectionViewLayout /** - Set this to adjust the offset of the sticky headers. Can be used to change the sticky header position as UI like the - navigation bar is scrolled offscreen. Changing this to the height of the navigation bar will give the effect of the - headers sticking to the nav as it is collapsed. - - @discussion Changing the value on this method will invalidate the layout every time. + Direction in which layout will be scrollable; items will be flowed in the perpendicular direction, "newlining" when they + run out of space along that axis or when a non-zero header is found. */ -@property (nonatomic, assign) CGFloat stickyHeaderOriginYAdjustment; +@property (nonatomic, readonly) UICollectionViewScrollDirection scrollDirection; + +/** + Set this to adjust the offset of the sticky headers in the scrolling direction. Can be used to change the sticky + header position as UI like the navigation bar is scrolled offscreen. In a vertically scrolling layout, changing + this to the height of the navigation bar will give the effect of the headers sticking to the nav as it is collapsed. + + @note Changing the value on this method will invalidate the layout every time. + */ +@property (nonatomic, assign) CGFloat stickyHeaderYOffset; + /** Create and return a new collection view layout. @param stickyHeaders Set to `YES` to stick section headers to the top of the bounds while scrolling. - @param topContentInset The top content inset used to offset the sticky headers. Ignored if stickyHeaders is `NO`. - @param stretchToEdge Specifies whether to stretch width of last item to right edge when distance from last item to right edge < epsilon(1) + @param scrollDirection Direction along which the collection view will be scrollable (if content size exceeds the frame size) + @param topContentInset The content inset (top or left, depending on scrolling direction) used to offset the sticky headers. Ignored if stickyHeaders is `NO`. + @param stretchToEdge Specifies whether to stretch width (in vertically scrolling layout) or height (horizontally scrolling) of last item to right/bottom edge when distance from last item to right/bottom edge < epsilon(1) @return A new collection view layout. */ - (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders + scrollDirection:(UICollectionViewScrollDirection)scrollDirection topContentInset:(CGFloat)topContentInset stretchToEdge:(BOOL)stretchToEdge NS_DESIGNATED_INITIALIZER; +/** + Create and return a new vertically scrolling collection view layout. + + @param stickyHeaders Set to `YES` to stick section headers to the top of the bounds while scrolling. + @param topContentInset The top content inset used to offset the sticky headers. Ignored if stickyHeaders is `NO`. + @param stretchToEdge Specifies whether to stretch width of last item to right edge when distance from last item to right edge < epsilon(1) + + @return A new collection view layout. + */ +- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders + topContentInset:(CGFloat)topContentInset + stretchToEdge:(BOOL)stretchToEdge; + /** :nodoc: */ diff --git a/Source/IGListCollectionViewLayout.mm b/Source/IGListCollectionViewLayout.mm index f8cf3e55..37df6e50 100644 --- a/Source/IGListCollectionViewLayout.mm +++ b/Source/IGListCollectionViewLayout.mm @@ -14,6 +14,55 @@ #import +static CGFloat UIEdgeInsetsLeadingInsetInDirection(UIEdgeInsets insets, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return insets.top; + case UICollectionViewScrollDirectionHorizontal: return insets.left; + } +} + +static CGFloat UIEdgeInsetsTrailingInsetInDirection(UIEdgeInsets insets, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return insets.bottom; + case UICollectionViewScrollDirectionHorizontal: return insets.right; + } +} + +static CGFloat CGPointGetCoordinateInDirection(CGPoint point, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return point.y; + case UICollectionViewScrollDirectionHorizontal: return point.x; + } +} + +static CGFloat CGRectGetLengthInDirection(CGRect rect, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return rect.size.height; + case UICollectionViewScrollDirectionHorizontal: return rect.size.width; + } +} + +static CGFloat CGRectGetMaxInDirection(CGRect rect, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return CGRectGetMaxY(rect); + case UICollectionViewScrollDirectionHorizontal: return CGRectGetMaxX(rect); + } +} + +static CGFloat CGRectGetMinInDirection(CGRect rect, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return CGRectGetMinY(rect); + case UICollectionViewScrollDirectionHorizontal: return CGRectGetMinX(rect); + } +} + +static CGFloat CGSizeGetLengthInDirection(CGSize size, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return size.height; + case UICollectionViewScrollDirectionHorizontal: return size.width; + } +} + static NSIndexPath *headerIndexPathForSection(NSInteger section) { return [NSIndexPath indexPathForItem:0 inSection:section]; } @@ -101,7 +150,18 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut - (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders topContentInset:(CGFloat)topContentInset stretchToEdge:(BOOL)stretchToEdge { + return [self initWithStickyHeaders:stickyHeaders + scrollDirection:UICollectionViewScrollDirectionVertical + topContentInset:topContentInset + stretchToEdge:stretchToEdge]; +} + +- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders + scrollDirection:(UICollectionViewScrollDirection)scrollDirection + topContentInset:(CGFloat)topContentInset + stretchToEdge:(BOOL)stretchToEdge { if (self = [super init]) { + _scrollDirection = scrollDirection; _stickyHeaders = stickyHeaders; _topContentInset = topContentInset; _stretchToEdge = stretchToEdge; @@ -139,7 +199,8 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut // do not add zero height headers or headers that are outside the rect const CGRect frame = attributes.frame; const CGRect intersection = CGRectIntersection(frame, rect); - if (!CGRectIsEmpty(intersection) && CGRectGetHeight(frame) > 0.0) { + if (!CGRectIsEmpty(intersection) + && CGRectGetLengthInDirection(frame, self.scrollDirection) > 0.0) { [result addObject:attributes]; } } @@ -199,18 +260,26 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut UICollectionView *collectionView = self.collectionView; const IGListSectionEntry entry = _sectionData[section]; - const CGFloat minY = CGRectGetMinY(entry.bounds); + const CGFloat minOffset = CGRectGetMinInDirection(entry.bounds, self.scrollDirection); CGRect frame = entry.headerBounds; if (self.stickyHeaders) { - const CGFloat yOffset = collectionView.contentOffset.y + self.topContentInset + self.stickyHeaderOriginYAdjustment; + CGFloat offset = CGPointGetCoordinateInDirection(collectionView.contentOffset, self.scrollDirection) + self.topContentInset + self.stickyHeaderYOffset; if (section + 1 == _sectionData.size()) { - frame.origin.y = MAX(minY, yOffset); + offset = MAX(minOffset, offset); } else { - const CGFloat maxY = CGRectGetMinY(_sectionData[section + 1].bounds) - CGRectGetHeight(frame); - frame.origin.y = MIN(MAX(minY, yOffset), maxY); + const CGFloat maxOffset = CGRectGetMinInDirection(_sectionData[section + 1].bounds, self.scrollDirection) - CGRectGetLengthInDirection(frame, self.scrollDirection); + offset = MIN(MAX(minOffset, offset), maxOffset); + } + switch (self.scrollDirection) { + case UICollectionViewScrollDirectionVertical: + frame.origin.y = offset; + break; + case UICollectionViewScrollDirectionHorizontal: + frame.origin.x = offset; + break; } } @@ -231,11 +300,20 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut } const IGListSectionEntry section = _sectionData[sectionCount - 1]; - const CGFloat height = CGRectGetMaxY(section.bounds) + section.insets.bottom; - UICollectionView *collectionView = self.collectionView; const UIEdgeInsets contentInset = collectionView.contentInset; - return CGSizeMake(CGRectGetWidth(collectionView.bounds) - contentInset.left - contentInset.right, height); + + switch (self.scrollDirection) { + case UICollectionViewScrollDirectionVertical: { + const CGFloat height = CGRectGetMaxY(section.bounds) + section.insets.bottom; + return CGSizeMake(CGRectGetWidth(collectionView.bounds) - contentInset.left - contentInset.right, height); + } + case UICollectionViewScrollDirectionHorizontal: { + const CGFloat width = CGRectGetMaxX(section.bounds) + section.insets.right; + return CGSizeMake(width, CGRectGetHeight(collectionView.bounds) - contentInset.top - contentInset.bottom); + } + } + } - (void)invalidateLayoutWithContext:(IGListCollectionViewLayoutInvalidationContext *)context { @@ -278,7 +356,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut const CGRect oldBounds = self.collectionView.bounds; // if the y origin has changed, only invalidate when using sticky headers - if (CGRectGetMinY(newBounds) != CGRectGetMinY(oldBounds)) { + if (CGRectGetMinInDirection(newBounds, self.scrollDirection) != CGRectGetMinInDirection(oldBounds, self.scrollDirection)) { return self.stickyHeaders; } @@ -294,11 +372,11 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut #pragma mark - Public API -- (void)setStickyHeaderOriginYAdjustment:(CGFloat)stickyHeaderOriginYAdjustment { +- (void)setStickyHeaderYOffset:(CGFloat)stickyHeaderYOffset { IGAssertMainThread(); - if (_stickyHeaderOriginYAdjustment != stickyHeaderOriginYAdjustment) { - _stickyHeaderOriginYAdjustment = stickyHeaderOriginYAdjustment; + if (_stickyHeaderYOffset != stickyHeaderYOffset) { + _stickyHeaderYOffset = stickyHeaderYOffset; IGListCollectionViewLayoutInvalidationContext *invalidationContext = [IGListCollectionViewLayoutInvalidationContext new]; invalidationContext.ig_invalidateSupplementaryAttributes = YES; @@ -321,13 +399,13 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut const NSInteger sectionCount = [dataSource numberOfSectionsInCollectionView:collectionView]; const UIEdgeInsets contentInset = collectionView.contentInset; - const CGFloat width = CGRectGetWidth(collectionView.bounds) - contentInset.left - contentInset.right; - + const CGRect contentInsetAdjustedCollectionViewBounds = UIEdgeInsetsInsetRect(collectionView.bounds, contentInset); + auto sectionData = std::vector(sectionCount); - CGFloat itemY = 0.0; - CGFloat itemX = 0.0; - CGFloat nextRowY = 0.0; + CGFloat itemCoordInScrollDirection = 0.0; + CGFloat itemCoordInFixedDirection = 0.0; + CGFloat nextRowCoordInScrollDirection = 0.0; // union item frames and optionally the header to find a bounding box of the entire section CGRect rollingSectionBounds; @@ -341,63 +419,78 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut const CGFloat lineSpacing = [delegate collectionView:collectionView layout:self minimumLineSpacingForSectionAtIndex:section]; const CGFloat interitemSpacing = [delegate collectionView:collectionView layout:self minimumInteritemSpacingForSectionAtIndex:section]; - const CGFloat paddedWidth = width - insets.left - insets.right; - const BOOL headerExists = headerSize.height > 0; + const CGSize paddedCollectionViewSize = UIEdgeInsetsInsetRect(contentInsetAdjustedCollectionViewBounds, insets).size; + const UICollectionViewScrollDirection fixedDirection = self.scrollDirection == UICollectionViewScrollDirectionHorizontal ? UICollectionViewScrollDirectionVertical : UICollectionViewScrollDirectionHorizontal; + const CGFloat paddedLengthInFixedDirection = CGSizeGetLengthInDirection(paddedCollectionViewSize, fixedDirection); + const CGFloat headerLengthInScrollDirection = CGSizeGetLengthInDirection(headerSize, self.scrollDirection); + const BOOL headerExists = headerLengthInScrollDirection > 0; + + // start the section accounting for the header size + // header length in scroll direction is subtracted from the sectionBounds when calculating the header bounds after items are done + // this bumps the first row of items over enough to make room for the header + itemCoordInScrollDirection += headerLengthInScrollDirection; + nextRowCoordInScrollDirection += headerLengthInScrollDirection; + + // add the leading inset in fixed direction in case the section falls on the same row as the previous + // if the section is newlined then the coord in fixed direction is reset + itemCoordInFixedDirection += UIEdgeInsetsLeadingInsetInDirection(insets, fixedDirection); - // start the section y accounting for the header height - // header height is subtracted from the sectionBounds when calculating the header bounds after items are done - // this bumps the first row of items down enough to make room for the header - itemY += headerSize.height; - nextRowY += headerSize.height; - - // add the left inset in case the section falls on the same row as the previous - // if the section is newlined then the x is reset - itemX += insets.left; - - // the farthest right the frame of an item in this section can go - const CGFloat maxX = width - insets.right; + // the farthest in the fixed direction the frame of an item in this section can go + const CGFloat maxCoordinateInFixedDirection = CGRectGetLengthInDirection(contentInsetAdjustedCollectionViewBounds, fixedDirection) - UIEdgeInsetsTrailingInsetInDirection(insets, fixedDirection); for (NSInteger item = 0; item < itemCount; item++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section]; const CGSize size = [delegate collectionView:collectionView layout:self sizeForItemAtIndexPath:indexPath]; - IGAssert(size.width <= paddedWidth || fabs(size.width - paddedWidth) < FLT_EPSILON, - @"Width of item %zi in section %zi must be less than container %.0f accounting for section insets %@", - item, section, width, NSStringFromUIEdgeInsets(insets)); - CGFloat itemWidth = MIN(size.width, paddedWidth); + IGAssert(CGSizeGetLengthInDirection(size, fixedDirection) <= paddedLengthInFixedDirection + || fabs(CGSizeGetLengthInDirection(size, fixedDirection) - paddedLengthInFixedDirection) < FLT_EPSILON, + @"%@ of item %zi in section %zi must be less than container %.0f accounting for section insets %@", + self.scrollDirection == UICollectionViewScrollDirectionVertical ? @"Width" : @"Height", + item, section, CGRectGetLengthInDirection(contentInsetAdjustedCollectionViewBounds, fixedDirection), + NSStringFromUIEdgeInsets(insets)); - // if the x + width of the item busts the width of the container + CGFloat itemLengthInFixedDirection = MIN(CGSizeGetLengthInDirection(size, fixedDirection), paddedLengthInFixedDirection); + + // if the origin and length in fixed direction of the item busts the size of the container // or if this is the first item and the header has a non-zero size // newline to the next row and reset // define epsilon to avoid float overflow issue const CGFloat epsilon = 1.0; - if (itemX + itemWidth > maxX + epsilon + if (itemCoordInFixedDirection + itemLengthInFixedDirection > maxCoordinateInFixedDirection + epsilon || (item == 0 && headerExists)) { - itemY = nextRowY; - itemX = insets.left; + itemCoordInScrollDirection = nextRowCoordInScrollDirection; + itemCoordInFixedDirection = UIEdgeInsetsLeadingInsetInDirection(insets, fixedDirection); + // if newlining, always append line spacing unless its the very first item of the section if (item > 0) { - itemY += lineSpacing; + itemCoordInScrollDirection += lineSpacing; } } - const CGFloat distanceToRighEdge = paddedWidth - (itemX + itemWidth); - if (self.stretchToEdge && distanceToRighEdge > 0 && distanceToRighEdge <= epsilon) { - itemWidth = paddedWidth - itemX; + const CGFloat distanceToEdge = paddedLengthInFixedDirection - (itemCoordInFixedDirection + itemLengthInFixedDirection); + if (self.stretchToEdge && distanceToEdge > 0 && distanceToEdge <= epsilon) { + itemLengthInFixedDirection = paddedLengthInFixedDirection - itemCoordInFixedDirection; } - const CGRect frame = IGListRectIntegralScaled(CGRectMake(itemX, - itemY + insets.top, - itemWidth, - size.height)); + const CGRect rawFrame = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ? + CGRectMake(itemCoordInFixedDirection, + itemCoordInScrollDirection + insets.top, + itemLengthInFixedDirection, + size.height) : + CGRectMake(itemCoordInScrollDirection + insets.left, + itemCoordInFixedDirection, + size.width, + itemLengthInFixedDirection); + const CGRect frame = IGListRectIntegralScaled(rawFrame); + sectionData[section].itemBounds[item] = frame; - // track the max size of the row to find the y of the next row, adjust for top inset while iterating items - nextRowY = MAX(CGRectGetMaxY(frame) - insets.top, nextRowY); + // track the max size of the row to find the coord of the next row, adjust for leading inset while iterating items + nextRowCoordInScrollDirection = MAX(CGRectGetMaxInDirection(frame, self.scrollDirection) - UIEdgeInsetsLeadingInsetInDirection(insets, self.scrollDirection), nextRowCoordInScrollDirection); - // increase the rolling x by the item width and add item spacing for all items on the same row - itemX += itemWidth + interitemSpacing; + // increase the rolling coord in fixed direction appropriately and add item spacing for all items on the same row + itemCoordInFixedDirection += itemLengthInFixedDirection + interitemSpacing; // union the rolling section bounds if (item == 0) { @@ -407,10 +500,16 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut } } - const CGRect headerBounds = CGRectMake(insets.left, - CGRectGetMinY(rollingSectionBounds) - headerSize.height, - paddedWidth, - headerSize.height); + const CGRect headerBounds = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ? + CGRectMake(insets.left, + CGRectGetMinY(rollingSectionBounds) - headerSize.height, + paddedLengthInFixedDirection, + headerSize.height) : + CGRectMake(CGRectGetMinX(rollingSectionBounds) - headerSize.width, + insets.top, + headerSize.width, + paddedLengthInFixedDirection); + sectionData[section].headerBounds = headerBounds; // union the header before setting the bounds of the section @@ -422,11 +521,11 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut sectionData[section].bounds = rollingSectionBounds; sectionData[section].insets = insets; - // bump the x for the next section with the right insets - itemX += insets.right; + // bump the coord for the next section with the right insets + itemCoordInFixedDirection += UIEdgeInsetsTrailingInsetInDirection(insets, fixedDirection); - // find the lowest point in the section and add the bottom inset to find the next row's Y - nextRowY = MAX(nextRowY, CGRectGetMaxY(rollingSectionBounds) + insets.bottom); + // find the farthest point in the section and add the trailing inset to find the next row's coord + nextRowCoordInScrollDirection = MAX(nextRowCoordInScrollDirection, CGRectGetMaxInDirection(rollingSectionBounds, self.scrollDirection) + UIEdgeInsetsTrailingInsetInDirection(insets, self.scrollDirection)); } _sectionData = sectionData; diff --git a/Tests/IGListCollectionViewLayoutTests.m b/Tests/IGListCollectionViewLayoutTests.m index c7a78f5a..9c607b82 100644 --- a/Tests/IGListCollectionViewLayoutTests.m +++ b/Tests/IGListCollectionViewLayoutTests.m @@ -42,7 +42,11 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}}; } - (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset stretchToEdge:(BOOL)stretchToEdge { - self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:sticky topContentInset:inset stretchToEdge:stretchToEdge]; + [self setUpWithStickyHeaders:sticky scrollDirection:UICollectionViewScrollDirectionVertical topInset:inset stretchToEdge:stretchToEdge]; +} + +- (void)setUpWithStickyHeaders:(BOOL)sticky scrollDirection:(UICollectionViewScrollDirection)scrollDirection topInset:(CGFloat)inset stretchToEdge:(BOOL)stretchToEdge { + self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:sticky scrollDirection:scrollDirection topContentInset:inset stretchToEdge:stretchToEdge]; self.dataSource = [IGLayoutTestDataSource new]; self.collectionView = [[UICollectionView alloc] initWithFrame:kTestFrame collectionViewLayout:self.layout]; self.collectionView.dataSource = self.dataSource; @@ -74,7 +78,7 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}}; XCTAssertTrue(CGSizeEqualToSize(CGSizeZero, self.collectionView.contentSize)); } -- (void)test_whenLayingOutCells_withHeaderHeight_withLineSpacing_withInsets_thatFramesCorrect { +- (void)test_whenLayingOutCellsVertically_withHeaderHeight_withLineSpacing_withInsets_thatFramesCorrect { [self setUpWithStickyHeaders:NO topInset:0]; const CGFloat headerHeight = 10; @@ -106,6 +110,39 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}}; IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 85, 85, 30); } +-(void)test_whenLayingOutCellsHorizontally_withHeaderHeight_withLineSpacing_withInsets_thatFramesCorrect { + [self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO]; + + const CGFloat headerHeight = 10; + const CGFloat lineSpacing = 10; + const UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 5, 5); + + [self prepareWithData:@[ + [[IGLayoutTestSection alloc] initWithInsets:insets + lineSpacing:lineSpacing + interitemSpacing:0 + headerHeight:headerHeight + items:@[ + [[IGLayoutTestItem alloc] initWithSize:(CGSize){45,10}], + [[IGLayoutTestItem alloc] initWithSize:(CGSize){45,20}], + ]], + [[IGLayoutTestSection alloc] initWithInsets:insets + lineSpacing:lineSpacing + interitemSpacing:0 + headerHeight:headerHeight + items:@[ + [[IGLayoutTestItem alloc] initWithSize:(CGSize){45,30}], + ]], + ]]; + XCTAssertEqual(self.collectionView.contentSize.width, 140); + IGAssertEqualFrame([self headerForSection:0].frame, 10, 10, 10, 85); + IGAssertEqualFrame([self cellForSection:0 item:0].frame, 20, 10, 45, 10); + IGAssertEqualFrame([self cellForSection:0 item:1].frame, 20, 20, 45, 20); + IGAssertEqualFrame([self headerForSection:1].frame, 80, 10, 10, 85); + IGAssertEqualFrame([self cellForSection:1 item:0].frame, 90, 10, 45, 30); +} + + - (void)test_whenUsingStickyHeaders_withSimulatedScrolling_thatYPositionsAdjusted { [self setUpWithStickyHeaders:YES topInset:10]; @@ -142,6 +179,42 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}}; IGAssertEqualFrame([self headerForSection:1].frame, 0, 55, 100, 10); } +- (void)test_whenUsingStickyHeaders_withSimulatedHorizontalScrolling_thatXPositionsAdjusted { + [self setUpWithStickyHeaders:YES scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:10 stretchToEdge:NO]; + + [self prepareWithData:@[ + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:10 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:(CGSize){20,100}], + [[IGLayoutTestItem alloc] initWithSize:(CGSize){20,100}], + ]], + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:10 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:(CGSize){30,100}], + [[IGLayoutTestItem alloc] initWithSize:(CGSize){30,100}], + [[IGLayoutTestItem alloc] initWithSize:(CGSize){30,100}], + ]], + ]]; + + // scroll header 0 halfway + self.collectionView.contentOffset = CGPointMake(5, 0); + [self.collectionView layoutIfNeeded]; + IGAssertEqualFrame([self headerForSection:0].frame, 15, 0, 10, 100); + IGAssertEqualFrame([self headerForSection:1].frame, 50, 0, 10, 100); + + // scroll header 0 off and 1 left + self.collectionView.contentOffset = CGPointMake(45, 0); + [self.collectionView layoutIfNeeded]; + IGAssertEqualFrame([self headerForSection:0].frame, 40, 0, 10, 100); + IGAssertEqualFrame([self headerForSection:1].frame, 55, 0, 10, 100); +} + - (void)test_whenAdjustingTopYInset_withVaryingHeaderHeights_thatYPositionsUpdated { [self setUpWithStickyHeaders:YES topInset:10]; @@ -171,12 +244,12 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}}; IGAssertEqualFrame([self headerForSection:0].frame, 0, 30, 100, 10); IGAssertEqualFrame([self headerForSection:1].frame, 0, 45, 100, 10); - self.layout.stickyHeaderOriginYAdjustment = -10; + self.layout.stickyHeaderYOffset = -10; [self.collectionView layoutIfNeeded]; IGAssertEqualFrame([self headerForSection:0].frame, 0, 30, 100, 10); IGAssertEqualFrame([self headerForSection:1].frame, 0, 40, 100, 10); - self.layout.stickyHeaderOriginYAdjustment = 10; + self.layout.stickyHeaderYOffset = 10; [self.collectionView layoutIfNeeded]; IGAssertEqualFrame([self headerForSection:0].frame, 0, 30, 100, 10); IGAssertEqualFrame([self headerForSection:1].frame, 0, 55, 100, 10); @@ -231,7 +304,27 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}}; IGAssertEqualFrame([self cellForSection:0 item:2].frame, 67, 0, 33, 33); } -- (void)test_whenSectionsSmallerThanContainerWidth_with0ItemSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameRow { +- (void)test_whenItemsLargerThanContainerHeight_withHorizontalScrolling_with5PointItemSpacing_with0Insets_with10PointLineSpacing_thatItemsBumpToNewColumn { + [self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO]; + + [self prepareWithData:@[ + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:10 + interitemSpacing:5 + headerHeight:0 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}], + [[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}], + [[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}], + ]], + ]]; + XCTAssertEqual(self.collectionView.contentSize.width, 76); + IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); + IGAssertEqualFrame([self cellForSection:0 item:1].frame, 0, 38, 33, 33); + IGAssertEqualFrame([self cellForSection:0 item:2].frame, 43, 0, 33, 33); +} + +- (void)test_whenSectionsSmallerThanContainerWidth_withVerticalScrolling_with0ItemSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameRow { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ @@ -263,6 +356,38 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}}; IGAssertEqualFrame([self cellForSection:2 item:0].frame, 66, 0, 33, 33); } +- (void)test_whenSectionsSmallerThanContainerHeight_withHorizontalScrolling_with0ItemSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameColumn { + [self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO]; + + [self prepareWithData:@[ + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:0 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}], + ]], + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:0 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}], + ]], + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:0 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}], + ]], + ]]; + XCTAssertEqual(self.collectionView.contentSize.width, 33); + IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); + IGAssertEqualFrame([self cellForSection:1 item:0].frame, 0, 33, 33, 33); + IGAssertEqualFrame([self cellForSection:2 item:0].frame, 0, 66, 33, 33); +} + - (void)test_whenSectionsSmallerThanContainerWidth_withHalfPointSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameRow { [self setUpWithStickyHeaders:NO topInset:0]; @@ -384,6 +509,30 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}}; IGAssertEqualFrame([self cellForSection:1 item:0].frame, 0, 43, 33, 33); } +- (void)test_whenSectionsSmallerThanHeight_withHorizontalScrolling_withSectionHeader_thatHeaderCausesNewline { + [self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO]; + + [self prepareWithData:@[ + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:0 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}], + ]], + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:10 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}], + ]], + ]]; + XCTAssertEqual(self.collectionView.contentSize.width, 76); + IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); + IGAssertEqualFrame([self cellForSection:1 item:0].frame, 43, 0, 33, 33); +} + - (void)test_whenBatchItemUpdates_withHeaderHeight_withLineSpacing_withInsets_thatLayoutCorrectAfterUpdates { [self setUpWithStickyHeaders:NO topInset:0]; diff --git a/Tests/Objects/IGLayoutTestDataSource.m b/Tests/Objects/IGLayoutTestDataSource.m index fcfc3ba0..e19fac88 100644 --- a/Tests/Objects/IGLayoutTestDataSource.m +++ b/Tests/Objects/IGLayoutTestDataSource.m @@ -64,7 +64,7 @@ static NSString * const kHeaderIdentifier = @"header"; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { - return CGSizeMake(CGRectGetWidth(collectionView.bounds), self.sections[section].headerHeight); + return CGSizeMake(self.sections[section].headerHeight, self.sections[section].headerHeight); // Only the dimension along scrolling direction is used } @end