How to Show a Custom Uimenuitem for a Uitableviewcell

How to show a custom UIMenuItem for a UITableViewCell?

As far as I understand there are two main problems:

1) you expect tableView canPerformAction: to support custom selectors while the documentation says it supports only two of UIResponderStandardEditActions (copy and/or paste);

2) there's no need for the part || action == @selector(test:) as you are adding the custom menu options by initializing menuItems property. For this items selectors the check will be automatical.

What you can do to get the custom menu item displayed and work is:

1) Fix the table view delegate methods with

a)

UIMenuItem *testMenuItem = [[UIMenuItem alloc] initWithTitle:@"Test" action:@selector(test:)];
[[UIMenuController sharedMenuController] setMenuItems: @[testMenuItem]];
[[UIMenuController sharedMenuController] update];

b)

- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}

-(BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
return (action == @selector(copy:));
}

- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
// required
}

2) Setup the cells (subclassing UITableViewCell) with

-(BOOL) canPerformAction:(SEL)action withSender:(id)sender {
return (action == @selector(copy:) || action == @selector(test:));
}

-(BOOL)canBecomeFirstResponder {
return YES;
}

/// this methods will be called for the cell menu items
-(void) test: (id) sender {

}

-(void) copy:(id)sender {

}
///////////////////////////////////////////////////////

UIMenuItem not showing in table

  1. Your test function needs to be in the UITableViewCell subclass.
  2. You need to implement canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool in that UITableViewCell subclass and return return action == #selector(test)
  3. In UIMenuController.shared.menuItems = [UIMenuItem(title: "Test", action: #selector(test))] change #selector(test) to #selector(YourCellSubclass.test).
  4. Keep the UITableViewDelegate methods you have in your view controller, and change || action == #selector(test) to || action == #selector(YourCellSubclass.test)

EDIT:
Adding working example.

ViewController:

class ViewController: UITableViewController {

override func viewDidLoad() {
super.viewDidLoad()

UIMenuController.shared.menuItems = [UIMenuItem(title: "Test", action: #selector(MyCell.test))]
UIMenuController.shared.update()

tableView.register(MyCell.self, forCellReuseIdentifier: "my")
// Do any additional setup after loading the view, typically from a nib.
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "my", for: indexPath) as! MyCell
cell.textLabel?.text = "\(indexPath.row)"
return cell
}

override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
return true
}

override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
return action == #selector(MyCell.test)
}

override func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
// needs to be here
}

}

Cell:

class MyCell: UITableViewCell {

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(test)
}

@objc func test() {
print("works")
}

}

Sample Image

show UIMenuController in UITableViewCell, grouped style

Yes!

Call [[UIMenuController sharedMenuController] setMenuVisible:YES animated:ani] (where ani is a BOOL determining whether the controller should be animated) from within - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath ( UITableView's delegate method)

Edit: The 'copy' command on the UIMenuController will not by default copy the detailTextLabel.text text. However, there is a workaround. Add the following code into your class.

-(void)copy:(id)sender {
[[UIPasteboard generalPasteboard] setString:detailTextLabel.text];
}


- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if(action == @selector(copy:)) {
return YES;
}
else {
return [super canPerformAction:action withSender:sender];
}
}

Showing UIMenuController deselects UITableViewCell

A simpler way of implementing this is using the specific UITableViewDelegate methods for dealing with UIMenuController.
But first, to make the cell stay selected, store the value of the cell presenting the menu in your class:

NSIndexPath                     *_editingIndexPath;

Then implement the UITableViewDelegateMethods:

- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyCustomTableViewCell *cell = (MyCustomTableViewCell *) [_tableView cellForRowAtIndexPath:indexPath];
_editingIndexPath = indexPath;
cell.showingMenu = YES;

return YES;
}

- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
if (action == @selector(copy:)) {
return YES;
}

return NO;
}


- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
if (action == @selector(copy:))
{
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
if (cell && [cell isKindOfClass:[MessageConversationCell class]])
{
[UIPasteboard generalPasteboard].string = cell.textLabel.text;
}
}
}

The code above will take care of showing a "copy" menu on the cell after a long press.
Now, if you want the cell to stay selected while the menu is displayed:

Add a @property in your custom cell named "showingMenu" (note that this property was already set in the first block of code in this answer).

@property (nonatomic, assign) BOOL showingMenu;

Add (or modified if already present) the following method to your custom cell. This will take care of keeping the cell highlighted after the menu tried to unhighlight it (you may implement your own logic for highlighting a cell, in that case put it in the first branch of the if conditional):

- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{

if (_showingMenu)
{
[super setHighlighted:YES]
}
else
{
[super setHighlighted:highlighted];
}
}

Add an observer to be notified when the menu is going to be presented. This goes into the view controller, NOT in the custom cell:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didShowEditMenu:) name:UIMenuControllerDidShowMenuNotification object:nil];

Add on the view controller the method to be called when the menu is displayed:

- (void)didShowEditMenu:(NSNotification *)not {
[_tableView selectRowAtIndexPath:_editingIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
MyCustomTableViewCell *cell = (MyCustomTableViewCell*)[_conversationTableView cellForRowAtIndexPath:_editingIndexPath];
cell.showingMenu = NO;
}

And don't forget to remove the observer when no longer needed:

[[NSNotificationCenter defaultCenter] removeObserver:self name:UIMenuControllerDidShowMenuNotification object:nil];

This will show a menu when a cell is long pressed, and keep the cell selected until the menu disappears, either because an option was chosen or because the user tapped somewhere else. It works pretty much like Whatsapp works when you select a message.

How to get the tapped table view cell in custom action in UIMenuController

Thanks valheru. I find a "nice" approach to achieve that:)

Step one: In MyTableViewController.m

- (void)viewDidLoad
{
[super viewDidLoad];

UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
longPressGesture.minimumPressDuration = .5;
longPressGesture.delegate = self;
[self.view addGestureRecognizer:longPressGesture];
}

which register the gesture recognizer of the long press on the table view controller.

- (BOOL)canBecomeFirstResponder
{
return YES;
}

which allows MyTableViewController response the long press and popup the context menu.

-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
if(gestureRecognizer.state == UIGestureRecognizerStateBegan)
{
CGPoint point = [gestureRecognizer locationInView:self.tableView];
NSIndexPath * indexPath = [self.tableView indexPathForRowAtPoint:point];
if(indexPath == nil) return ;

MyCell *cell = (MyCell *)[self.tableView cellForRowAtIndexPath:indexPath];
UIMenuItem *determine = [[UIMenuItem alloc] initWithTitle:@"My Action on this cell" action:@selector(handleMyAction:)];
UIMenuController *menu = [UIMenuController sharedMenuController];
[menu setMenuItems:[NSArray arrayWithObjects:determine, nil]];
[menu setTargetRect:cell.frame inView:cell.superview];
[menu setMenuVisible:YES animated:YES];
[cell becomeFirstResponder]; //here set the cell as the responder of the menu action
cell.delegate = self;// this is optional, if you don't want to implement logic in cell class
}
}

create the UIMenuController and popup when i long press the cell.

-(void)handleMyAction: (UITableViewCell *)cell
{
NSLog(@"%@", cell);
}

this function will be called later from the cell which is pressed.

Step two: Create a subclass of UITableViewCell named MyCell

In MyCell.h defines the table view controller the cell belongs as the delegate of the cell. And the callback function when the menu entry clicked

@property (nonatomic, strong) MyTableViewController *delegate;
-(void)handleMyAction:(id)sender;

in MyCell.m

- (BOOL)canBecomeFirstResponder
{
return YES;
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if(action == @selector(handleMyAction:))
{
return YES;
}
return NO;
}

allows the MyCell to be the first responder and response the handleMyAction action by clicking on menu entry.

-(void)handleMyAction:(id)sender
{
[self.delegate handleMyAction:self]; //it's a coincidence both functions have the same name:)
}

this is the definition of the callback function, which will be called when click on the menu entry, and it in turn call the handMyAction function in the delegate of the cell (MyTableViewController, where the logic regarding the cell could be implemented.)



Related Topics



Leave a reply



Submit