created IGListCollectionView

Summary: * IGListCollectionView passes update information to IGListCollectionViewLayout.

Reviewed By: rnystrom

Differential Revision: D6510139

fbshipit-source-id: ada194d009eca7f308887ec592ecc41fb9557855
This commit is contained in:
Maxime Ollivier 2017-12-15 09:19:24 -08:00 committed by Facebook Github Bot
parent f0b947cd1f
commit 23bcc5c7c0
6 changed files with 355 additions and 1 deletions

View file

@ -0,0 +1,44 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <UIKit/UIKit.h>
@class IGListCollectionViewLayout;
NS_ASSUME_NONNULL_BEGIN
/**
This `UICollectionView` subclass allows for partial layout invalidation using `IGListCollectionViewLayout`.
@note When updating a collection view (ex: calling `-insertSections`), `-invalidateLayoutWithContext` gets called on
the layout object. However, the invalidation context doesn't provide details on which index paths are being modified,
which typically forces a full layout re-calculation. `IGListCollectionView` gives `IGListCollectionViewLayout` the
missing information to re-calculate only the modified layout attributes.
*/
NS_SWIFT_NAME(ListCollectionView)
@interface IGListCollectionView : UICollectionView
/**
Create a new view with an `IGListcollectionViewLayout` class or subclass.
@param frame The frame to initialize with.
@param collectionViewLayout The layout to use with the collection view.
@note You can initialize a new view with a base layout by simply calling `-[IGListCollectionView initWithFrame:]`.
*/
- (instancetype)initWithFrame:(CGRect)frame listCollectionViewLayout:(IGListCollectionViewLayout *)collectionViewLayout NS_DESIGNATED_INITIALIZER;
/**
:nodoc:
*/
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)collectionViewLayout NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,106 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "IGListCollectionView.h"
#import "IGListCollectionViewLayout.h"
@implementation IGListCollectionView
#pragma mark - Init
- (instancetype)initWithFrame:(CGRect)frame {
IGListCollectionViewLayout *layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:NO topContentInset:0 stretchToEdge:YES];
return [self initWithFrame:frame listCollectionViewLayout:layout];
}
- (instancetype)initWithFrame:(CGRect)frame listCollectionViewLayout:(IGListCollectionViewLayout *)collectionViewLayout {
return [super initWithFrame:frame collectionViewLayout:collectionViewLayout];
}
#pragma mark - IGListCollectionViewLayout
- (IGListCollectionViewLayout *)listLayout {
if ([self.collectionViewLayout isKindOfClass:[IGListCollectionViewLayout class]]) {
return (IGListCollectionViewLayout *)self.collectionViewLayout;
}
return nil;
}
#pragma mark - Overides reloads
- (void)reloadItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
[self didModifyIndexPaths:indexPaths];
[super reloadItemsAtIndexPaths:indexPaths];
}
- (void)reloadSections:(NSIndexSet *)sections {
[self didModifySections:sections];
[super reloadSections:sections];
}
#pragma mark - Override deletes
- (void)deleteItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
[self didModifyIndexPaths:indexPaths];
[super deleteItemsAtIndexPaths:indexPaths];
}
- (void)deleteSections:(NSIndexSet *)sections {
[self didModifySections:sections];
[super deleteSections:sections];
}
#pragma mark - Override inserts
- (void)insertItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
[self didModifyIndexPaths:indexPaths];
[super insertItemsAtIndexPaths:indexPaths];
}
- (void)insertSections:(NSIndexSet *)sections {
[self didModifySections:sections];
[super insertSections:sections];
}
#pragma mark - Override moves
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath {
[self didModifyIndexPaths:@[indexPath, newIndexPath]];
[super moveItemAtIndexPath:indexPath toIndexPath:newIndexPath];
}
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection {
[self didModifySection:MIN(section, newSection)];
[super moveSection:section toSection:newSection];
}
#pragma mark - Modify section
- (void)didModifySections:(NSIndexSet *)sections {
if (sections.count == 0) {
return;
}
[self didModifySection:sections.firstIndex];
}
- (void)didModifySection:(NSUInteger)section {
[self.listLayout didModifySection:section];
}
#pragma mark - Modified index path
- (void)didModifyIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
for (NSIndexPath *indexPath in indexPaths) {
[self didModifySection:indexPath.section];
}
}
@end

View file

@ -0,0 +1,189 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <XCTest/XCTest.h>
#import <IGListKit/IGListCollectionView.h>
#import "IGLayoutTestItem.h"
#import "IGLayoutTestSection.h"
#import "IGLayoutTestDataSource.h"
#import "IGListTestHelpers.h"
@interface IGListCollectionViewTests : XCTestCase
@property (nonatomic, strong) IGListCollectionView *collectionView;
@property (nonatomic, strong) IGLayoutTestDataSource *dataSource;
@end
@implementation IGListCollectionViewTests
- (void)setUp {
[super setUp];
self.dataSource = [IGLayoutTestDataSource new];
self.collectionView = [[IGListCollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
self.collectionView.dataSource = self.dataSource;
self.collectionView.delegate = self.dataSource;
[self.dataSource configCollectionView:self.collectionView];
}
#pragma mark - Reload All
- (void)test_whenReloadData_thatEntireLayoutUpdates {
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))])
];
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))])
];
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 20, 20);
IGAssertEqualFrame([self cellForSection:1 item:0].frame, 20, 0, 10, 10);
}
#pragma mark - Insert/Delete/Reload/Move
- (void)test_whenInsertingSection_thatLayoutPartiallyUpdates {
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))])
];
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))])
];
[self.collectionView insertSections:[NSIndexSet indexSetWithIndex:1]];
// check that section 0 wasn't updated
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 10, 10);
// check that section 1 was updated
IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 0, 10, 10);
}
- (void)test_whenDeletingSection_thatLayoutPartiallyUpdates {
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))])
];
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]),
];
[self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:1]];
// check that section 0 wasn't updated
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 10, 10);
}
- (void)test_whenReloadingSection_thatLayoutPartiallyUpdates {
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))])
];
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]),
];
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:1]];
// check that section 0 wasn't updated
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 10, 10);
// check that section 1 was updated
IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 0, 20, 20);
}
- (void)test_whenMoveSection_thatLayoutPartiallyUpdates {
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(30, 30))])
];
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(40, 40))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(30, 30))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]),
];
[self.collectionView moveSection:1 toSection:2];
// check that section 0 wasn't updated
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 10, 10);
// check that section 1 was updated
IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 0, 30, 30);
// check that section 2 was updated
IGAssertEqualFrame([self cellForSection:2 item:0].frame, 40, 0, 20, 20);
}
#pragma mark - Batch
- (void)test_whenInsertDeleteMoveSection_thatLayoutPartiallyUpdates {
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(1, 1))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(2, 2))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(3, 3))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(4, 4))]),
];
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
self.dataSource.sections = @[
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(0, 0))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(4, 4))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(3, 3))]),
genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(5, 5))]),
];
XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)];
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:1]]; // deleted (2, 2)
[self.collectionView moveSection:3 toSection:1]; // move (4, 4)
[self.collectionView insertSections:[NSIndexSet indexSetWithIndex:3]]; // inserted (5, 5)
} completion:^(BOOL finished) {
[self.collectionView layoutIfNeeded];
[expectation fulfill];
// check that section 0 wasn't updated
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 1, 1);
// check that section 1 was updated
IGAssertEqualFrame([self cellForSection:1 item:0].frame, 1, 0, 4, 4);
// check that section 2 was updated
IGAssertEqualFrame([self cellForSection:2 item:0].frame, 5, 0, 3, 3);
// check that section 3 was updated
IGAssertEqualFrame([self cellForSection:3 item:0].frame, 8, 0, 5, 5);
}];
[self waitForExpectationsWithTimeout:30 handler:^(NSError * _Nullable error) {
XCTAssertNil(error);
}];
}
#pragma mark - Helpers
- (UICollectionViewCell *)cellForSection:(NSInteger)section item:(NSInteger)item {
return [self.collectionView cellForItemAtIndexPath:genIndexPath(section, item)];
}
@end

View file

@ -9,6 +9,8 @@
#import <UIKit/UIKit.h>
#define genLayoutTestItem(s) [[IGLayoutTestItem alloc] initWithSize:s]
@interface IGLayoutTestItem : NSObject
@property (nonatomic, assign, readonly) CGSize size;

View file

@ -11,6 +11,8 @@
@class IGLayoutTestItem;
#define genLayoutTestSection(i) [[IGLayoutTestSection alloc] initWithItems:i]
@interface IGLayoutTestSection : NSObject
@property (nonatomic, assign, readonly) UIEdgeInsets insets;
@ -20,11 +22,13 @@
@property (nonatomic, assign, readonly) CGFloat footerHeight;
@property (nonatomic, strong, readonly) NSArray<IGLayoutTestItem *> *items;
- (instancetype)initWithItems:(NSArray<IGLayoutTestItem *> *)items;
- (instancetype)initWithInsets:(UIEdgeInsets)insets
lineSpacing:(CGFloat)lineSpacing
interitemSpacing:(CGFloat)interitemSpacing
headerHeight:(CGFloat)headerHeight
footerHeight:(CGFloat)footerHeight
items:(NSArray<IGLayoutTestItem *> *)items;
items:(NSArray<IGLayoutTestItem *> *)items NS_DESIGNATED_INITIALIZER;
@end

View file

@ -11,6 +11,15 @@
@implementation IGLayoutTestSection
- (instancetype)initWithItems:(NSArray<IGLayoutTestItem *> *)items {
return [self initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:0
footerHeight:0
items:items];
}
- (instancetype)initWithInsets:(UIEdgeInsets)insets
lineSpacing:(CGFloat)lineSpacing
interitemSpacing:(CGFloat)interitemSpacing