8

I'm using a UIMenuItem to perform a custom action in UICollectionView cell long press. this worked perfectly with iOS 6, but now I am converting my application to iOS 7 and Xcode 5 and it don't work. The custom item do not shown.

UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:@"Unfavorite"
                                                  action:@selector(unFavorite:)];
[[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObject:menuItem]];
[UIMenuController sharedMenuController].menuVisible = YES;

- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender;
{
    //do not show default itens like copy, paste....
    [self becomeFirstResponder];
    return NO;
}


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

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

}

 - (BOOL)collectionView:(UICollectionView *)collectionView
 shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath {

    myIndexPath = indexPath;
    return YES;
}
Nikola Kirev
  • 3,152
  • 3
  • 22
  • 30
Raw
  • 139
  • 1
  • 2
  • 6
  • 1
    Exactly the same issue. I discovered that is now the custom cell that is becoming first responder for the menu. So, if you implement CanPerformAction and the custom action selectors on your custom cell, it is working. But, I would prefer to keep the selectors on the view controller. – nicolas Sep 20 '13 at 06:58
  • Yes Abramov, you're right. I implemented on custom cell and works, but a prefer keep selectors on the view controller too. what you suggest to use this feature while keeping the selector in the VC? maybe delegate a protocol... – Raw Sep 20 '13 at 14:58
  • It's a solution but not very elegant. I keep trying to find a better one. – nicolas Sep 23 '13 at 07:30
  • Adding a delegate to the collection VC in the cell, causes the collection VC to never dealloc (see code as answer). We need to find better solution in the long run. – Gamma-Point Sep 24 '13 at 05:15

4 Answers4

8

I don't know about iOS 6, but in iOS 7 it's very simple. You just need the three standard collection view delegate menu-handling methods, plus an action method in the cell subclass. There is no need to play with first responder or anything like that. So, for example (in this example, Copy is a standard item but Capital is something I've added to the menu):

// collection view delegate:

- (BOOL)collectionView:(UICollectionView *)collectionView 
        shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath {
    UIMenuItem* mi = [[UIMenuItem alloc] initWithTitle:@"Capital" 
                      action:NSSelectorFromString(@"capital:")];
    [[UIMenuController sharedMenuController] setMenuItems:@[mi]];
    return YES;
}

- (BOOL)collectionView:(UICollectionView *)collectionView 
        canPerformAction:(SEL)action 
        forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
    return (action == NSSelectorFromString(@"copy:") || 
            action == NSSelectorFromString(@"capital:"));
}

- (void)collectionView:(UICollectionView *)collectionView 
        performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath 
        withSender:(id)sender {
    // in real life, would do something here
    NSString* state = (self.sectionData)[indexPath.section][indexPath.row];
    if (action == NSSelectorFromString(@"copy:"))
        NSLog(@"copying %@", state);
    else if (action == NSSelectorFromString(@"capital:"))
        NSLog(@"fetching the capital of %@", state);
}

// cell subclass:

-(void)capital:(id)sender {
    // find my collection view
    UIView* v = self;
    do {
        v = v.superview;
    } while (![v isKindOfClass:[UICollectionView class]]);
    UICollectionView* cv = (UICollectionView*) v;
    // ask it what index path we are
    NSIndexPath* ip = [cv indexPathForCell:self];
    // talk to its delegate
    if (cv.delegate && 
        [cv.delegate respondsToSelector:
             @selector(collectionView:performAction:forItemAtIndexPath:withSender:)])
        [cv.delegate collectionView:cv performAction:_cmd
             forItemAtIndexPath:ip withSender:sender];
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Nice alternative that works without the extra stuff from iOS 6.0. I think it'd be nicer to encapsulate behavior to look up the UICollectionView in a method to reuse for multiple actions. Is there ever a chance that while lookup fails? – Paul Solt Oct 07 '13 at 19:35
  • No. The one thing you know is that the cell is, at some depth, a subview of a collection view. If it were not, it would not be the first responder and hence would not be receiving this message to begin with. — Obviously the *entire* method I've shown should be abstracted for multiple actions; it is completely general (note the use of `_cmd`). – matt Oct 08 '13 at 03:22
  • what about the x-position of the menu in the cell is it customizable? – Nava Carmon Jan 06 '15 at 16:40
  • @NavaCarmon Please don't use comments for leeching. If you have a new question, ask a new question. – matt Jan 06 '15 at 16:51
  • @matt I would like 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 05:36
  • @S.J Well, I guess I don't need to! Very nice answer there, well deserving of the bounty. Excellent. – matt Jul 06 '15 at 15:46
  • @matt yes what I want is accomplished, but confusion is still there why setTargetRect didn't work any where accept in UIMenuControllerWillShowMenuNotification. – S.J Jul 07 '15 at 05:54
1

I updated my solution for iOS 7.0 with images and examples in a similar StackOverflow issue.

I use ARC and weak references for the delegate. Seems to work on both iOS 6.0 and iOS 7.0

https://stackoverflow.com/a/13618212/276626

Community
  • 1
  • 1
Paul Solt
  • 8,375
  • 5
  • 41
  • 46
1

I had added a searchBar on my UICollectionView with the result of the UIMenuController going MIA. After two days trial & error I think finally found a way to make it work.

The Problem (running on iOS7):

  1. UILongPressGestureRecognizer on UICollectionViewController is called
  2. The MenuController shows
  3. Added a UISearchBar and as soon as the collectionView reloads its data: No more menu controller

I think the trick to make it work is to explicitly remove the first responder status from the searchBar:

- (void)longPressGestureDetected:(UILongPressGestureRecognizer *)gesture {
if(gesture.state == UIGestureRecognizerStateBegan) {
    CGPoint touchPoint = [gesture locationInView:gesture.view];
    NSInteger index = [self.collectionView indexPathForItemAtPoint:touchPoint].item;
    if(index >= 0 && index < self.documents.count) {
        // dismiss searchBar
        [self.searchBar resignFirstResponder];
        [self becomeFirstResponder];
        // select the right document
        //[self.documentManager selectDocumentWithIndex:index];
        // show menu
        UIMenuController *menu = [UIMenuController sharedMenuController];
        menu.menuItems = [self defaultMenuItems];
        [menu setTargetRect:CGRectMake(touchPoint.x, touchPoint.y, 0, 0) inView:gesture.view];
        [gesture.view becomeFirstResponder];
        [menu update];
        [menu setMenuVisible:YES animated:YES];
    }
}

of course there are also these responder status methods in the controller:

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

- (NSArray*)defaultMenuItems {
    // add menu items
    UIMenuItem *renameItem      = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"Rename", @"Rename Menu Item")           action:@selector(renameDocument:)];
    return @[renameItem];
}
auco
  • 9,329
  • 4
  • 47
  • 54
0

as metioned by nicolas, the relevant code follows. Note: This causes the collection VC to never release (meaning the dealloc is never called). We need to find better solution in the long run or until Apple fixes this iOS 7.x bug.

in NibCell.h

@protocol CellToVCDelegate <NSObject>

@optional

- (void)deleteActivity:(id)sender ;
- (void)shareActivity:(id)sender;

@end

@interface NibCell : UICollectionViewCell{

    id <CellToVCDelegate> delegate;
  }

@property (nonatomic, retain) id <CellToVCDelegate> delegate;

in NibCell.m

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

    [self.delegate deleteActivity:sender];


}

- (void)shareActivity:(id)sender {
    NSLog(@"shareActivity action! %@", sender);
    [self.delegate shareActivity:sender];



}

in the collection VC.h

@interface VC : UIViewController <

                        CellToVCDelegate>

in VC.m:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NibCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cvCellIdentifier
                                                              forIndexPath:indexPath];

cell.delegate = self;
return cell;
}
Gamma-Point
  • 1,514
  • 13
  • 14
  • 1
    It will never dealloc because your cell has a `strong` reference to its delegate (the view controller) and the view controller has a strong reference to the cell. The `delegate` property should be `weak` (and you don't need to declare an instance variable, either). – jbrennan Sep 30 '13 at 19:43
  • for non-arc:changed the retain to assign @property (nonatomic) id delegate; – Gamma-Point Oct 02 '13 at 03:15