diff --git a/Source/IGListCollectionViewLayout.h b/Source/IGListCollectionViewLayout.h index 65ee1b18..d641adeb 100644 --- a/Source/IGListCollectionViewLayout.h +++ b/Source/IGListCollectionViewLayout.h @@ -83,10 +83,12 @@ NS_ASSUME_NONNULL_BEGIN 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) @return A new collection view layout. */ - (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders - topContentInset:(CGFloat)topContentInset NS_DESIGNATED_INITIALIZER; + topContentInset:(CGFloat)topContentInset + stretchToEdge:(BOOL)stretchToEdge NS_DESIGNATED_INITIALIZER; /** :nodoc: diff --git a/Source/IGListCollectionViewLayout.mm b/Source/IGListCollectionViewLayout.mm index 9d71dc3b..1d678564 100644 --- a/Source/IGListCollectionViewLayout.mm +++ b/Source/IGListCollectionViewLayout.mm @@ -76,6 +76,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut @property (nonatomic, assign, readonly) BOOL stickyHeaders; @property (nonatomic, assign, readonly) CGFloat topContentInset; +@property (nonatomic, assign, readonly) BOOL stretchToEdge; @end @@ -98,10 +99,12 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut } - (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders - topContentInset:(CGFloat)topContentInset { + topContentInset:(CGFloat)topContentInset + stretchToEdge:(BOOL)stretchToEdge { if (self = [super init]) { _stickyHeaders = stickyHeaders; _topContentInset = topContentInset; + _stretchToEdge = stretchToEdge; _attributesCache = [NSMutableDictionary new]; _headerAttributesCache = [NSMutableDictionary new]; _cachedLayoutInvalid = YES; @@ -110,7 +113,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut } - (instancetype)initWithCoder:(NSCoder *)aDecoder { - return [self initWithStickyHeaders:NO topContentInset:0]; + return [self initWithStickyHeaders:NO topContentInset:0 stretchToEdge:NO]; } #pragma mark - UICollectionViewLayout @@ -349,19 +352,21 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut // the farthest right the frame of an item in this section can go const CGFloat maxX = width - insets.right; - + 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, @"Width of item %zi in section %zi must be less than container %.0f accounting for section insets %@", item, section, width, NSStringFromUIEdgeInsets(insets)); - const CGFloat itemWidth = MIN(size.width, paddedWidth); + CGFloat itemWidth = MIN(size.width, paddedWidth); // if the x + width of the item busts the width 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 - if (itemX + itemWidth > maxX + // define epsilon to avoid float overflow issue + const CGFloat epsilon = 1.0; + if (itemX + itemWidth > maxX + epsilon || (item == 0 && headerExists)) { itemY = nextRowY; itemX = insets.left; @@ -371,6 +376,11 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut itemY += lineSpacing; } } + + const CGFloat distanceToRighEdge = paddedWidth - (itemX + itemWidth); + if (self.stretchToEdge && distanceToRighEdge > 0 && distanceToRighEdge <= epsilon) { + itemWidth = paddedWidth - itemX; + } const CGRect frame = IGListRectIntegralScaled(CGRectMake(itemX, itemY + insets.top, diff --git a/Tests/IGListCollectionViewLayoutTests.m b/Tests/IGListCollectionViewLayoutTests.m index b468cf3f..df764877 100644 --- a/Tests/IGListCollectionViewLayoutTests.m +++ b/Tests/IGListCollectionViewLayoutTests.m @@ -50,7 +50,11 @@ XCTAssertEqual(CGRectGetHeight(expected), CGRectGetHeight(frame)); \ } - (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset { - self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:sticky topContentInset:inset]; + [self setUpWithStickyHeaders:sticky topInset:inset stretchToEdge:NO]; +} + +- (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset stretchToEdge:(BOOL)stretchToEdge { + self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:sticky topContentInset:inset stretchToEdge:stretchToEdge]; self.dataSource = [IGLayoutTestDataSource new]; self.collectionView = [[UICollectionView alloc] initWithFrame:kTestFrame collectionViewLayout:self.layout]; self.collectionView.dataSource = self.dataSource; @@ -590,6 +594,81 @@ XCTAssertEqual(CGRectGetHeight(expected), CGRectGetHeight(frame)); \ IGAssertEqualFrame([self cellForSection:0 item:1].frame, 0, 20, 40, 20); } +- (void)test_whenItemsAddedWidthSmallerThanWidth_DifferenceSmallerThanEpsilon { + [self setUpWithStickyHeaders:NO topInset:0 stretchToEdge:YES]; + + const CGSize size = CGSizeMake(33, 33); + [self prepareWithData:@[ + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:0 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:size], + [[IGLayoutTestItem alloc] initWithSize:size], + [[IGLayoutTestItem alloc] initWithSize:size], + ]], + ]]; + + IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); + IGAssertEqualFrame([self cellForSection:0 item:1].frame, 33, 0, 33, 33); + IGAssertEqualFrame([self cellForSection:0 item:2].frame, 66, 0, 34, 33); +} + +- (void)test_whenItemsAddedWidthSmallerThanWidth_DifferenceBiggerThanEpsilon { + [self setUpWithStickyHeaders:NO topInset:0 stretchToEdge:YES]; + + [self prepareWithData:@[ + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:0 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(33, 33)], + [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(65, 33)], + ]], + ]]; + + IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); + IGAssertEqualFrame([self cellForSection:0 item:1].frame, 33, 0, 65, 33); +} + +- (void)test_whenItemsAddedWithBiggerThanWidth_DifferenceSmallerThanEpsilon { + [self setUpWithStickyHeaders:NO topInset:0]; + + [self prepareWithData:@[ + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:0 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(50, 50)], + [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(51, 50)], + ]], + ]]; + + IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 50, 50); + IGAssertEqualFrame([self cellForSection:0 item:1].frame, 50, 0, 51, 50); +} + +- (void)test_whenItemsAddedWithBiggerThanWidth_DifferenceBiggerThanEpsilon { + [self setUpWithStickyHeaders:NO topInset:0]; + + [self prepareWithData:@[ + [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero + lineSpacing:0 + interitemSpacing:0 + headerHeight:0 + items:@[ + [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(50, 50)], + [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(52, 50)], + ]], + ]]; + + IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 50, 50); + IGAssertEqualFrame([self cellForSection:0 item:1].frame, 0, 50, 52, 50); +} + - (void)test_ { [self setUpWithStickyHeaders:NO topInset:0]; self.collectionView.frame = CGRectMake(0, 0, 414, 736);