mirror of
https://github.com/Instagram/IGListKit
synced 2026-05-23 17:28:22 +00:00
created IGListCollectionView
Summary: * IGListCollectionView passes update information to IGListCollectionViewLayout. Reviewed By: rnystrom Differential Revision: D6510139 fbshipit-source-id: ada194d009eca7f308887ec592ecc41fb9557855
This commit is contained in:
parent
f0b947cd1f
commit
23bcc5c7c0
6 changed files with 355 additions and 1 deletions
44
Source/IGListCollectionView.h
Normal file
44
Source/IGListCollectionView.h
Normal 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
|
||||
106
Source/IGListCollectionView.m
Normal file
106
Source/IGListCollectionView.m
Normal 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
|
||||
189
Tests/IGListCollectionViewTests.m
Normal file
189
Tests/IGListCollectionViewTests.m
Normal 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
|
||||
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#define genLayoutTestItem(s) [[IGLayoutTestItem alloc] initWithSize:s]
|
||||
|
||||
@interface IGLayoutTestItem : NSObject
|
||||
|
||||
@property (nonatomic, assign, readonly) CGSize size;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue