26

I want the UIMenuController that pops up when I long-press a UITableViewCell to show custom UIMenuItems.

I set up the custom item in viewDidLoad

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

And then I set all the right delegate methods.

- (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:) || action == @selector(test:));
}

- (BOOL)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
    if (action == @selector(copy:)) {
         // do stuff
    }

    return YES;
}

But all it does, is show the "Copy" item, since I only allow it and my custom item. The custom item, however, won't show up.

I realize, I could add a gesture recognizer to the cell itself, but that kind of defeats the purpose of the shared instance of UIMenuController, doesn't it?

leberwurstsaft
  • 405
  • 1
  • 5
  • 11

4 Answers4

52

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 {

}
///////////////////////////////////////////////////////
A-Live
  • 8,904
  • 2
  • 39
  • 74
  • 2
    Oh, the second part was the solution. Thank you for rubbing my nose with the relevant documentation, as well! – leberwurstsaft Sep 06 '12 at 14:51
  • 2
    Thank you, it helped me too, but I performed one change: I implemented my custom method (`test:` in your example) and the responder query method (`canPerformAction:withSender:`) on my view controller instead of in a table view subclass. `UIResponder` takes care of going through the responder chain, so a subclass shouldn't be needed if it's only meant for this problem. This spares me a headache — as long as the custom method has a pretty distinct name to prevent collisions. ;-) – Constantino Tsarouhas Sep 30 '12 at 18:21
  • Thanks for the comment, @Randy Marsh, i only hope your view controller will not get as large as thousands of lines :) – A-Live Oct 01 '12 at 11:59
  • @A-live I prefer placing things in one file and use `#pragma mark` to structure things than to have many classes and files. But yes, I easily gets to thousands of lines. ;-p – Constantino Tsarouhas Oct 01 '12 at 18:33
  • I am getting issues to get called `canPerformAction` when coming on this UITableView Controller from UITabBar action menu. – User16119012 Feb 18 '14 at 11:50
  • @Rakesh Ostwal, make sure you are using the subclass implementing the methods, not another subclass or standard `UITableViewCell`. If you don't want to subclass `UITableViewCell`, the methods can be implemented at "super" responder, e.g. your view controller. – A-Live Feb 18 '14 at 15:29
  • @A-Live: Thanks. Can you please look into my question once at http://stackoverflow.com/questions/21852971/ios-6-canperformaction-not-getting-called-on-uitableviewcontroller – User16119012 Feb 18 '14 at 15:56
  • I fixed your BOOL return in the performAction tableView method, it should have been a void. – malhal Feb 18 '14 at 22:08
  • @indiekiduk You are right, not sure why your edit was rejected, let me fix it. – A-Live Feb 20 '14 at 12:21
  • @A-Live: This solution is not working on ios 7. Any idea on this? – User16119012 Mar 25 '14 at 07:38
  • @Rakesh Ostwal I've checked it and am positive that the solution is working fine at iOs 7.1. Can't really see any reason why it would stop working, you'll need to check your implementation. – A-Live Mar 25 '14 at 13:06
  • @A-Live: I have checked my implementation. It is as same as answer you gave. I am testing it on iPad ios 7.0.4. Copy menu item is displaying but custom menu item is not coming. – User16119012 Mar 25 '14 at 17:27
  • Instead all standard menu items are showing, but custom menu item is not coming. Is it possible it is not getting added to sharedMenuController's menuItems? – User16119012 Mar 25 '14 at 17:39
  • @Rakesh Ostwa, make sure the methods from `2)` are implemented at your custom cell, tableview or any other UI class you are using that is involved at the actions chain. – A-Live Mar 26 '14 at 09:50
  • @A-Live I would like you to invite you to answer this question http://stackoverflow.com/questions/31183894/uimenucontroller-method-settargetrectinview-not-working-in-uitableview – S.J Jul 06 '15 at 06:26
8

To implement copy and a custom action for UITableViewCell:

Once in your application, register the custom action:

struct Token { static var token: dispatch_once_t = 0 }
dispatch_once(&Token.token) {
    let customMenuItem = UIMenuItem(title: "Custom", action: #selector(MyCell.customMenuItemTapped(_:))
    UIMenuController.sharedMenuController().menuItems = [customMenuItem]
    UIMenuController.sharedMenuController().update()
}

In your UITableViewCell subclass, implement the custom method:

func customMenuItemTapped(sender: UIMenuController) {
    // implement custom action here
}

In your UITableViewDelegate, implement the following methods:

override func tableView(tableView: UITableView, shouldShowMenuForRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}

override func tableView(tableView: UITableView, canPerformAction action: Selector, forRowAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) -> Bool {
    return action == #selector(NSObject.copy(_:)) || action == #selector(MyCell.customMenuItemTapped(_:))
}

override func tableView(tableView: UITableView, performAction action: Selector, forRowAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) {
    switch action {
    case #selector(NSObject.copy(_:)):
        // implement copy here
    default:
        assertionFailure()
    }
}

Notes:

Community
  • 1
  • 1
Senseful
  • 86,719
  • 67
  • 308
  • 465
  • thanks this really helped me. this works when you long press on the cell, however can we perform the same when i just click on the cell ? and also how can i remove the "Copy" option, its still there along with my custom times. thanks :) – spaceMonkey Jul 20 '16 at 08:26
  • 1
    Note that using Xcode8/swift3 and supplying `return action == "copy:"` to `canPerformAction` triggers a Fix-It that generates `return action == #selector(UIResponderStandardEditActions.copy(_:))` – Victor Bogdan Dec 09 '16 at 13:01
2

Example allowing copy only row 0 of section 0

Updated to Swift 5.2

func shouldAllowCopyOn(indexPath: IndexPath) -> Bool {
    if indexPath.section == 0 && indexPath.row == 0 {
       return true
    }
    return false
}

func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
    return self.shouldAllowCopyOn(indexPath: indexPath)
}

func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
    if (action == #selector(UIResponderStandardEditActions.copy(_:))) {
          return self.shouldAllowCopyOn(indexPath: indexPath)
    }
}

func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
    if (action == #selector(UIResponderStandardEditActions.copy(_:)) && self.shouldAllowCopyOn(indexPath: indexPath)) {
       if let cell = self.tableView.cellForRow(at: indexPath) as? UITableViewCell {
         self.copyAction(cell: cell)
        }
    }
}

@objc
private func copyAction(cell: UITableViewCell) {
    UIPasteboard.general.string = cell.titleLabel.text
}
Reinier Melian
  • 20,519
  • 3
  • 38
  • 55
1

SWIFT 3:

In AppDelegate didFinishLaunchingWithOptions:

let customMenuItem = UIMenuItem(title: "Delete", action:
#selector(TableViewCell.deleteMessageActionTapped(sender:)))
        UIMenuController.shared.menuItems = [customMenuItem]
        UIMenuController.shared.update()

in your TableViewContoller Class:

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(copy(_:)) || action == #selector(TableViewCell.yourActionTapped(sender:))
    }



 override func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
   if action == #selector(copy(_:)) {
        let pasteboard = UIPasteboard.general
        pasteboard.string = messages[indexPath.row].text
   }
}
Yaroslav Dukal
  • 3,894
  • 29
  • 36
  • You should only perform the UIMenuController setup once for the lifetime of your application. Otherwise you'll get additional menu items each time the view is loaded. – James Bedford Aug 06 '18 at 14:11
  • that's why it's in viewDidLoad? – Yaroslav Dukal Aug 06 '18 at 21:36
  • If you were to navigate away from the UIViewController's view it will be released to save memory. viewDidLoad will then be called again the next time the view is loaded. You should be fine if the UIViewController is the root view but if you ever move the UIViewController you're going to end up with duplicate menu items for subsequent resists to the UIViewController. viewDidLoad may be called multiple times within your app's lifetime whereas your AppDelegate's applicationDidFinishLaunching: will be called once, so I think this is a better place to setup the menu. – James Bedford Aug 07 '18 at 08:09