Skip to content

Commit

Permalink
Layout invalidation API
Browse files Browse the repository at this point in the history
Summary:
Adding a new layout-invalidation API, telling the layout object to query and rebuild the layout for all items in the section controller. This works with `UICollectionViewFlowLayout` and should work with other custom layouts (including our own).

Issue fixed: #360, #459

- [x] All tests pass. Demo project builds and runs.
- [x] I added tests, an experiment, or detailed why my change isn't tested.
- [x] I added an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes.
Closes #499

Reviewed By: jessesquires

Differential Revision: D4590274

Pulled By: rnystrom

fbshipit-source-id: f87235be4e6c024bf979b831a8938be68895e011
  • Loading branch information
Ryan Nystrom authored and facebook-github-bot committed Feb 21, 2017
1 parent 2adea72 commit 6bdcac8
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
})
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}()
Expand Down
21 changes: 21 additions & 0 deletions Source/IGListAdapter.m
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,27 @@ - (void)scrollToSectionController:(IGListSectionController<IGListSectionType> *)
[self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated];
}

- (void)invalidateLayoutForSectionController:(IGListSectionController<IGListSectionType> *)sectionController
completion:(void (^)(BOOL finished))completion{
const NSInteger section = [self sectionForSectionController:sectionController];
const NSInteger items = [_collectionView numberOfItemsInSection:section];

NSMutableArray<NSIndexPath *> *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 {
Expand Down
13 changes: 13 additions & 0 deletions Source/IGListCollectionContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,19 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)reloadSectionController:(IGListSectionController<IGListSectionType> *)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<IGListSectionType> *)sectionController
completion:(nullable void (^)(BOOL finished))completion;

/**
Batches and performs many cell-level updates in a single transaction.
Expand Down
4 changes: 4 additions & 0 deletions Source/IGListStackedSectionController.m
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ - (void)scrollToSectionController:(IGListSectionController<IGListSectionType> *)
animated:animated];
}

- (void)invalidateLayoutForSectionController:(IGListSectionController<IGListSectionType> *)sectionController completion:(void (^)(BOOL))completion {
[self.collectionContext invalidateLayoutForSectionController:self completion:completion];
}

#pragma mark - IGListDisplayDelegate

- (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController<IGListSectionType> *)sectionController cell:(UICollectionViewCell *)cell atIndex:(NSInteger)index {
Expand Down
28 changes: 28 additions & 0 deletions Tests/IGListAdapterE2ETests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions Tests/Objects/IGTestDelegateController.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion Tests/Objects/IGTestDelegateController.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ - (instancetype)init {
if (self = [super init]) {
_willDisplayCellIndexes = [NSCountedSet new];
_didEndDisplayCellIndexes = [NSCountedSet new];
_height = 10.0;
self.workingRangeDelegate = self;
}
return self;
Expand All @@ -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 {
Expand Down

0 comments on commit 6bdcac8

Please sign in to comment.