From 361eea4ea398c0bf9afd3a18751370ac0d26151b Mon Sep 17 00:00:00 2001 From: Kevin Delannoy Date: Wed, 13 Sep 2017 16:57:03 -0400 Subject: [PATCH 1/3] Add cell (un)highlight APIs --- CHANGELOG.md | 10 +++ 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, 262 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bed6aaa4b..e16283175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instagram/IGListKit/releases) on GitHub. +TBD (**Upcoming release**) +----- + +### Fixes + + +### 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 From be70c0aedf7d263a0fc2e3f39873166ee83c3066 Mon Sep 17 00:00:00 2001 From: Kevin Delannoy Date: Thu, 14 Sep 2017 08:10:46 -0400 Subject: [PATCH 2/3] Sets the release version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e16283175..1c5493d35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instagram/IGListKit/releases) on GitHub. -TBD (**Upcoming release**) +3.2.0 (**Upcoming release**) ----- ### Fixes From 4875e2a511903c5c0233ce2561239c4ee9ecb4dd Mon Sep 17 00:00:00 2001 From: Kevin Delannoy Date: Tue, 19 Sep 2017 17:26:36 -0400 Subject: [PATCH 3/3] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5693d12fe..9ec3a8080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,10 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag ### Fixes +- 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 -- 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) - 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