42

Is there a simple way for subclasses of UITableViewCell to show the 'Copy' UIMenuController popup like in the Address book app (see screenshot), after the selection is held for a while?

address book
(source: icog.net)

7 Answers 7

42

There is now official interface for displaying UITableView cell menus in iOS 5. Example (from the table delegate):

- (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
{
    if (action == @selector(copy:)){
        UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
        [[UIPasteboard generalPasteboard] setString:cell.textLabel.text];
    }
}

I tried modifying the UIMenuController's shared controller to add my own menu item, and I was able to add it and get the canPerformAction message for it, but returning YES didn't help; I wasn't able to make my custom menu item appear. From my experiments, it looks like only Copy, Cut, and Paste are supported. [EDIT Since this was posted, I've learned how to add custom menu items.]

Note that this works only if all three delegate methods are implemented.

7
  • 4
    Add this under NSLog to copy to pasteboard UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath]; [[UIPasteboard generalPasteboard] setString:cell.detailTextLabel.text];
    – malaki1974
    Commented May 17, 2013 at 17:21
  • Yeh that was essential so I edited his answer with that :-)
    – malhal
    Commented Oct 22, 2016 at 1:09
  • The shouldShowMenuForRowAtIndexPath & canPerformAction APIs are both deprecated in iOS 13. Is there any built-in way to copy without swiping? Or I should just add a gesture to my label?
    – mfaani
    Commented Oct 16, 2019 at 14:53
  • @Honey Those APIs are completely and coherently replaced by tableView(_:contextMenuConfigurationForRowAt:point:) so what's the problem supposed to be? I do not know what you imagine "swiping" has to do with this.
    – matt
    Commented Oct 16, 2019 at 15:06
  • Thanks. I wrote an answer based off your suggestion. Yet the UI looks different (I added an image to my answer) + Settings app on iOS 13 >> General >> About, Apple is not using that API themselves. I guess Apple has the old API ie they're using deprecated code...
    – mfaani
    Commented Oct 16, 2019 at 19:55
31

The method before iOS 5 is to get the UIMenuController's shared instance, set the target rect and view and call -setMenuVisible:animated:. Remeber to implement -canPerformAction:withSender: in your responder.


The method after iOS 5 (previously available as undocumented feature) is to implement these 3 methods in your data source (see https://developer.apple.com/reference/uikit/uitableviewdelegate#1653389).

-(void)tableView:(UITableView*)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath*)indexPath withSender:(id)sender;
-(BOOL)tableView:(UITableView*)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath*)indexPath withSender:(id)sender;
-(BOOL)tableView:(UITableView*)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath*)indexPath; 
5
  • Thanks! The undocumented method is exactly what I was looking for. The "official" method is rather clunky. Hopefully it becomes documented soon. It isn't ideal that it is undocumented, but in this case it's just implementing the protocol, not calling a method, so hopefully it's safe(er). Commented Mar 22, 2010 at 3:56
  • Why these methods are still undocumented? They are some kind of private methods? If they are then your app may be not accepted by the app store.
    – dombesz
    Commented Feb 24, 2011 at 14:30
  • These are documented as of iOS 5.0 as part of the UITableViewDelegate protocol: developer.apple.com/library/ios/#DOCUMENTATION/UIKit/Reference/….
    – Nate Cook
    Commented Mar 22, 2012 at 8:15
  • Any solution for supported iOS below 5.0?
    – alloc_iNit
    Commented May 16, 2013 at 10:46
  • @alloc_iNit actually this code will work on iOS way back to 3.0, it's just that it wasn't documented before iOS 5. Commented Aug 8, 2013 at 4:19
30

Here is the Swift syntax for copying detailTextLabel.

func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
    return (tableView.cellForRow(at: indexPath)?.detailTextLabel?.text) != nil
}

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

func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
    if action == #selector(copy(_:)) {
        let cell = tableView.cellForRow(at: indexPath)
        let pasteboard = UIPasteboard.general
        pasteboard.string = cell?.detailTextLabel?.text
    }
}
14

Your UITableViewCell subclass may look like this

@interface MenuTableViewCell : UITableViewCell {
}
- (IBAction)copy:(id)sender;
- (void)showMenu;

@end


@implementation MenuTableViewCell

- (BOOL)canBecomeFirstResponder {
    return YES;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
    if (action == @selector(copy:)) {
        return YES;
    }
    return NO;
}
- (IBAction)copy:(id)sender {
}
- (void)showMenu {
    [[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
    [self becomeFirstResponder];
    [[UIMenuController sharedMenuController] update];
    [[UIMenuController sharedMenuController] setTargetRect:CGRectZero inView:self];
    [[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES];

}

@end

And the UITableView delegate methods are like

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    MenuTableViewCell *cell = (MenuTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[MenuTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    // Configure the cell.
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    MenuTableViewCell *cell = (MenuTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
    [cell showMenu];
}
4
  • Thanks for the answer. This is a good way to do it "officially" I guess, but what I don't like is that it shows the Copy view when selected, where as the Address Book app shows the copy view when held for X seconds. This isn't an issue with your solution, but rather with the API. Thanks very much for your help. Commented Mar 23, 2010 at 6:00
  • When I try this solution it appears to work, but after the menu is shown, further calls to [self.tableView reloadData] don't call the UITableViewDataSource callbacks: numberOfSections, etc. It looks like the call to becomeFirstResponder is the method that whacks the tableView from reloading, but the becomeFirstResponder call is needed to show the menu. It's hard to believe something else isn't going on. Very confused. Perhaps I'll start a new project to see if I can reproduce in isolation.
    – Daniel
    Commented Mar 31, 2011 at 7:11
  • I reproduced the problem in a fresh, stripped down project. In my custom UITableViewCell I had implemented: - (BOOL)resignFirstResponder because I thought this would be a good place to clear the custom menu items from the sharedMenuController. If you don't clear the custom menu items, they get attached to other UITextFields in the same view - where they don't make sense. In any case, even if my resignFirstResponder does nothing except return YES, it still causes subsequent [tableView reloadData] calls to fail. This is very strange to me. I guess I'll look for another place to clear the menu.
    – Daniel
    Commented Mar 31, 2011 at 8:04
  • Hi Daniel, were you able to resolve this issue? I had the same issue as your Commented May 9, 2011 at 16:08
10
 #pragma mark - COPY/PASTE Cell Text via Menu

- (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
{
    if (action == @selector(copy:))
    {
        UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
        UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard];
        [pasteBoard setString:cell.textLabel.text];
    }
}
0
2

For pre iOS13, refer to Alexander's answer. For iOS13 shouldShowMenuForRowAt and canPerformAction are deprecated, hence you have to do use the following API:

@available(iOS 13.0, *)
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {

    return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { suggestedActions in

        return self.makeContextMenu(for: indexPath)
    })
}

@available(iOS 13.0, *)
func makeContextMenu(for indexPath: IndexPath) -> UIMenu {

    let copyAction = UIAction(title: "Copy") { [weak self] _ in
        guard let self = self else { return }
        let cell = self.tableView.cellForRow(at: indexPath)
        let pasteboard = UIPasteboard.general
        pasteboard.string = cell?.detailTextLabel?.text

    }

    // Create and return a UIMenu with the share action
    return UIMenu(title: "Options", children: [copyAction])
}

Note: The end result will be different. But this is out of the box functionality that Apple is providing. However checking the Settings app on iOS 13 Sim >> General >> About. long-pressing a cell will not have the UI below, it will be the old UI, which is a bit inconsistent.

enter image description here

1

created 2 scenarios out of Alexander's Code:

1.in case you want to copy textLabel and not detailTextLabel just use this code:

//MARK: Delegate
func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
    return (tableView.cellForRow(at: indexPath)?.textLabel?.text) != nil
}

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

func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
    if action == #selector(copy(_:)) {
        let cell = tableView.cellForRow(at: indexPath)
        let pasteboard = UIPasteboard.general
        pasteboard.string = cell?.textLabel?.text
    }
}

2.if you have customCell with customLabels and you want to copy all customLabels text Do this:

//MARK: Delegate
func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {

    return (tableView.cellForRow(at: indexPath) != nil)
}

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

func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {

    if action == #selector(copy(_:)) {

        /* change these array names according to your own array names */
        let customlabel1 = customlabel1Array[indexPath.row]     
        let customlabel2 = customlabel2Array[indexPath.row]
        let customlabel3 = customlabel3Array[indexPath.row]


        let pasteboard = UIPasteboard.general
        pasteboard.string = "\(customlabel1)\n\(customlabel2)\n\(customlabel3)"     /*    \n is for new line.   */
    }
}

}

By the way you should set your tableView delegate to self in viewDidLoad for these to work, like this:

 override func viewDidLoad() {
  yourTableView.delegate = self
   }
1
  • Is there a way to offset the menu to one edge of the screen instead of the center? I have a table view with chats and hence the cells have dynamic width and are aligned to the left or right
    – Dracula
    Commented Jan 22, 2021 at 6:30

Not the answer you're looking for? Browse other questions tagged or ask your own question.