Skip to content

Commit b4dc116

Browse files
lorixxfacebook-github-bot
authored andcommitted
Add a IGListBindingSingleSectionController
Summary: = Problem = Most of the UICollectionView/List UI only needs to deal with 1 dimensional array of data. The current N section N item setup is a big more complicated for most use case who only deals with 1 dimensional array. In fact, the IGListDiff algorithm works pretty with 1 dimensional array. Then inside `IGListAdapterUpdater`, we do a lot changes to ensure UICollectionVie do not crash: 1. Convert section moves -> delete+insert; 2. Convert section reload -> delete+insert; This results in the animation limitation for the UI updates, we lose the move animation support by UICollectionView and the updates for delete+insert does not look great. = Solution = Hence I am proposing to use simple **Single Item** Section Controller, and we can apply the diffing result directly to the UICollectionView, which is much simpler. However, for the inline section update, we need to support update at the right timing: inside -didUpdateObject:, and now we want to get back the existing cell, and apply data to it. Ideally, `IGListSingleSectionController` is the right call but `IGListSingleSectionController` is not subclassable and we would also need to change the way -didUpdateObject: works in that class. Hence I am building this `IGListBindingSingleSectionController`, which only contains single item, and it can update/bind the cell whenever item is updated. Reviewed By: iperry90 Differential Revision: D18942974 fbshipit-source-id: 539fbe2b72691b649e3ae3d8ed725006bc54b705
1 parent cf1db53 commit b4dc116

5 files changed

Lines changed: 373 additions & 0 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <UIKit/UIKit.h>
9+
10+
#import <IGListDiffKit/IGListMacros.h>
11+
#import <IGListKit/IGListSectionController.h>
12+
13+
NS_ASSUME_NONNULL_BEGIN
14+
15+
/**
16+
Special section controller that only contains a single item, and it will apply the view model update during -didUpdateObject: call, usually happened inside -[UICollectionView performBatchUpdates:completion:].
17+
18+
This class is intended to be subclassed.
19+
*/
20+
NS_SWIFT_NAME(ListBindingSingleSectionController)
21+
@interface IGListBindingSingleSectionController<__covariant ViewModel : id<IGListDiffable>, Cell : UICollectionViewCell *> : IGListSectionController
22+
23+
#pragma mark - Subclass
24+
25+
// Required to be implemented by subclass.
26+
- (Class)cellClass;
27+
28+
// Required to be implemented by subclass.
29+
- (void)configureCell:(Cell)cell withViewModel:(ViewModel)viewModel;
30+
31+
// Required to be implemented by subclass.
32+
- (CGSize)sizeForViewModel:(ViewModel)viewModel;
33+
34+
// Subclasable. Defaults is no-op.
35+
- (void)didSelectItemWithCell:(Cell)cell;
36+
37+
// Subclasable. Defaults is no-op.
38+
- (void)didDeselectItemWithCell:(Cell)cell;
39+
40+
// Subclasable. Defaults is no-op.
41+
- (void)didHighlightItemWithCell:(Cell)cell;
42+
43+
// Subclasable. Defaults is no-op.0
44+
- (void)didUnhighlightItemWithCell:(Cell)cell;
45+
46+
@end
47+
48+
NS_ASSUME_NONNULL_END
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "IGListBindingSingleSectionController.h"
9+
10+
#import <IGListDiffKit/IGListAssert.h>
11+
12+
13+
@implementation IGListBindingSingleSectionController {
14+
id _item;
15+
}
16+
17+
- (void)didSelectItemWithCell:(UICollectionViewCell *)cell {
18+
// no-op
19+
}
20+
21+
- (void)didDeselectItemWithCell:(UICollectionViewCell *)cell {
22+
// no-op
23+
}
24+
25+
- (void)didHighlightItemWithCell:(UICollectionViewCell *)cell {
26+
// no-op
27+
}
28+
29+
- (void)didUnhighlightItemWithCell:(UICollectionViewCell *)cell {
30+
// no-op
31+
}
32+
33+
- (Class)cellClass {
34+
IGFailAssert(@"Implemented by subclass");
35+
return nil;
36+
}
37+
38+
- (void)configureCell:(UICollectionViewCell *)cell withViewModel:(id)viewModel {
39+
IGFailAssert(@"Implemented by subclass");
40+
}
41+
42+
- (CGSize)sizeForViewModel:(id)viewModel {
43+
IGFailAssert(@"Implemented by subclass");
44+
return CGSizeZero;
45+
}
46+
47+
#pragma mark - IGListSectionController Overrides
48+
49+
- (NSInteger)numberOfItems {
50+
return 1;
51+
}
52+
53+
- (CGSize)sizeForItemAtIndex:(NSInteger)index {
54+
IGParameterAssert(index == 0);
55+
return [self sizeForViewModel:_item];
56+
}
57+
58+
- (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index {
59+
IGParameterAssert(index == 0);
60+
UICollectionViewCell *cell = [self.collectionContext dequeueReusableCellOfClass:[self cellClass] forSectionController:self atIndex:index];
61+
IGAssertNonnull(cell);
62+
[self configureCell:cell withViewModel:_item];
63+
return cell;
64+
}
65+
66+
- (void)didUpdateToObject:(id)object {
67+
_item = object;
68+
69+
UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self];
70+
if (cell) {
71+
[self configureCell:cell withViewModel:_item];
72+
}
73+
}
74+
75+
- (void)didSelectItemAtIndex:(NSInteger)index {
76+
IGParameterAssert(index == 0);
77+
UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self];
78+
[self didSelectItemWithCell:cell];
79+
}
80+
81+
- (void)didDeselectItemAtIndex:(NSInteger)index {
82+
IGParameterAssert(index == 0);
83+
UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self];
84+
[self didDeselectItemWithCell:cell];
85+
}
86+
87+
- (void)didHighlightItemAtIndex:(NSInteger)index {
88+
IGParameterAssert(index == 0);
89+
UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self];
90+
[self didHighlightItemWithCell:cell];
91+
}
92+
93+
- (void)didUnhighlightItemAtIndex:(NSInteger)index {
94+
IGParameterAssert(index == 0);
95+
UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self];
96+
[self didUnhighlightItemWithCell:cell];
97+
}
98+
99+
@end
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <XCTest/XCTest.h>
9+
10+
#import "IGTestCell.h"
11+
#import "IGListTestCase.h"
12+
#import "IGListAdapterInternal.h"
13+
#import "IGTestCell.h"
14+
#import "IGTestBindingSingleItemDataSource.h"
15+
16+
17+
@interface IGListBindingSingleSectionControllerTests : IGListTestCase
18+
19+
@end
20+
21+
@implementation IGListBindingSingleSectionControllerTests
22+
23+
- (void)setUp {
24+
self.dataSource = [IGTestBindingSingleItemDataSource new];
25+
self.frame = CGRectMake(0, 0, 100, 1000);
26+
[super setUp];
27+
}
28+
29+
- (void)test_whenSetupWithObjects_collectionViewHasSections {
30+
[self setupWithObjects:@[
31+
genTestObject(@1, @"Foo"),
32+
genTestObject(@2, @"Bar"),
33+
genTestObject(@3, @"Baz"),
34+
]];
35+
XCTAssertEqual([self.collectionView numberOfSections], 3);
36+
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1);
37+
XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1);
38+
XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 1);
39+
}
40+
41+
- (void)test_whenSetupWithObjects_sizeIsCalled {
42+
[self setupWithObjects:@[
43+
genTestObject(@1, @"Foo"),
44+
genTestObject(@2, @"Bar"),
45+
genTestObject(@3, @"Baz"),
46+
]];
47+
IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
48+
IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]];
49+
IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]];
50+
51+
// Check the size is set in `IGTestBindingSingleSectionController`
52+
XCTAssertEqual(cell1.frame.size.height, 44);
53+
XCTAssertEqual(cell2.frame.size.height, 44);
54+
XCTAssertEqual(cell3.frame.size.height, 44);
55+
XCTAssertEqual(cell1.frame.size.width, 100);
56+
XCTAssertEqual(cell2.frame.size.width, 100);
57+
XCTAssertEqual(cell3.frame.size.width, 100);
58+
}
59+
60+
- (void)test_whenSetupWithObjects_cellsAreConfigured {
61+
[self setupWithObjects:@[
62+
genTestObject(@1, @"Foo"),
63+
genTestObject(@2, @"Bar"),
64+
genTestObject(@3, @"Baz"),
65+
]];
66+
IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
67+
IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]];
68+
IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]];
69+
70+
// Check the cell is configured in `IGTestBindingSingleSectionController`
71+
XCTAssertEqualObjects(cell1.label.text, @"Foo");
72+
XCTAssertEqualObjects(cell2.label.text, @"Bar");
73+
XCTAssertEqualObjects(cell3.label.text, @"Baz");
74+
}
75+
76+
- (void)test_whenSetupWithObjects_cellClassIsExpected {
77+
[self setupWithObjects:@[
78+
genTestObject(@1, @"Foo"),
79+
genTestObject(@2, @"Bar"),
80+
genTestObject(@3, @"Baz"),
81+
]];
82+
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
83+
XCTAssertTrue([cell isKindOfClass:[IGTestCell class]]);
84+
}
85+
86+
- (void)test_whenDidSelectIsCalled_subclassIsCalled {
87+
[self setupWithObjects:@[
88+
genTestObject(@1, @"Foo"),
89+
]];
90+
IGListSectionController *controller = [self.adapter sectionControllerForSection:0];
91+
[controller didSelectItemAtIndex:0];
92+
IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
93+
94+
// Check the cell label is updated in `IGTestBindingSingleSectionController`
95+
XCTAssertEqualObjects(cell1.label.text, @"did-select");
96+
}
97+
98+
- (void)test_whenDidDeselectIsCalled_subclassIsCalled {
99+
[self setupWithObjects:@[
100+
genTestObject(@1, @"Foo"),
101+
]];
102+
IGListSectionController *controller = [self.adapter sectionControllerForSection:0];
103+
[controller didDeselectItemAtIndex:0];
104+
IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
105+
106+
// Check the cell label is updated in `IGTestBindingSingleSectionController`
107+
XCTAssertEqualObjects(cell1.label.text, @"did-deselect");
108+
}
109+
110+
- (void)test_whenDidHighlightIsCalled_subclassIsCalled {
111+
[self setupWithObjects:@[
112+
genTestObject(@1, @"Foo"),
113+
]];
114+
IGListSectionController *controller = [self.adapter sectionControllerForSection:0];
115+
[controller didHighlightItemAtIndex:0];
116+
IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
117+
118+
// Check the cell label is updated in `IGTestBindingSingleSectionController`
119+
XCTAssertEqualObjects(cell1.label.text, @"did-highlight");
120+
}
121+
122+
- (void)test_whenDidUnhighlightIsCalled_subclassIsCalled {
123+
[self setupWithObjects:@[
124+
genTestObject(@1, @"Foo"),
125+
]];
126+
IGListSectionController *controller = [self.adapter sectionControllerForSection:0];
127+
[controller didUnhighlightItemAtIndex:0];
128+
IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
129+
130+
// Check the cell label is updated in `IGTestBindingSingleSectionController`
131+
XCTAssertEqualObjects(cell1.label.text, @"did-unhighlight");
132+
}
133+
134+
@end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
#import <Foundation/Foundation.h>
11+
12+
#import <IGListKit/IGListAdapterDataSource.h>
13+
14+
#import "IGTestObject.h"
15+
#import "IGListTestCase.h"
16+
17+
NS_ASSUME_NONNULL_BEGIN
18+
19+
@interface IGTestBindingSingleItemDataSource : NSObject <IGListTestCaseDataSource>
20+
21+
@property (nonatomic, strong) NSArray<IGTestObject *> *objects;
22+
23+
@end
24+
25+
NS_ASSUME_NONNULL_END
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
#import "IGTestBindingSingleItemDataSource.h"
11+
12+
#import <IGListKit/IGListBindingSingleSectionController.h>
13+
14+
#import "IGTestCell.h"
15+
16+
@interface IGTestBindingSingleSectionController : IGListBindingSingleSectionController
17+
18+
@end
19+
20+
@implementation IGTestBindingSingleSectionController
21+
22+
- (Class)cellClass {
23+
return IGTestCell.class;
24+
}
25+
26+
- (void)configureCell:(IGTestCell *)cell withViewModel:(IGTestObject *)viewModel {
27+
cell.label.text = [viewModel.value description];
28+
}
29+
30+
- (CGSize)sizeForViewModel:(IGTestObject *)viewModel {
31+
return CGSizeMake([self.collectionContext containerSize].width, 44);
32+
}
33+
34+
- (void)didSelectItemWithCell:(IGTestCell *)cell {
35+
cell.label.text = @"did-select";
36+
}
37+
38+
- (void)didDeselectItemWithCell:(IGTestCell *)cell {
39+
cell.label.text = @"did-deselect";
40+
}
41+
42+
- (void)didHighlightItemWithCell:(IGTestCell *)cell {
43+
cell.label.text = @"did-highlight";
44+
}
45+
46+
- (void)didUnhighlightItemWithCell:(IGTestCell *)cell {
47+
cell.label.text = @"did-unhighlight";
48+
}
49+
50+
@end
51+
52+
53+
@implementation IGTestBindingSingleItemDataSource
54+
55+
- (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter {
56+
return self.objects;
57+
}
58+
59+
- (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object {
60+
return [IGTestBindingSingleSectionController new];
61+
}
62+
63+
- (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter {
64+
return nil;
65+
}
66+
67+
@end

0 commit comments

Comments
 (0)