From 9ddc64f4ce227758ba85c4ed92f87e0edcfedee4 Mon Sep 17 00:00:00 2001 From: Kevin Delannoy Date: Wed, 20 Sep 2017 08:34:08 -0700 Subject: [PATCH] Add cell (un)highlight APIs Summary: This implements the `collectionView:didHighlightItemAtIndexPath:` and `collectionView:didUnhighlightItemAtIndexPath:` `UICollectionViewDelegate` APIs and forward the calls to the appropriate `IGListSectionController`. For the most part, it's doing the same thing as the `collectionView:didDeselectItemAtIndexPath:` calls and tests are also based on that feature. - Implements `collectionView:didHighlightItemAtIndexPath:` and `collectionView:didUnhighlightItemAtIndexPath:` in IGListAdapter - Catch those selectors in `IGListAdapterProxy` - Adds `didHighlightItemAtIndex:` and `didUnhighlightItemAtIndex:` methods to IGListSectionController and friends (supports `IGListBindingSectionController` via its delegate and `IGListStackedSectionController`) - Tests for changes Issue fixed: I didn't open one, so none I guess. - [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. - [x] I have reviewed the [contributing guide](https://github.com/Instagram/IGListKit/blob/master/.github/CONTRIBUTING.md) Closes https://github.com/Instagram/IGListKit/pull/933 Differential Revision: D5872090 Pulled By: rnystrom fbshipit-source-id: adc93b68aced3e995f32c291bf607a263cd58edf --- CHANGELOG.md | 3 + Source/IGListBindingSectionController.m | 14 ++++ ...indingSectionControllerSelectionDelegate.h | 24 +++++++ Source/IGListSectionController.h | 18 +++++ Source/IGListSectionController.m | 4 ++ Source/IGListStackedSectionController.m | 12 ++++ .../Internal/IGListAdapter+UICollectionView.m | 22 ++++++ Source/Internal/IGListAdapterProxy.m | 2 + Tests/IGListAdapterTests.m | 68 +++++++++++++++++++ Tests/IGListBindingSectionControllerTests.m | 18 +++++ Tests/IGListStackSectionControllerTests.m | 50 ++++++++++++++ Tests/Objects/IGListTestSection.h | 2 + Tests/Objects/IGListTestSection.m | 8 +++ .../Objects/IGTestDiffingSectionController.h | 2 + .../Objects/IGTestDiffingSectionController.m | 8 +++ 15 files changed, 255 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff946fcdb..9ec3a8080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag - Weakly reference the `UICollectionView` in coalescence so that it can be released if the rest of system is destroyed. [Ryan Nystrom](https://github.com/rnystrom) [(#tbd)](https://github.com/Instagram/IGListKit/pull/tbd) +### Enhancements + +- Added `-[IGListSectionController didHighlightItemAtIndex:]` and `-[IGListSectionController didUnhighlightItemAtIndex:]` APIs to support `UICollectionView` cell highlighting. [Kevin Delannoy](https://github.com/delannoyk) [(#933)](https://github.com/Instagram/IGListKit/pull/933) 3.1.1 ----- diff --git a/Source/IGListBindingSectionController.m b/Source/IGListBindingSectionController.m index 549cf486d..2157720d9 100644 --- a/Source/IGListBindingSectionController.m +++ b/Source/IGListBindingSectionController.m @@ -130,4 +130,18 @@ - (void)didDeselectItemAtIndex:(NSInteger)index { } } +- (void)didHighlightItemAtIndex:(NSInteger)index { + id selectionDelegate = self.selectionDelegate; + if ([selectionDelegate respondsToSelector:@selector(sectionController:didHighlightItemAtIndex:viewModel:)]) { + [selectionDelegate sectionController:self didHighlightItemAtIndex:index viewModel:self.viewModels[index]]; + } +} + +- (void)didUnhighlightItemAtIndex:(NSInteger)index { + id selectionDelegate = self.selectionDelegate; + if ([selectionDelegate respondsToSelector:@selector(sectionController:didUnhighlightItemAtIndex:viewModel:)]) { + [selectionDelegate sectionController:self didUnhighlightItemAtIndex:index viewModel:self.viewModels[index]]; + } +} + @end diff --git a/Source/IGListBindingSectionControllerSelectionDelegate.h b/Source/IGListBindingSectionControllerSelectionDelegate.h index 936e221a5..ae5992e54 100644 --- a/Source/IGListBindingSectionControllerSelectionDelegate.h +++ b/Source/IGListBindingSectionControllerSelectionDelegate.h @@ -44,6 +44,30 @@ NS_SWIFT_NAME(ListBindingSectionControllerSelectionDelegate) didDeselectItemAtIndex:(NSInteger)index viewModel:(id)viewModel; +/** + Tells the delegate that a cell at a given index was highlighted. + + @param sectionController The section controller the highlight occurred in. + @param index The index of the highlighted cell. + @param viewModel The view model that was bound to the cell. + */ +@optional +- (void)sectionController:(IGListBindingSectionController *)sectionController + didHighlightItemAtIndex:(NSInteger)index + viewModel:(id)viewModel; + +/** + Tells the delegate that a cell at a given index was unhighlighted. + + @param sectionController The section controller the unhighlight occurred in. + @param index The index of the unhighlighted cell. + @param viewModel The view model that was bound to the cell. + */ +@optional +- (void)sectionController:(IGListBindingSectionController *)sectionController +didUnhighlightItemAtIndex:(NSInteger)index + viewModel:(id)viewModel; + @end NS_ASSUME_NONNULL_END diff --git a/Source/IGListSectionController.h b/Source/IGListSectionController.h index 8d02fbf68..1846a9b92 100644 --- a/Source/IGListSectionController.h +++ b/Source/IGListSectionController.h @@ -90,6 +90,24 @@ NS_SWIFT_NAME(ListSectionController) */ - (void)didDeselectItemAtIndex:(NSInteger)index; +/** + Tells the section controller that the cell at the specified index path was highlighted. + + @param index The index of the highlighted cell. + + @note The default implementation does nothing. **Calling super is not required.** + */ +- (void)didHighlightItemAtIndex:(NSInteger)index; + +/** + Tells the section controller that the cell at the specified index path was unhighlighted. + + @param index The index of the unhighlighted cell. + + @note The default implementation does nothing. **Calling super is not required.** + */ +- (void)didUnhighlightItemAtIndex:(NSInteger)index; + /** The view controller housing the adapter that created this section controller. diff --git a/Source/IGListSectionController.m b/Source/IGListSectionController.m index ccef02eae..242a73aea 100644 --- a/Source/IGListSectionController.m +++ b/Source/IGListSectionController.m @@ -86,4 +86,8 @@ - (void)didSelectItemAtIndex:(NSInteger)index {} - (void)didDeselectItemAtIndex:(NSInteger)index {} +- (void)didHighlightItemAtIndex:(NSInteger)index {} + +- (void)didUnhighlightItemAtIndex:(NSInteger)index {} + @end diff --git a/Source/IGListStackedSectionController.m b/Source/IGListStackedSectionController.m index 9e7d26277..0ac91c4e7 100644 --- a/Source/IGListStackedSectionController.m +++ b/Source/IGListStackedSectionController.m @@ -166,6 +166,18 @@ - (void)didDeselectItemAtIndex:(NSInteger)index { [sectionController didDeselectItemAtIndex:localIndex]; } +- (void)didHighlightItemAtIndex:(NSInteger)index { + IGListSectionController *sectionController = [self sectionControllerForObjectIndex:index]; + const NSInteger localIndex = [self localIndexForSectionController:sectionController index:index]; + [sectionController didHighlightItemAtIndex:localIndex]; +} + +- (void)didUnhighlightItemAtIndex:(NSInteger)index { + IGListSectionController *sectionController = [self sectionControllerForObjectIndex:index]; + const NSInteger localIndex = [self localIndexForSectionController:sectionController index:index]; + [sectionController didUnhighlightItemAtIndex:localIndex]; +} + #pragma mark - IGListCollectionContext - (CGSize)containerSize { diff --git a/Source/Internal/IGListAdapter+UICollectionView.m b/Source/Internal/IGListAdapter+UICollectionView.m index c3c60d141..84d0973f4 100644 --- a/Source/Internal/IGListAdapter+UICollectionView.m +++ b/Source/Internal/IGListAdapter+UICollectionView.m @@ -150,6 +150,28 @@ - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupple [self removeMapForView:view]; } +- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath { + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id collectionViewDelegate = self.collectionViewDelegate; + if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didHighlightItemAtIndexPath:)]) { + [collectionViewDelegate collectionView:collectionView didHighlightItemAtIndexPath:indexPath]; + } + + IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; + [sectionController didHighlightItemAtIndex:indexPath.item]; +} + +- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath { + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id collectionViewDelegate = self.collectionViewDelegate; + if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didUnhighlightItemAtIndexPath:)]) { + [collectionViewDelegate collectionView:collectionView didUnhighlightItemAtIndexPath:indexPath]; + } + + IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; + [sectionController didUnhighlightItemAtIndex:indexPath.item]; +} + #pragma mark - UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { diff --git a/Source/Internal/IGListAdapterProxy.m b/Source/Internal/IGListAdapterProxy.m index e54381b8a..af3b2686f 100644 --- a/Source/Internal/IGListAdapterProxy.m +++ b/Source/Internal/IGListAdapterProxy.m @@ -21,6 +21,8 @@ static BOOL isInterceptedSelector(SEL sel) { sel == @selector(collectionView:didSelectItemAtIndexPath:) || sel == @selector(collectionView:willDisplayCell:forItemAtIndexPath:) || sel == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) || + sel == @selector(collectionView:didHighlightItemAtIndexPath:) || + sel == @selector(collectionView:didUnhighlightItemAtIndexPath:) || // UICollectionViewDelegateFlowLayout sel == @selector(collectionView:layout:sizeForItemAtIndexPath:) || sel == @selector(collectionView:layout:insetForSectionAtIndex:) || diff --git a/Tests/IGListAdapterTests.m b/Tests/IGListAdapterTests.m index c74164572..69abac677 100644 --- a/Tests/IGListAdapterTests.m +++ b/Tests/IGListAdapterTests.m @@ -1170,6 +1170,74 @@ - (void)test_whenEndDisplayingSupplementaryView_thatCollectionViewDelegateReceiv [mockDelegate verify]; } +- (void)test_whenHighlightingCell_thatCollectionViewDelegateReceivesMethod { + self.dataSource.objects = @[@0, @1, @2]; + [self.adapter reloadDataWithCompletion:nil]; + + id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; + self.adapter.collectionViewDelegate = mockDelegate; + + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + [[mockDelegate expect] collectionView:self.collectionView didHighlightItemAtIndexPath:indexPath]; + + // simulates the collectionview telling its delegate that it was highlighted + [self.adapter collectionView:self.collectionView didHighlightItemAtIndexPath:indexPath]; + + [mockDelegate verify]; +} + +- (void)test_whenHighlightingCell_thatSectionControllerReceivesMethod { + self.dataSource.objects = @[@0, @1, @2]; + [self.adapter reloadDataWithCompletion:nil]; + + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + + // simulates the collectionview telling its delegate that it was highlighted + [self.adapter collectionView:self.collectionView didHighlightItemAtIndexPath:indexPath]; + + IGListTestSection *s0 = [self.adapter sectionControllerForObject:@0]; + IGListTestSection *s1 = [self.adapter sectionControllerForObject:@1]; + IGListTestSection *s2 = [self.adapter sectionControllerForObject:@2]; + + XCTAssertTrue(s0.wasHighlighted); + XCTAssertFalse(s1.wasHighlighted); + XCTAssertFalse(s2.wasHighlighted); +} + +- (void)test_whenUnhighlightingCell_thatCollectionViewDelegateReceivesMethod { + self.dataSource.objects = @[@0, @1, @2]; + [self.adapter reloadDataWithCompletion:nil]; + + id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; + self.adapter.collectionViewDelegate = mockDelegate; + + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + [[mockDelegate expect] collectionView:self.collectionView didUnhighlightItemAtIndexPath:indexPath]; + + // simulates the collectionview telling its delegate that it was unhighlighted + [self.adapter collectionView:self.collectionView didUnhighlightItemAtIndexPath:indexPath]; + + [mockDelegate verify]; +} + +- (void)test_whenUnlighlightingCell_thatSectionControllerReceivesMethod { + self.dataSource.objects = @[@0, @1, @2]; + [self.adapter reloadDataWithCompletion:nil]; + + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + + // simulates the collectionview telling its delegate that it was unhighlighted + [self.adapter collectionView:self.collectionView didUnhighlightItemAtIndexPath:indexPath]; + + IGListTestSection *s0 = [self.adapter sectionControllerForObject:@0]; + IGListTestSection *s1 = [self.adapter sectionControllerForObject:@1]; + IGListTestSection *s2 = [self.adapter sectionControllerForObject:@2]; + + XCTAssertTrue(s0.wasUnhighlighted); + XCTAssertFalse(s1.wasUnhighlighted); + XCTAssertFalse(s2.wasUnhighlighted); +} + - (void)test_whenDataSourceDoesntHandleObject_thatObjectIsDropped { // IGListTestAdapterDataSource does not handle NSStrings self.dataSource.objects = @[@1, @"dog", @2]; diff --git a/Tests/IGListBindingSectionControllerTests.m b/Tests/IGListBindingSectionControllerTests.m index ecf3eed62..bf2d14f9b 100644 --- a/Tests/IGListBindingSectionControllerTests.m +++ b/Tests/IGListBindingSectionControllerTests.m @@ -123,6 +123,24 @@ - (void)test_whenDeselectingCell_thatCorrectViewModelSelected { XCTAssertEqualObjects(section.deselectedViewModel, @"seven"); } +- (void)test_whenHighlightingCell_thatCorrectViewModelHighlighted { + [self setupWithObjects:@[ + [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], + ]]; + [self.adapter collectionView:self.collectionView didHighlightItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; + IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; + XCTAssertEqualObjects(section.highlightedViewModel, @"seven"); +} + +- (void)test_whenUnhighlightingCell_thatCorrectViewModelUnhighlighted { + [self setupWithObjects:@[ + [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], + ]]; + [self.adapter collectionView:self.collectionView didUnhighlightItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; + IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; + XCTAssertEqualObjects(section.unhighlightedViewModel, @"seven"); +} + - (void)test_whenDeselectingCell_withoutImplementation_thatNoOps { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], diff --git a/Tests/IGListStackSectionControllerTests.m b/Tests/IGListStackSectionControllerTests.m index 5554c7b19..54c666f73 100644 --- a/Tests/IGListStackSectionControllerTests.m +++ b/Tests/IGListStackSectionControllerTests.m @@ -592,6 +592,56 @@ - (void)test_whenDeselectingItems_thatChildSectionControllersSelected { XCTAssertTrue([stack2.sectionControllers[1] wasDeselected]); } +- (void)test_whenHighlightingItems_thatChildSectionControllersSelected { + [self setupWithObjects:@[ + [[IGTestObject alloc] initWithKey:@0 value:@[@1, @2, @3]], + [[IGTestObject alloc] initWithKey:@1 value:@[@1, @2, @3]], + [[IGTestObject alloc] initWithKey:@2 value:@[@1, @1]] + ]]; + + [self.adapter collectionView:self.collectionView didHighlightItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + [self.adapter collectionView:self.collectionView didHighlightItemAtIndexPath:[NSIndexPath indexPathForItem:2 inSection:1]]; + [self.adapter collectionView:self.collectionView didHighlightItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:2]]; + + IGListStackedSectionController *stack0 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; + IGListStackedSectionController *stack1 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; + IGListStackedSectionController *stack2 = [self.adapter sectionControllerForObject:self.dataSource.objects[2]]; + + XCTAssertTrue([stack0.sectionControllers[0] wasHighlighted]); + XCTAssertFalse([stack0.sectionControllers[1] wasHighlighted]); + XCTAssertFalse([stack0.sectionControllers[2] wasHighlighted]); + XCTAssertFalse([stack1.sectionControllers[0] wasHighlighted]); + XCTAssertTrue([stack1.sectionControllers[1] wasHighlighted]); + XCTAssertFalse([stack1.sectionControllers[2] wasHighlighted]); + XCTAssertFalse([stack2.sectionControllers[0] wasHighlighted]); + XCTAssertTrue([stack2.sectionControllers[1] wasHighlighted]); +} + +- (void)test_whenUnhighlightingItems_thatChildSectionControllersUnhighlighted { + [self setupWithObjects:@[ + [[IGTestObject alloc] initWithKey:@0 value:@[@1, @2, @3]], + [[IGTestObject alloc] initWithKey:@1 value:@[@1, @2, @3]], + [[IGTestObject alloc] initWithKey:@2 value:@[@1, @1]] + ]]; + + [self.adapter collectionView:self.collectionView didUnhighlightItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + [self.adapter collectionView:self.collectionView didUnhighlightItemAtIndexPath:[NSIndexPath indexPathForItem:2 inSection:1]]; + [self.adapter collectionView:self.collectionView didUnhighlightItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:2]]; + + IGListStackedSectionController *stack0 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; + IGListStackedSectionController *stack1 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; + IGListStackedSectionController *stack2 = [self.adapter sectionControllerForObject:self.dataSource.objects[2]]; + + XCTAssertTrue([stack0.sectionControllers[0] wasUnhighlighted]); + XCTAssertFalse([stack0.sectionControllers[1] wasUnhighlighted]); + XCTAssertFalse([stack0.sectionControllers[2] wasUnhighlighted]); + XCTAssertFalse([stack1.sectionControllers[0] wasUnhighlighted]); + XCTAssertTrue([stack1.sectionControllers[1] wasUnhighlighted]); + XCTAssertFalse([stack1.sectionControllers[2] wasUnhighlighted]); + XCTAssertFalse([stack2.sectionControllers[0] wasUnhighlighted]); + XCTAssertTrue([stack2.sectionControllers[1] wasUnhighlighted]); +} + - (void)test_whenUsingNibs_withStoryboards_thatCellsAreConfigured { [self setupWithObjects:@[ [[IGTestObject alloc] initWithKey:@0 value:@[@1, @"nib", @"storyboard"]], diff --git a/Tests/Objects/IGListTestSection.h b/Tests/Objects/IGListTestSection.h index f2650875c..0002a22e0 100644 --- a/Tests/Objects/IGListTestSection.h +++ b/Tests/Objects/IGListTestSection.h @@ -18,5 +18,7 @@ @property (nonatomic, assign) CGSize size; @property (nonatomic, assign) BOOL wasSelected; @property (nonatomic, assign) BOOL wasDeselected; +@property (nonatomic, assign) BOOL wasHighlighted; +@property (nonatomic, assign) BOOL wasUnhighlighted; @end diff --git a/Tests/Objects/IGListTestSection.m b/Tests/Objects/IGListTestSection.m index bb3e3a2d5..998e47e6a 100644 --- a/Tests/Objects/IGListTestSection.m +++ b/Tests/Objects/IGListTestSection.m @@ -50,4 +50,12 @@ - (void)didDeselectItemAtIndex:(NSInteger)index { self.wasDeselected = YES; } +- (void)didHighlightItemAtIndex:(NSInteger)index { + self.wasHighlighted = YES; +} + +- (void)didUnhighlightItemAtIndex:(NSInteger)index { + self.wasUnhighlighted = YES; +} + @end diff --git a/Tests/Objects/IGTestDiffingSectionController.h b/Tests/Objects/IGTestDiffingSectionController.h index a798bd788..f14ba0ea5 100644 --- a/Tests/Objects/IGTestDiffingSectionController.h +++ b/Tests/Objects/IGTestDiffingSectionController.h @@ -13,5 +13,7 @@ @property (nonatomic, strong) id selectedViewModel; @property (nonatomic, strong) id deselectedViewModel; +@property (nonatomic, strong) id highlightedViewModel; +@property (nonatomic, strong) id unhighlightedViewModel; @end diff --git a/Tests/Objects/IGTestDiffingSectionController.m b/Tests/Objects/IGTestDiffingSectionController.m index 589a5637f..ae3a9b12a 100644 --- a/Tests/Objects/IGTestDiffingSectionController.m +++ b/Tests/Objects/IGTestDiffingSectionController.m @@ -59,4 +59,12 @@ - (void)sectionController:(IGListBindingSectionController *)sectionController di self.deselectedViewModel = viewModel; } +- (void)sectionController:(IGListBindingSectionController *)sectionController didHighlightItemAtIndex:(NSInteger)index viewModel:(id)viewModel { + self.highlightedViewModel = viewModel; +} + +- (void)sectionController:(IGListBindingSectionController *)sectionController didUnhighlightItemAtIndex:(NSInteger)index viewModel:(id)viewModel { + self.unhighlightedViewModel = viewModel; +} + @end