Skip to content

Commit

Permalink
Provided support for iOS 13 Context Menus
Browse files Browse the repository at this point in the history
  • Loading branch information
Jerome Boursier committed Feb 26, 2020
1 parent 191063e commit 78a5fc0
Show file tree
Hide file tree
Showing 15 changed files with 141 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag
4.1.0 (upcoming release)
-----

### Enhancements

- Introduce `IGListSwiftKit`, with Swift refinements for `dequeueReusableCellOfClass` methods. [Koen Punt](https://github.com/koenpunt) [(#1388)](https://github.com/Instagram/IGListKit/pull/1388).

- Added support for iOS 13 Context Menus with `contextMenuConfigurationForItemAt` method. [Jérôme B.](https://github.com/jjbourdev) [(#1430)](https://github.com/Instagram/IGListKit/pull/1430).

4.0.0
-----
### Breaking Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,26 @@ final class MonthSectionController: ListBindingSectionController<ListDiffable>,

func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, didUnhighlightItemAt index: Int, viewModel: Any) {}

@available(iOS 13.0, *)
func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, contextMenuConfigurationForItemAt index: Int, point: CGPoint, viewModel: Any) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
// Create an action for sharing
let share = UIAction(title: "Share", image: UIImage(systemName: "square.and.arrow.up")) { _ in
// Show share sheet
}

// Create an action for copy
let rename = UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc")) { _ in
// Perform copy
}

// Create an action for delete with destructive attributes (highligh in red)
let delete = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { _ in
// Perform delete
}

// Create a UIMenu with all the actions as children
return UIMenu(title: "", children: [share, rename, delete])
}
}
}
6 changes: 6 additions & 0 deletions Source/IGListKit/IGListBindingSectionController.m
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,10 @@ - (void)didUnhighlightItemAtIndex:(NSInteger)index {
[self.selectionDelegate sectionController:self didUnhighlightItemAtIndex:index viewModel:self.viewModels[index]];
}

#if !TARGET_OS_TV
- (nullable UIContextMenuConfiguration *)contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point {
return [self.selectionDelegate sectionController:self contextMenuConfigurationForItemAtIndex:index point:point viewModel:self.viewModels[index]];
}
#endif

@end
15 changes: 15 additions & 0 deletions Source/IGListKit/IGListBindingSectionControllerSelectionDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ NS_SWIFT_NAME(ListBindingSectionControllerSelectionDelegate)
didUnhighlightItemAtIndex:(NSInteger)index
viewModel:(id)viewModel;

/**
Tells the delegate that a cell has requested a menu configuration.
@param sectionController The section controller the request of a menu configuration occurred in.
@param index The index of the cell that is being longed tap.
@param point The point of the tap on the cell.
@param viewModel The view model that was bound to the cell.
@return An object that conforms to `UIContextMenuConfiguration`.
*/
- (nullable UIContextMenuConfiguration *)sectionController:(IGListBindingSectionController *)sectionController
contextMenuConfigurationForItemAtIndex:(NSInteger)index
point:(CGPoint)point
viewModel:(id)viewModel API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos);

@end

NS_ASSUME_NONNULL_END
12 changes: 12 additions & 0 deletions Source/IGListKit/IGListSectionController.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ NS_SWIFT_NAME(ListSectionController)
*/
- (void)didUnhighlightItemAtIndex:(NSInteger)index;

/**
Tells the section controller that the cell has requested a menu configuration.
@param index The index of the cell that requested the menu.
@param point The point of the tap on the cell.
@return An object that conforms to `UIContextMenuConfiguration`
@note The default implementation does nothing. **Calling super is not required.**
*/
- (nullable UIContextMenuConfiguration *)contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos);

/**
Identifies whether an object can be moved through interactive reordering.
Expand Down
4 changes: 4 additions & 0 deletions Source/IGListKit/IGListSectionController.m
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ - (void)didHighlightItemAtIndex:(NSInteger)index {}

- (void)didUnhighlightItemAtIndex:(NSInteger)index {}

- (nullable UIContextMenuConfiguration *)contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point {
return nil;
}

- (BOOL)canMoveItemAtIndex:(NSInteger)index {
return NO;
}
Expand Down
13 changes: 13 additions & 0 deletions Source/IGListKit/Internal/IGListAdapter+UICollectionView.m
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,19 @@ - (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIn
[sectionController didUnhighlightItemAtIndex:indexPath.item];
}

#if !TARGET_OS_TV
- (UIContextMenuConfiguration *)collectionView:(UICollectionView *)collectionView contextMenuConfigurationForItemAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) {
// forward this method to the delegate b/c this implementation will steal the message from the proxy
id<UICollectionViewDelegate> collectionViewDelegate = self.collectionViewDelegate;
if ([collectionViewDelegate respondsToSelector:@selector(collectionView:contextMenuConfigurationForItemAtIndexPath:point:)]) {
[collectionViewDelegate collectionView:collectionView contextMenuConfigurationForItemAtIndexPath:indexPath point:point];
}

IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section];
return [sectionController contextMenuConfigurationForItemAtIndex:indexPath.item point:point];
}
#endif

#pragma mark - UICollectionViewDelegateFlowLayout

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
Expand Down
1 change: 1 addition & 0 deletions Source/IGListKit/Internal/IGListAdapterProxy.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ static BOOL isInterceptedSelector(SEL sel) {
sel == @selector(collectionView:didDeselectItemAtIndexPath:) ||
sel == @selector(collectionView:didHighlightItemAtIndexPath:) ||
sel == @selector(collectionView:didUnhighlightItemAtIndexPath:) ||
sel == @selector(collectionView:contextMenuConfigurationForItemAtIndexPath:point:) ||
// UICollectionViewDelegateFlowLayout
sel == @selector(collectionView:layout:sizeForItemAtIndexPath:) ||
sel == @selector(collectionView:layout:insetForSectionAtIndex:) ||
Expand Down
36 changes: 35 additions & 1 deletion Tests/IGListAdapterTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -1425,7 +1425,7 @@ - (void)test_whenUnhighlightingCell_thatCollectionViewDelegateReceivesMethod {
[mockDelegate verify];
}

- (void)test_whenUnlighlightingCell_thatSectionControllerReceivesMethod {
- (void)test_whenUnhighlightingCell_thatSectionControllerReceivesMethod {
self.dataSource.objects = @[@0, @1, @2];
[self.adapter reloadDataWithCompletion:nil];

Expand All @@ -1443,6 +1443,40 @@ - (void)test_whenUnlighlightingCell_thatSectionControllerReceivesMethod {
XCTAssertFalse(s2.wasUnhighlighted);
}

- (void)test_whenContextMenuAskedCell_thatCollectionViewDelegateReceivesMethod API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) {
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 contextMenuConfigurationForItemAtIndexPath:indexPath point:CGPointZero];

// simulates the collectionview telling its delegate that it needs the context menu configuration
[self.adapter collectionView:self.collectionView contextMenuConfigurationForItemAtIndexPath:indexPath point:CGPointZero];

[mockDelegate verify];
}

- (void)test_whenContextMenuAskedCell_thatSectionControllerReceivesMethod API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) {
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 needs the context menu configuration
[self.adapter collectionView:self.collectionView contextMenuConfigurationForItemAtIndexPath:indexPath point:CGPointZero];

IGListTestSection *s0 = [self.adapter sectionControllerForObject:@0];
IGListTestSection *s1 = [self.adapter sectionControllerForObject:@1];
IGListTestSection *s2 = [self.adapter sectionControllerForObject:@2];

XCTAssertTrue(s0.requestedContextMenu);
XCTAssertFalse(s1.requestedContextMenu);
XCTAssertFalse(s2.requestedContextMenu);
}

- (void)test_whenDataSourceDoesntHandleObject_thatObjectIsDropped {
// IGListTestAdapterDataSource does not handle NSStrings
self.dataSource.objects = @[@1, @"dog", @2];
Expand Down
9 changes: 9 additions & 0 deletions Tests/IGListBindingSectionControllerTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,15 @@ - (void)test_whenUnhighlightingCell_thatCorrectViewModelUnhighlighted {
XCTAssertEqualObjects(section.unhighlightedViewModel, @"seven");
}

- (void)test_whenContextMenuAskedCell_thatCorrectViewModelRetrieved API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) {
[self setupWithObjects:@[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
]];
[self.adapter collectionView:self.collectionView contextMenuConfigurationForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0] point:CGPointZero];
IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject];
XCTAssertEqualObjects(section.contextMenuViewModel, @"seven");
}

- (void)test_whenDeselectingCell_withoutImplementation_thatNoOps {
[self setupWithObjects:@[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
Expand Down
1 change: 1 addition & 0 deletions Tests/Objects/IGListTestSection.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
@property (nonatomic, assign) BOOL wasHighlighted;
@property (nonatomic, assign) BOOL wasUnhighlighted;
@property (nonatomic, assign) BOOL wasDisplayed;
@property (nonatomic, assign) BOOL requestedContextMenu;

@end
5 changes: 5 additions & 0 deletions Tests/Objects/IGListTestSection.m
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ - (void)didUnhighlightItemAtIndex:(NSInteger)index {
self.wasUnhighlighted = YES;
}

- (nullable UIContextMenuConfiguration *)contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point {
self.requestedContextMenu = YES;
return nil;
}

#pragma mark - IGListDisplayDelegate

- (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController *)sectionController {
Expand Down
8 changes: 8 additions & 0 deletions Tests/Objects/IGTestBindingWithoutDeselectionDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,12 @@ - (void)sectionController:(nonnull IGListBindingSectionController *)sectionContr
viewModel:(nonnull id)viewModel {
}


- (nullable UIContextMenuConfiguration *)sectionController:(nonnull IGListBindingSectionController *)sectionController
contextMenuConfigurationForItemAtIndex:(NSInteger)index
point:(CGPoint)point
viewModel:(nonnull id)viewModel API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) {
return nil;
}

@end
1 change: 1 addition & 0 deletions Tests/Objects/IGTestDiffingSectionController.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
@property (nonatomic, strong) id deselectedViewModel;
@property (nonatomic, strong) id highlightedViewModel;
@property (nonatomic, strong) id unhighlightedViewModel;
@property (nonatomic, strong) id contextMenuViewModel;

@end
5 changes: 5 additions & 0 deletions Tests/Objects/IGTestDiffingSectionController.m
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,9 @@ - (void)sectionController:(IGListBindingSectionController *)sectionController di
self.unhighlightedViewModel = viewModel;
}

- (nullable UIContextMenuConfiguration *)sectionController:(IGListBindingSectionController *)sectionController contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point viewModel:(id)viewModel API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) {
self.contextMenuViewModel = viewModel;
return nil;
}

@end

0 comments on commit 78a5fc0

Please sign in to comment.