diff --git a/Source/IGListKit/IGListBindingSingleSectionController.h b/Source/IGListKit/IGListBindingSingleSectionController.h new file mode 100644 index 00000000..aa619c15 --- /dev/null +++ b/Source/IGListKit/IGListBindingSingleSectionController.h @@ -0,0 +1,48 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + 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:]. + + This class is intended to be subclassed. + */ +NS_SWIFT_NAME(ListBindingSingleSectionController) +@interface IGListBindingSingleSectionController<__covariant ViewModel : id, Cell : UICollectionViewCell *> : IGListSectionController + +#pragma mark - Subclass + +// Required to be implemented by subclass. +- (Class)cellClass; + +// Required to be implemented by subclass. +- (void)configureCell:(Cell)cell withViewModel:(ViewModel)viewModel; + +// Required to be implemented by subclass. +- (CGSize)sizeForViewModel:(ViewModel)viewModel; + +// Subclasable. Defaults is no-op. +- (void)didSelectItemWithCell:(Cell)cell; + +// Subclasable. Defaults is no-op. +- (void)didDeselectItemWithCell:(Cell)cell; + +// Subclasable. Defaults is no-op. +- (void)didHighlightItemWithCell:(Cell)cell; + +// Subclasable. Defaults is no-op.0 +- (void)didUnhighlightItemWithCell:(Cell)cell; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/IGListBindingSingleSectionController.m b/Source/IGListKit/IGListBindingSingleSectionController.m new file mode 100644 index 00000000..e00c28e8 --- /dev/null +++ b/Source/IGListKit/IGListBindingSingleSectionController.m @@ -0,0 +1,99 @@ +/** +* Copyright (c) Facebook, Inc. and its affiliates. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import "IGListBindingSingleSectionController.h" + +#import + + +@implementation IGListBindingSingleSectionController { + id _item; +} + +- (void)didSelectItemWithCell:(UICollectionViewCell *)cell { + // no-op +} + +- (void)didDeselectItemWithCell:(UICollectionViewCell *)cell { + // no-op +} + +- (void)didHighlightItemWithCell:(UICollectionViewCell *)cell { + // no-op +} + +- (void)didUnhighlightItemWithCell:(UICollectionViewCell *)cell { + // no-op +} + +- (Class)cellClass { + IGFailAssert(@"Implemented by subclass"); + return nil; +} + +- (void)configureCell:(UICollectionViewCell *)cell withViewModel:(id)viewModel { + IGFailAssert(@"Implemented by subclass"); +} + +- (CGSize)sizeForViewModel:(id)viewModel { + IGFailAssert(@"Implemented by subclass"); + return CGSizeZero; +} + +#pragma mark - IGListSectionController Overrides + +- (NSInteger)numberOfItems { + return 1; +} + +- (CGSize)sizeForItemAtIndex:(NSInteger)index { + IGParameterAssert(index == 0); + return [self sizeForViewModel:_item]; +} + +- (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { + IGParameterAssert(index == 0); + UICollectionViewCell *cell = [self.collectionContext dequeueReusableCellOfClass:[self cellClass] forSectionController:self atIndex:index]; + IGAssertNonnull(cell); + [self configureCell:cell withViewModel:_item]; + return cell; +} + +- (void)didUpdateToObject:(id)object { + _item = object; + + UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; + if (cell) { + [self configureCell:cell withViewModel:_item]; + } +} + +- (void)didSelectItemAtIndex:(NSInteger)index { + IGParameterAssert(index == 0); + UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; + [self didSelectItemWithCell:cell]; +} + +- (void)didDeselectItemAtIndex:(NSInteger)index { + IGParameterAssert(index == 0); + UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; + [self didDeselectItemWithCell:cell]; +} + +- (void)didHighlightItemAtIndex:(NSInteger)index { + IGParameterAssert(index == 0); + UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; + [self didHighlightItemWithCell:cell]; +} + +- (void)didUnhighlightItemAtIndex:(NSInteger)index { + IGParameterAssert(index == 0); + UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; + [self didUnhighlightItemWithCell:cell]; +} + +@end diff --git a/Tests/IGListBindingSingleSectionControllerTests.m b/Tests/IGListBindingSingleSectionControllerTests.m new file mode 100644 index 00000000..3c094b6b --- /dev/null +++ b/Tests/IGListBindingSingleSectionControllerTests.m @@ -0,0 +1,134 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "IGTestCell.h" +#import "IGListTestCase.h" +#import "IGListAdapterInternal.h" +#import "IGTestCell.h" +#import "IGTestBindingSingleItemDataSource.h" + + +@interface IGListBindingSingleSectionControllerTests : IGListTestCase + +@end + +@implementation IGListBindingSingleSectionControllerTests + +- (void)setUp { + self.dataSource = [IGTestBindingSingleItemDataSource new]; + self.frame = CGRectMake(0, 0, 100, 1000); + [super setUp]; +} + +- (void)test_whenSetupWithObjects_collectionViewHasSections { + [self setupWithObjects:@[ + genTestObject(@1, @"Foo"), + genTestObject(@2, @"Bar"), + genTestObject(@3, @"Baz"), + ]]; + XCTAssertEqual([self.collectionView numberOfSections], 3); + XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); + XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); + XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 1); +} + +- (void)test_whenSetupWithObjects_sizeIsCalled { + [self setupWithObjects:@[ + genTestObject(@1, @"Foo"), + genTestObject(@2, @"Bar"), + genTestObject(@3, @"Baz"), + ]]; + IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; + IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; + + // Check the size is set in `IGTestBindingSingleSectionController` + XCTAssertEqual(cell1.frame.size.height, 44); + XCTAssertEqual(cell2.frame.size.height, 44); + XCTAssertEqual(cell3.frame.size.height, 44); + XCTAssertEqual(cell1.frame.size.width, 100); + XCTAssertEqual(cell2.frame.size.width, 100); + XCTAssertEqual(cell3.frame.size.width, 100); +} + +- (void)test_whenSetupWithObjects_cellsAreConfigured { + [self setupWithObjects:@[ + genTestObject(@1, @"Foo"), + genTestObject(@2, @"Bar"), + genTestObject(@3, @"Baz"), + ]]; + IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; + IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; + + // Check the cell is configured in `IGTestBindingSingleSectionController` + XCTAssertEqualObjects(cell1.label.text, @"Foo"); + XCTAssertEqualObjects(cell2.label.text, @"Bar"); + XCTAssertEqualObjects(cell3.label.text, @"Baz"); +} + +- (void)test_whenSetupWithObjects_cellClassIsExpected { + [self setupWithObjects:@[ + genTestObject(@1, @"Foo"), + genTestObject(@2, @"Bar"), + genTestObject(@3, @"Baz"), + ]]; + UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + XCTAssertTrue([cell isKindOfClass:[IGTestCell class]]); +} + +- (void)test_whenDidSelectIsCalled_subclassIsCalled { + [self setupWithObjects:@[ + genTestObject(@1, @"Foo"), + ]]; + IGListSectionController *controller = [self.adapter sectionControllerForSection:0]; + [controller didSelectItemAtIndex:0]; + IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + + // Check the cell label is updated in `IGTestBindingSingleSectionController` + XCTAssertEqualObjects(cell1.label.text, @"did-select"); +} + +- (void)test_whenDidDeselectIsCalled_subclassIsCalled { + [self setupWithObjects:@[ + genTestObject(@1, @"Foo"), + ]]; + IGListSectionController *controller = [self.adapter sectionControllerForSection:0]; + [controller didDeselectItemAtIndex:0]; + IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + + // Check the cell label is updated in `IGTestBindingSingleSectionController` + XCTAssertEqualObjects(cell1.label.text, @"did-deselect"); +} + +- (void)test_whenDidHighlightIsCalled_subclassIsCalled { + [self setupWithObjects:@[ + genTestObject(@1, @"Foo"), + ]]; + IGListSectionController *controller = [self.adapter sectionControllerForSection:0]; + [controller didHighlightItemAtIndex:0]; + IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + + // Check the cell label is updated in `IGTestBindingSingleSectionController` + XCTAssertEqualObjects(cell1.label.text, @"did-highlight"); +} + +- (void)test_whenDidUnhighlightIsCalled_subclassIsCalled { + [self setupWithObjects:@[ + genTestObject(@1, @"Foo"), + ]]; + IGListSectionController *controller = [self.adapter sectionControllerForSection:0]; + [controller didUnhighlightItemAtIndex:0]; + IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + + // Check the cell label is updated in `IGTestBindingSingleSectionController` + XCTAssertEqualObjects(cell1.label.text, @"did-unhighlight"); +} + +@end diff --git a/Tests/Objects/IGTestBindingSingleItemDataSource.h b/Tests/Objects/IGTestBindingSingleItemDataSource.h new file mode 100644 index 00000000..df2be344 --- /dev/null +++ b/Tests/Objects/IGTestBindingSingleItemDataSource.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * 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 + +#import + +#import "IGTestObject.h" +#import "IGListTestCase.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface IGTestBindingSingleItemDataSource : NSObject + +@property (nonatomic, strong) NSArray *objects; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/Objects/IGTestBindingSingleItemDataSource.m b/Tests/Objects/IGTestBindingSingleItemDataSource.m new file mode 100644 index 00000000..bd522e63 --- /dev/null +++ b/Tests/Objects/IGTestBindingSingleItemDataSource.m @@ -0,0 +1,67 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * 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 "IGTestBindingSingleItemDataSource.h" + +#import + +#import "IGTestCell.h" + +@interface IGTestBindingSingleSectionController : IGListBindingSingleSectionController + +@end + +@implementation IGTestBindingSingleSectionController + +- (Class)cellClass { + return IGTestCell.class; +} + +- (void)configureCell:(IGTestCell *)cell withViewModel:(IGTestObject *)viewModel { + cell.label.text = [viewModel.value description]; +} + +- (CGSize)sizeForViewModel:(IGTestObject *)viewModel { + return CGSizeMake([self.collectionContext containerSize].width, 44); +} + +- (void)didSelectItemWithCell:(IGTestCell *)cell { + cell.label.text = @"did-select"; +} + +- (void)didDeselectItemWithCell:(IGTestCell *)cell { + cell.label.text = @"did-deselect"; +} + +- (void)didHighlightItemWithCell:(IGTestCell *)cell { + cell.label.text = @"did-highlight"; +} + +- (void)didUnhighlightItemWithCell:(IGTestCell *)cell { + cell.label.text = @"did-unhighlight"; +} + +@end + + +@implementation IGTestBindingSingleItemDataSource + +- (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter { + return self.objects; +} + +- (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { + return [IGTestBindingSingleSectionController new]; +} + +- (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { + return nil; +} + +@end