UIMenuController with custom item not working with UICollectionview
Asked Answered
T

6

6

I have added custom menu controller when long press on UICollectionViewCell

    [self becomeFirstResponder];
    UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:@"Custom Action"
                                                      action:@selector(customAction:)];
    [[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObject:menuItem]];
    [[UIMenuController sharedMenuController] setTargetRect: self.frame inView:self.superview];
    [[UIMenuController sharedMenuController] setMenuVisible:YES animated: YES];

canBecomeFirstResponder Is also being called

- (BOOL)canBecomeFirstResponder {
    // NOTE: This menu item will not show if this is not YES!
    return YES;
}

//This method is not being called

- (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;
}

I have Also Implemented these methods

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


    if([NSStringFromSelector(action) isEqualToString:@"customAction:"]){
        NSLog(@"indexpath : %@",indexPath);
        UIAlertView *alertview = [[UIAlertView alloc] initWithTitle:@"warning.." message:@"Do you really want to delete this photo?" delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
        [alertview show];
        return YES;
    }

    return YES;

}

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

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

Though it is showing only "cut, copy, and paste" menus

Translucid answered 10/6, 2013 at 13:7 Comment(0)
P
8

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 {

}
Piste answered 10/9, 2013 at 17:28 Comment(12)
dont work for ios7 ? any ideas. been stuck on this for a couple days.Acerbate
for me it works exactly like this in ios 7. Does the delegate methods get called?Piste
are you using a custom class for the collectionViewCell?Acerbate
Yes as i mentioned in my answer.Piste
Sorry, i dont know why it does not work. I have a working app in the app store like this.Piste
You can remove, canPerformAction:(SEL)action withSender:(id)sender from your customCell and call this from you VC. But then you have to have the customAction in BOTH the VC and CellSublass - otherwise you get a warning - in the VC stating undeclared selector. The VC custom Action isnt used.Acerbate
I don't have the method in both the VC or the Cell. I let the collectionView do the work for me.Piste
Now it should work exactly like i typed this because i did a copy and paste mistake.Piste
so you dont get a warning as the "action:" method is not the the VC class?Acerbate
No why should I? If you are calling methods with selectors you never get a warning. You only get them if you call them directly ([self action]). If you perform a selector it checks it during runtime.Piste
Actually this is only an if statement. The method action: is called in the cell by the collectionViewPiste
Thank you for pointing that out. It seems that it is new in Xcode 5 and i didn't use Xcode 5 that much. See this thread here: #18571407Piste
W
6

You need to trigger delegate functions from custom UICollectionViewCell

Here is my working sample code for Swift3

CollectionViewController

override func viewDidLoad() {
    super.viewDidLoad()
    let editMenuItem = UIMenuItem(title: "Edit", action: NSSelectorFromString("editCollection"))
    let deleteMenuItem = UIMenuItem(title: "Delete", action: NSSelectorFromString("deleteCollection"))
    UIMenuController.shared.menuItems = [editMenuItem, deleteMenuItem]

}

override func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
    return true
}

override func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
    return action == NSSelectorFromString("editCollection") || action == NSSelectorFromString("deleteCollection")
}

override func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
    print("action:\(action.description)")
    //Custom actions here..
}

Add following functions to your custom UICollectionViewCell

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
    return action == NSSelectorFromString("editCollection") || action == NSSelectorFromString("deleteCollection")
}

To call delegate function from cell (needs to be in your custom UICollectionViewCell)

func editCollection()
{
    let collectionView = self.superview as! UICollectionView
    let d:UICollectionViewDelegate = collectionView.delegate!
    d.collectionView!(collectionView, performAction: NSSelectorFromString("editCollection"), forItemAt: collectionView.indexPath(for: self)!, withSender: self)
}
func deleteCollection()
{
    let collectionView = self.superview as! UICollectionView
    let d:UICollectionViewDelegate = collectionView.delegate!
    d.collectionView!(collectionView, performAction: NSSelectorFromString("deleteCollection"), forItemAt: collectionView.indexPath(for: self)!, withSender: self)
}
Wisconsin answered 10/10, 2016 at 10:59 Comment(1)
thaks your solution, but i have crash. and the error log is "-[roompi.OutgoingTextCell copyCollection]: unrecognized selector sent to instance 0x10bc05fd0 2019-06-17 10:43:41.878416+0700 roompi[3356:428347] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[roompi.OutgoingTextCell copyCollection]: unrecognized selector sent to instance 0x10bc05fd0''"Conservancy
S
4

I've just spent two days trying to figure out the "correct" way of doing this, and barking up the wrong tree with some of the suggestions that are around.

This article shows the correct way of doing this. I hope that by posting it here someone will be saved a few hours.

http://dev.glide.me/2013/05/custom-item-in-uimenucontroller-of.html

Supertonic answered 28/8, 2013 at 0:33 Comment(6)
I haven't begun work on iOS7 yet, but I'm anticipating problems here in light of your comment. Have you found any workaround yet? I think I'm going to tackle it in the next few days.Supertonic
yes I got it to work finally. See my answer to my own question here #18774765Acerbate
Thanks; I think I have that code but will double-check. Glad you got it going.Supertonic
This is working for me on iOS7 as well. Based on Smick's link, it looks like the key is to implement - (BOOL)canPerformAction:(SEL)action withSender:(id)senderSupertonic
While the link you referred might have been a solution, the link no longer works and thus this answer is of no use anymore. So always post the relevant parts of the code instead of just linking to it.Reidreidar
So, then you'll have the code that no longer works in multiple places. Sounds like a plan.Supertonic
N
2

Swift 3 Solution:

Simply do all stuff inside UICollectionView class and assign this class to UICollectionView object.

import UIKit

class MyAppCollectionView: UICollectionView {

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        addLongPressGesture()
    }

    func addLongPressGesture() {
        let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(MyAppCollectionView.longPressed(_:)))
        longPressGesture.minimumPressDuration = 0.5
        self.addGestureRecognizer(longPressGesture)
    }

    func longPressed(_ gesture: UILongPressGestureRecognizer) {

        let point = gesture.location(in: self)
        let indexPath = self.indexPathForItem(at: point)

        if indexPath != nil {

            MyAppViewController.cellIndex = indexPath!.row
            let editMenu = UIMenuController.shared
            becomeFirstResponder()
            let custom1Item = UIMenuItem(title: "Custom1", action: #selector(MyAppViewController.custome1Method))
            let custom2Item = UIMenuItem(title: "Custom2", action: #selector(MyAppViewController.custome2Method))
            editMenu.menuItems = [custom1Item, custom2Item]
            editMenu.setTargetRect(CGRect(x: point.x, y: point.y, width: 20, height: 20), in: self)
            editMenu.setMenuVisible(true, animated: true)
        }

    }

    override var canBecomeFirstResponder: Bool {

        return true
    }
}

class MyAppViewController: UIViewController {

     override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
            // You need to only return true for the actions you want, otherwise you get the whole range of
            //  iOS actions. You can see this by just removing the if statement here.

            //For folder edit
            if action == #selector(MyAppViewController.custome1Method) {
                return true
            }

            if action == #selector(MyAppViewController.custome2Method) {
                return true
            }

            return false
        }
}
Negrito answered 25/2, 2017 at 18:47 Comment(0)
I
1

When people have trouble getting menus to work on long press in a collection view (or table view, for that matter), it is always for one of two reasons:

  • You're using the long press gesture recognizer for something. You cannot, for example, have both dragging and menus in the same collection view.

  • You've forgotten to implement the selector in the cell.

For example, the OP's code says:

UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:@"Custom Action"
                                           action:@selector(customAction:)];

The implication is that customAction is a method this class. This is wrong. customAction: must be a method of the cell class. The reason is that the runtime will look at the cell class and will not show the menu item unless the cell implements the menu item's action method.

For a complete minimal working example (in Swift), see my answer here: https://mcmap.net/q/1631314/-uicollectionview-shouldshowmenuforitemat-not-called

Interventionist answered 17/8, 2018 at 15:15 Comment(0)
D
0

On iOS 9 with Swift to SHOW ONLY CUSTOM ITEMS (without the default cut, paste and so on), I only managed to make work with the following code.

On method viewDidLoad:

let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(contextMenuHandler))
longPressRecognizer.minimumPressDuration = 0.3
longPressRecognizer.delaysTouchesBegan = true
self.collectionView?.addGestureRecognizer(longPressRecognizer)

Override method canBecomeFirstResponder:

override func canBecomeFirstResponder() -> Bool {
    return true
}

Override these two collection related methods:

override func collectionView(collectionView: UICollectionView, shouldShowMenuForItemAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}

override func collectionView(collectionView: UICollectionView, canPerformAction action: Selector,
                             forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) -> Bool {
    return (action == #selector(send) || action == #selector(delete))
}

Create the gesture handler method:

func contextMenuHandler(gesture: UILongPressGestureRecognizer) {

    if gesture.state == UIGestureRecognizerState.Began {

        let indexPath = self.collectionView?.indexPathForItemAtPoint(gesture.locationInView(self.collectionView))

        if indexPath != nil {

            self.selectedIndexPath = indexPath!

            let cell = self.collectionView?.cellForItemAtIndexPath(self.selectedIndexPath)
            let menu = UIMenuController.sharedMenuController()
            let sendMenuItem = UIMenuItem(title: "Send", action: #selector(send))
            let deleteMenuItem = UIMenuItem(title: "Delete", action: #selector(delete))
            menu.setTargetRect(CGRectMake(0, 5, 60, 80), inView: (cell?.contentView)!)
            menu.menuItems = [sendMenuItem, deleteMenuItem]
            menu.setMenuVisible(true, animated: true)
        }
    }
}

And, finally, create the selector's methods:

func send() {
    print("Send performed!")
}

func delete() {
    print("Delete performed!")
}

Hope that helps. :)

Cheers.

Discontented answered 6/7, 2016 at 14:6 Comment(4)
you can do all this stuff without gestureRecognizer, just override default methods.Extemporaneous
@NosovPavel With the default methods you add values to the default menu. If you want a menu with just your entries, you have to use a gesture recognizer. Please, do not downvote so rapidly.Discontented
Your solution works, but this is not a right way to do all this stuff. You can also add custom menu items with default menu without recognizers. for example: dev.glide.me/2013/05/custom-item-in-uimenucontroller-of.htmlExtemporaneous
I tried all other solutions described here as also the link you have pasted here as many others tutorials. None worked for my context: show only custom entries without the default entries. Have you tried do this? If so, please, put a screenshot (with only custom entries without the default copy, paste and so on) of your solution with code. Thanks.Discontented

© 2022 - 2024 — McMap. All rights reserved.