8

I have a UICollectionView with UIImageView in each cell, now I want to add Copy Callout, like in Photos.app:

enter image description here

I saw this method in the UICollectionViewDelegate:

- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

After few additional minutes of research I found UIMenuController Class, as I understood, I must to work with it to get the Menu, but anyway, I think that there must to be more simple way then creating UIGestureRecognizer, and creating, positioning etc. my UIMenu.

Am I on the right track? How could you implement this feature?

Anatoliy Gatt
  • 2,501
  • 3
  • 26
  • 42

3 Answers3

18

Yes you're on the right track. You can also implement custom actions beyond cut, copy, paste using this technique.

Custom actions for the UICollectionView

// ViewController.h
@interface ViewController : UICollectionViewController

// ViewController.m
-(void)viewDidLoad
{
    [super viewDidLoad];
    self.collectionView.delegate = self;

    UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:@"Custom Action"
                                                      action:@selector(customAction:)];
    [[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObject:menuItem]];

}

#pragma mark - UICollectionViewDelegate methods
- (BOOL)collectionView:(UICollectionView *)collectionView
      canPerformAction:(SEL)action
    forItemAtIndexPath:(NSIndexPath *)indexPath
            withSender:(id)sender {
    return YES;  // YES for the Cut, copy, paste actions
}

- (BOOL)collectionView:(UICollectionView *)collectionView
shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

- (void)collectionView:(UICollectionView *)collectionView
         performAction:(SEL)action
    forItemAtIndexPath:(NSIndexPath *)indexPath
            withSender:(id)sender {
    NSLog(@"performAction");
}

#pragma mark - UIMenuController required methods
- (BOOL)canBecomeFirstResponder {
    // NOTE: The menu item will on iOS 6.0 without YES (May be optional on iOS 7.0)
    return YES;
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    NSLog(@"canPerformAction");
     // The selector(s) should match your UIMenuItem selector
    if (action == @selector(customAction:)) {
        return YES;
    }
    return NO;
}

#pragma mark - Custom Action(s)
- (void)customAction:(id)sender {
    NSLog(@"custom action! %@", sender);
}

Note: iOS 7.0 Changes Behavior

  1. In your UICollectionViewCell subclass you'll need to add the custom action methods, or nothing will appear.

    // Cell.m
    #import "Cell.h"
    
    @implementation Cell
    
    - (id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            // custom logic
        }
        return self;
    }
    
    - (void)customAction:(id)sender {
        NSLog(@"Hello");
    
        if([self.delegate respondsToSelector:@selector(customAction:forCell:)]) {
            [self.delegate customAction:sender forCell:self];
        }
    }
    @end
    
  2. You'll need to create a delegate protocol and set it on every cell to call back to the UIController that maintains your UICollectionView. This is because the cell should no nothing about your model, since it only is involved in displaying content.

    // Cell.h
    #import <UIKit/UIKit.h>
    
    @class Cell; // Forward declare Custom Cell for the property
    
    @protocol MyMenuDelegate <NSObject>
    @optional
    - (void)customAction:(id)sender forCell:(Cell *)cell;
    @end
    
    @interface Cell : UICollectionViewCell
    
    @property (strong, nonatomic) UILabel* label;
    @property (weak, nonatomic) id<MyMenuDelegate> delegate;
    @end
    
  3. In your ViewController or Subclass of UICollectionViewController you'll need to conform to the protocol and implement the new method.

    // ViewController.m
    @interface ViewController () <MyMenuDelegate>
    @end
    
    // @implementation ViewController  ...
    
    - (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath;
    {
        Cell *cell = [cv dequeueReusableCellWithReuseIdentifier:@"MY_CELL" forIndexPath:indexPath];
        cell.delegate = self;
        return cell;
    }
    // ...
    
    // Delegate method for iOS 7.0 to get action from UICollectionViewCell
    - (void)customAction:(id)sender forCell:(Cell *)cell {
        NSLog(@"custom action! %@", sender);
    }
    

    Custom longpress action menu for UICollectionView

  4. Optional: In your UIView Subclass you can override the default Cut, Copy, Paste if you implement the method canPerformAction here, rather than in the UIViewController. Otherwise the behavior will show the default methods before your custom methods.

    // Cell.m
    - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
        NSLog(@"canPerformAction");
        // The selector(s) should match your UIMenuItem selector
    
        NSLog(@"Sender: %@", sender);
        if (action == @selector(customAction:)) {
            return YES;
        }
        return NO;
    }
    

    Custom Action from UICell canPerformAction

Paul Solt
  • 8,375
  • 5
  • 41
  • 46
  • 3
    In customAction, sender is the UIMenuController. How do you get reference to the cell from that? – drhr Mar 09 '13 at 05:23
  • @Odelya I think you can use the method to store the last cell that has requested the action. - (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath You can get the indexPath there. – Paul Solt Oct 03 '13 at 18:23
  • Strange. It looks like this code works in iOS 6.0, but not on iOS 7.0. Were you able to run the code to show a custom item on iOS 7.0? – Paul Solt Oct 03 '13 at 18:31
  • 1
    Looks like this Stackoverflow helps with iOS 7. Need calls in the custom cell: http://stackoverflow.com/questions/18906991/uimenucontroller-sharedmenucontroller-custom-menuitem-for-uicollectionview-do – Paul Solt Oct 03 '13 at 19:10
  • I've updated the post to include iOS 7.0 support. It appears to work on iOS 6 with the new changes. (You'll probably have to quit/restart Xcode or the Simulator to switch between, that's what I had to do) – Paul Solt Oct 03 '13 at 20:08
  • http://stackoverflow.com/questions/18823679/uicollectioncell-to-call-method-in-collectionviewcontroller – DogCoffee Oct 03 '13 at 20:26
  • I had an issue with UISearchBar preventing the firstResponder status. I got UIMenuController to show now on iOS7 without subclassing UICollectionViewCell: http://stackoverflow.com/a/19183509/388412 – auco Oct 04 '13 at 14:16
  • 2
    For iOS 7 this is way over-engineered. It's much simpler: see my answer with complete code at http://stackoverflow.com/a/19232074/341994 – matt Oct 07 '13 at 18:35
  • Hi, I am getting menu controller on bottom of the cell where as i want it to show on Top. It is not working even though i put arrow direction down. Any help? – Rajeev May 30 '14 at 05:59
  • 1
    @PaulSolt : solved my problem, i had been trying since three four days ! Insort, thanks for saving my time. – Chintan Aug 28 '15 at 01:00
10

This is the full solution:

- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath {
        return YES;
    }

- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
        if ([NSStringFromSelector(action) isEqualToString:@"copy:"])
            return YES;
        else
            return NO;
    }

- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
        if ([NSStringFromSelector(action) isEqualToString:@"copy:"]) {
            UIPasteboard *pasteBoard = [UIPasteboard pasteboardWithName:UIPasteboardNameGeneral create:NO];
            pasteBoard.persistent = YES;
            NSData *capturedImageData = UIImagePNGRepresentation([_capturedPhotos objectAtIndex:indexPath.row]);
            [pasteBoard setData:capturedImageData forPasteboardType:(NSString *)kUTTypePNG];
        }
    }

In my case, I'm allowing only Copy feature in my CollectionView, and if the copy is pressed, I'm copying the image that is inside the cell to the PasteBoard.

Anatoliy Gatt
  • 2,501
  • 3
  • 26
  • 42
  • Yeah you only need these 3 methods. Anything else is unnecessary. – Samuel Goodwin Sep 21 '13 at 22:33
  • Thanks, I tried you solution, while writing UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:@"Edit" action:@selector(editPlate:)]; however, it requires me to have a method editPlate but I would like to use performAction, so I can know the cell id. How do you locate the menu item? – Dejell Sep 30 '13 at 14:20
5

Maybe a bit late but i maybe found a better solution for those who are still search for this:

In viewDidLoad of your UICollectionViewController add your item:

UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:@"Title" action:@selector(action:)];
[[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObject:menuItem]];

Add the following delegate methods:

//This method is called instead of canPerformAction for each action (copy, cut and paste too)
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
        if (action == @selector(action:)) {
            return YES;
        }
        return NO;
    }
    //Yes for showing menu in general
    - (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath {
        return YES;
    }

Subclass UICollectionViewCell if you didn't already. Add the method you specified for your item:

- (void)action:(UIMenuController*)menuController {

}

This way you don't need any becomeFirstResponder or other methods. You have all actions in one place and you can easily handle different cells if you call a general method with the cell itself as a parameter.

Edit: Somehow the uicollectionview needs the existence of this method (this method isn't called for your custom action, i think the uicollectionview just checks for existance)

- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {

}
Dejell
  • 13,947
  • 40
  • 146
  • 229
Nilz11
  • 1,242
  • 14
  • 16
  • @ThinkBonobo Thanks for your comment but actually it was my edit, he just improved the English ;-). – Nilz11 Oct 22 '14 at 08:21
  • I found this answer particularly helpful. However, I still had to end up creating a delegate that referred back to the collection controller so that I could refresh the collection controller. Also, pay attention to the edit. that empty performAction class is needed. – ThinkBonobo Oct 23 '14 at 03:17