diff --git a/CHANGELOG.md b/CHANGELOG.md index b2a14c54e..cdb81618f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ This release closes the [3.0.0 milestone](https://github.com/Instagram/IGListKit - You can now manually move items (cells) within a section controller, ex: `[self.collectionContext moveInSectionController:self fromIndex:0 toIndex:1]`. [Ryan Nystrom](https://github.com/rnystrom) [(#418)](https://github.com/Instagram/IGListKit/pull/418) +- Invalidate the layout of a section controller and control the transition with `UIView` animation APIs. [Ryan Nystrom](https://github.com/rnystrom) [(#499)](https://github.com/Instagram/IGListKit/pull/499) + ### Fixes - Gracefully handle a `nil` section controller returned by an `IGListAdapterDataSource`. [Ryan Nystrom](https://github.com/rnystrom) [(tbd)](https://github.com/Instagram/IGListKit/pull/tbd) diff --git a/Examples/Examples-iOS/IGListKitExamples/SectionControllers/ExpandableSectionController.swift b/Examples/Examples-iOS/IGListKitExamples/SectionControllers/ExpandableSectionController.swift index 5ac451a0b..df47c1727 100644 --- a/Examples/Examples-iOS/IGListKitExamples/SectionControllers/ExpandableSectionController.swift +++ b/Examples/Examples-iOS/IGListKitExamples/SectionControllers/ExpandableSectionController.swift @@ -32,7 +32,6 @@ final class ExpandableSectionController: IGListSectionController, IGListSectionT func cellForItem(at index: Int) -> UICollectionViewCell { let cell = collectionContext!.dequeueReusableCell(of: LabelCell.self, for: self, at: index) as! LabelCell - cell.label.numberOfLines = expanded ? 0 : 1 cell.label.text = object return cell } @@ -43,7 +42,14 @@ final class ExpandableSectionController: IGListSectionController, IGListSectionT func didSelectItem(at index: Int) { expanded = !expanded - collectionContext?.reload(in: self, at: IndexSet(integer: 0)) + UIView.animate(withDuration: 0.5, + delay: 0, + usingSpringWithDamping: 0.4, + initialSpringVelocity: 0.6, + options: [], + animations: { + self.collectionContext?.invalidateLayout(for: self) + }) } } diff --git a/Examples/Examples-iOS/IGListKitExamples/Views/LabelCell.swift b/Examples/Examples-iOS/IGListKitExamples/Views/LabelCell.swift index 25bb77dc7..fee2f730f 100644 --- a/Examples/Examples-iOS/IGListKitExamples/Views/LabelCell.swift +++ b/Examples/Examples-iOS/IGListKitExamples/Views/LabelCell.swift @@ -34,7 +34,7 @@ class LabelCell: UICollectionViewCell { let label: UILabel = { let label = UILabel() label.backgroundColor = .clear - label.numberOfLines = 1 + label.numberOfLines = 0 label.font = LabelCell.font return label }() diff --git a/Source/IGListAdapter.m b/Source/IGListAdapter.m index 7b9b0a087..495094162 100644 --- a/Source/IGListAdapter.m +++ b/Source/IGListAdapter.m @@ -1104,6 +1104,27 @@ - (void)scrollToSectionController:(IGListSectionController *) [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; } +- (void)invalidateLayoutForSectionController:(IGListSectionController *)sectionController + completion:(void (^)(BOOL finished))completion{ + const NSInteger section = [self sectionForSectionController:sectionController]; + const NSInteger items = [_collectionView numberOfItemsInSection:section]; + + NSMutableArray *indexPaths = [NSMutableArray new]; + for (NSInteger item = 0; item < items; item++) { + [indexPaths addObject:[NSIndexPath indexPathForItem:item inSection:section]]; + } + + UICollectionViewLayout *layout = _collectionView.collectionViewLayout; + UICollectionViewLayoutInvalidationContext *context = [[[layout.class invalidationContextClass] alloc] init]; + [context invalidateItemsAtIndexPaths:indexPaths]; + + void (^block)() = ^{ + [layout invalidateLayoutWithContext:context]; + }; + + [_collectionView performBatchUpdates:block completion:completion]; +} + #pragma mark - UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { diff --git a/Source/IGListCollectionContext.h b/Source/IGListCollectionContext.h index e7ced324b..80672d722 100644 --- a/Source/IGListCollectionContext.h +++ b/Source/IGListCollectionContext.h @@ -218,6 +218,19 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadSectionController:(IGListSectionController *)sectionController; +/** + Invalidate the backing `UICollectionViewLayout` for all items in the section controller. + + @param sectionController The section controller that needs invalidating. + @param completion An optional completion block to execute when the updates are finished. + + @note This method can be wrapped in `UIView` animation APIs to control the duration or perform without animations. This + will end up calling `-[UICollectionView performBatchUpdates:completion:] internally, so invalidated changes may not be + reflected in the cells immediately. + */ +- (void)invalidateLayoutForSectionController:(IGListSectionController *)sectionController + completion:(nullable void (^)(BOOL finished))completion; + /** Batches and performs many cell-level updates in a single transaction. diff --git a/Source/IGListStackedSectionController.m b/Source/IGListStackedSectionController.m index 9561a9b2d..90e13843e 100644 --- a/Source/IGListStackedSectionController.m +++ b/Source/IGListStackedSectionController.m @@ -311,6 +311,10 @@ - (void)scrollToSectionController:(IGListSectionController *) animated:animated]; } +- (void)invalidateLayoutForSectionController:(IGListSectionController *)sectionController completion:(void (^)(BOOL))completion { + [self.collectionContext invalidateLayoutForSectionController:self completion:completion]; +} + #pragma mark - IGListDisplayDelegate - (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController *)sectionController cell:(UICollectionViewCell *)cell atIndex:(NSInteger)index { diff --git a/Tests/IGListAdapterE2ETests.m b/Tests/IGListAdapterE2ETests.m index adee84f4b..d1dc51604 100644 --- a/Tests/IGListAdapterE2ETests.m +++ b/Tests/IGListAdapterE2ETests.m @@ -1337,4 +1337,32 @@ - (void)test_whenMovingItems_withNoBatchUpdate_thatCollectionViewWorks { XCTAssertEqualObjects(movedCell2.label.text, @"foo"); } +- (void)test_whenInvalidatingSectionController_withSizeChange_thatCellsAreSameInstance_thatCellsFrameChanged { + [self setupWithObjects:@[ + genTestObject(@1, @2), + ]]; + + NSIndexPath *path1 = [NSIndexPath indexPathForItem:0 inSection:0]; + NSIndexPath *path2 = [NSIndexPath indexPathForItem:1 inSection:0]; + IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:path1]; + IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:path2]; + + XCTAssertEqual(cell1.frame.size.height, 10); + XCTAssertEqual(cell2.frame.size.height, 10); + + IGTestDelegateController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; + section.height = 20.0; + + XCTestExpectation *expectation = genExpectation; + [section.collectionContext invalidateLayoutForSectionController:section completion:^(BOOL finished) { + XCTAssertEqual(cell1, [self.collectionView cellForItemAtIndexPath:path1]); + XCTAssertEqual(cell2, [self.collectionView cellForItemAtIndexPath:path2]); + XCTAssertEqual(cell1.frame.size.height, 20); + XCTAssertEqual(cell2.frame.size.height, 20); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:15 handler:nil]; +} + @end diff --git a/Tests/Objects/IGTestDelegateController.h b/Tests/Objects/IGTestDelegateController.h index 87acf6b1f..2da61bd6d 100644 --- a/Tests/Objects/IGTestDelegateController.h +++ b/Tests/Objects/IGTestDelegateController.h @@ -17,6 +17,8 @@ @property (nonatomic, strong, readonly) IGTestObject *item; +@property (nonatomic, assign) CGFloat height; + @property (nonatomic, copy) void (^itemUpdateBlock)(); @property (nonatomic, copy) void (^cellConfigureBlock)(IGTestDelegateController *); @property (nonatomic, assign, readonly) NSInteger updateCount; diff --git a/Tests/Objects/IGTestDelegateController.m b/Tests/Objects/IGTestDelegateController.m index 00dcdca7e..bedae4870 100644 --- a/Tests/Objects/IGTestDelegateController.m +++ b/Tests/Objects/IGTestDelegateController.m @@ -18,6 +18,7 @@ - (instancetype)init { if (self = [super init]) { _willDisplayCellIndexes = [NSCountedSet new]; _didEndDisplayCellIndexes = [NSCountedSet new]; + _height = 10.0; self.workingRangeDelegate = self; } return self; @@ -31,7 +32,7 @@ - (NSInteger)numberOfItems { } - (CGSize)sizeForItemAtIndex:(NSInteger)index { - return CGSizeMake(self.collectionContext.containerSize.width, 10); + return CGSizeMake(self.collectionContext.containerSize.width, self.height); } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index {