Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Copy Callout in UICollectionView

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?

like image 375
Anatoliy Gatt Avatar asked Nov 19 '12 16:11

Anatoliy Gatt


3 Answers

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

like image 188
Paul Solt Avatar answered Nov 09 '22 12:11

Paul Solt


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.

like image 38
Anatoliy Gatt Avatar answered Nov 09 '22 13:11

Anatoliy Gatt


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 {

}
like image 37
Nilz11 Avatar answered Nov 09 '22 13:11

Nilz11