9

With the following setup

....
MyUIMenuItem *someAction  = [[MyUIMenuItem alloc]initWithTitle : @"Something"  action : @selector(menuItemSelected:)];
MyUIMenuItem *someAction2 = [[MyUIMenuItem alloc]initWithTitle : @"Something2" action : @selector(menuItemSelected:)];
....

- (IBAction) menuItemSelected : (id) sender
{
    UIMenuController *mmi = (UIMenuController*) sender;
}

How to figure out which menu item was selected.

And don't say that you need to have two methods... Thanks in advance.

ort11
  • 3,359
  • 4
  • 36
  • 69

4 Answers4

17

Okay, I've solved this one. The solution isn't pretty, and the better option is "Apple fixes the problem", but this at least works.

First of all, prefix your UIMenuItem action selectors with "magic_". And don't make corresponding methods. (If you can do that, then you don't need this solution anyway).

I'm building my UIMenuItems thus:

NSArray *buttons = [NSArray arrayWithObjects:@"some", @"random", @"stuff", nil];
NSMutableArray *menuItems = [NSMutableArray array];
for (NSString *buttonText in buttons) {
    NSString *sel = [NSString stringWithFormat:@"magic_%@", buttonText];
    [menuItems addObject:[[UIMenuItem alloc] 
                         initWithTitle:buttonText
                         action:NSSelectorFromString(sel)]];
}
[UIMenuController sharedMenuController].menuItems = menuItems;

Now your class that catches the button tap messages needs a few additions. (In my case the class is a subclass of UITextField. Yours might be something else.)

First up, the method that we've all been wanting to have but that didn't exist:

- (void)tappedMenuItem:(NSString *)buttonText {
    NSLog(@"They tapped '%@'", buttonText);
}

Then the methods that make it possible:

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    NSString *sel = NSStringFromSelector(action);
    NSRange match = [sel rangeOfString:@"magic_"];
    if (match.location == 0) {
        return YES;
    }
    return NO;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    if ([super methodSignatureForSelector:sel]) {
        return [super methodSignatureForSelector:sel];
    }
    return [super methodSignatureForSelector:@selector(tappedMenuItem:)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    NSString *sel = NSStringFromSelector([invocation selector]);
    NSRange match = [sel rangeOfString:@"magic_"];
    if (match.location == 0) {
        [self tappedMenuItem:[sel substringFromIndex:6]];
    } else {
        [super forwardInvocation:invocation];
    }
}
user102008
  • 30,736
  • 10
  • 83
  • 104
sobri
  • 1,626
  • 15
  • 28
1

One would expect that the action associated with a given menu item would include a sender parameter that should point to the chosen menu item. Then you could simply examine the title of the item, or do as kforkarim suggests and subclass UIMenuItem to include a proeprty that you can use to identify the item. Unfortunately, according to this SO question, the sender parameter is always nil. That question is over a year old, so things may have changed -- take a look at what you get in that parameter.

Alternately, it looks like you'll need to a different action for each menu item. Of course, you could set it up so that all your actions call a common method, and if they all do something very similar that might make sense.

Community
  • 1
  • 1
Caleb
  • 124,013
  • 19
  • 183
  • 272
  • Trying to make a soft menu. Doing the different actions is impractical for this. The parameter is not always nil, it is a menu. I don't see a way to do what you say hence the question, thanks. – ort11 Feb 05 '12 at 14:40
0

Turns out it's possible to obtain the UIButton object (which is actually UICalloutBarButton) that represents UIMenuItem if you subclass UIApplication and reimplement -sendAction:to:from:forEvent:. Although only -flash selector goes through UIApplication, it's enough.

@interface MyApplication : UIApplication
@end

@implementation MyApplication
- (BOOL)sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event
{
    // target == sender condition is just an additional one
    if (action == @selector(flash) && target == sender && [target isKindOfClass:NSClassFromString(@"UICalloutBarButton")]) {
        NSLog(@"pressed menu item title: %@", [(UIButton *)target titleLabel].text);
    }
    return [super sendAction:action to:target from:sender forEvent:event];
}
@end

You can save target (or any data you need from it) in e.g. property and access it later from your UIMenuItem's action.

And to make your UIApplication subclass work, you must pass its name as a third parameter to UIApplicationMain():

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, NSStringFromClass([MyApplication class]), NSStringFromClass([YOUR_APP_DELEGATE class]));
    }
}

This solution works on iOS 5.x-7.0 as of post date (didn't test on older versions).

kambala
  • 2,341
  • 19
  • 20
-1

ort11, you might want to create a property of myuimenuitem and set some sort of Tag. Thay way the object of sender could be recognized by its tag it. In Ibaction then you can set a switch statement that can correspond to each sender.tag and work throught that logic. I guess thats the simplest way to go.

topgun
  • 2,463
  • 8
  • 31
  • 46
  • The sender is the Menu not the item. This is why the question. Why would IOS do this? Just does not make since. – ort11 Feb 05 '12 at 14:39
  • ord11, you are initializing the menu with different "Titles" anyways, so why can't you check your reference of the object with its Title as Caleb suggested. Since sender is just the id type, you can check for particular Class type, and then check its "Title", against it. That way you can reference to MyUIMenuItem class type. something like this. if ([sender isKindofClass:[MyUIMenuItem class]]) and then another if statement as if ([sender.title isEqualToString:@"Something2"]) then UIMenuController *mmi = (UIMenuController*) sender; where your sender is *someAction2 in this case. Hope this helps – topgun Feb 06 '12 at 19:50
  • Sender is Menu, not Menu Item – ort11 Feb 14 '12 at 02:14
  • ort initially ur sender is menuitem and then ur casting as menu. just take nslog of ur sender description before ur casting, to get an overview of sender object – topgun Feb 15 '12 at 04:10
  • 1
    Sender is Menu, Not Menu Item – ort11 Apr 03 '12 at 14:22
  • ort: Initially your sender is menu item if you look closely: - (IBAction) menuItemSelected : (id) sender { UIMenuController *mmi = (UIMenuController*) sender; } The (id)sender you are receiving is menuitem however, you are casting it with your menu controller. I will make more sense if you just do NSLog previously you cast it to notice which menuitems you have selected. Persist or retain there somewhere and then add that to your menu. – topgun May 01 '12 at 15:39
  • `kforkarim`: the object passed in the `sender` parameter is a `UIMenuController`, not a `UIMenuItem`. How `ort11` is subsequently casting it is a red herring. – hatfinch May 30 '16 at 20:06