Search code examples
iosswiftobjective-cuikituicontrol

UIControl subclass - show UIMenu when pressing


Summary: All I am trying to do is to have a UIControl subclass that shows a UIMenu when pressed and I have read this other question but I am running into a crash when setting the contextMenuinteractionEnabled property to YES.

Similar question: iOS 14 Context Menu from UIView (Not from UIButton or UIBarButtonItem)


I have a UIControl subclass and I am wanting to add a menu to it and for the UIMenu to be shown when single tapping the control. But I keep getting an error when setting the contextMenuInteractionEnabled property to YES.

Here is the control subclass:

@interface MyControl : UIControl
@end

@implementation MyControl
@end

It is just a plain UIControl subclass. Then, I create an instance of that class and set the value of contextMenuInteractionEnabled to YES, as shown here:

MyControl *myControl = [[MyControl alloc] init];
myControl.contextMenuInteractionEnabled = YES; //<-- Thread 1: "Invalid parameter not satisfying: interaction"

But then when running this, I get the following error message:

Error Message

*** Assertion failure in -[MyControl addInteraction:], UIView.m:17965

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: interaction'

Error: NSInternalInconsistencyException

Reason: Invalid parameter not satisfying: interaction

Stack trace: (

0 CoreFoundation 0x00000001b8181e94 5CDC5D9A-E506-3740-B64E-BB30867B4F1B + 40596

1 libobjc.A.dylib 0x00000001b14b78d8 objc_exception_throw + 60

2 Foundation 0x00000001b2aa5b4c C431ACB6-FE04-3D28-B677-4DE6E1C7D81F + 5528396

3 UIKitCore 0x00000001ba47e5bc 179501B6-0FC2-344A-B969-B4E3961EBE10 + 1349052

4 UIKitCore 0x00000001ba47ea70 179501B6-0FC2-344A-B969-B4E3961EBE10 + 1350256

)

libc++abi: terminating with uncaught exception of type NSException

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: interaction' terminating with uncaught exception of type NSException


Questions

  1. What is meant by the error message "Invalid parameter not satisfying: interaction"? Where is "interaction" coming from and how could this be fixed?

  2. What am I doing wrong? All I want is a custom UIControl that can display a menu when pressed. That's it.

As a side note, this code works fine for a UIButton instance, so the UIButton class is doing something internal that I am not doing, but I don't know what.

Update 1

Following the answer from @DonMag, I tried this:

    MyControl *control = [[MyControl alloc] init];
    control.showsMenuAsPrimaryAction = YES;

    UIContextMenuInteraction *contextMenuInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
    [control addInteraction:contextMenuInteraction];

    [self.view addSubview:control];

And this no longer crashes and the menu even shows up if I long press it. But I was hoping to get it to show up like a regular menu, as the primary action for pressing the control. What I am trying to do is to emulate the menu property on UIButton. That would be the most ideal thing is if I could implement a control that has a menu property and then having that menu be available as the primary action.


Solution

  • There's no need to call .contextMenuInteractionEnabled = YES; ...

    Here's a quick, complete example:

    #import <UIKit/UIKit.h>
    
    @interface MyControl : UIControl
    @end
    
    @implementation MyControl
    @end
    
    @interface ControlMenuViewController : UIViewController <UIContextMenuInteractionDelegate>
    @end
    
    @interface ControlMenuViewController ()
    @end
    
    @implementation ControlMenuViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        MyControl *myControl = [MyControl new];
    
        UIContextMenuInteraction *interaction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
        [myControl addInteraction:interaction];
        
        myControl.frame = CGRectMake(100, 200, 200, 50);
        myControl.backgroundColor = UIColor.systemRedColor;
        [self.view addSubview:myControl];
        
    }
    
    - (nullable UIContextMenuConfiguration *)contextMenuInteraction:(nonnull UIContextMenuInteraction *)interaction configurationForMenuAtLocation:(CGPoint)location {
        
        UIContextMenuConfiguration* config = [UIContextMenuConfiguration configurationWithIdentifier:nil
                                                                                     previewProvider:nil
                                                                                      actionProvider:^UIMenu* _Nullable(NSArray<UIMenuElement*>* _Nonnull suggestedActions) {
            
            NSMutableArray* actions = [[NSMutableArray alloc] init];
            
            [actions addObject:[UIAction actionWithTitle:@"Stand!" image:[UIImage systemImageNamed:@"figure.stand"] identifier:nil handler:^(__kindof UIAction* _Nonnull action) {
                NSLog(@"Stand!");
                //[self performMenuCommandStand];
            }]];
            [actions addObject:[UIAction actionWithTitle:@"Walk!" image:[UIImage systemImageNamed:@"figure.walk"] identifier:nil handler:^(__kindof UIAction* _Nonnull action) {
                NSLog(@"Walk!");
                //[self performMenuCommandWalk];
            }]];
            [actions addObject:[UIAction actionWithTitle:@"Run!" image:[UIImage systemImageNamed:@"figure.run"] identifier:nil handler:^(__kindof UIAction* _Nonnull action) {
                NSLog(@"Run!");
                //[self performMenuCommandRun];
            }]];
            
            UIMenu* menu = [UIMenu menuWithTitle:@"" children:actions];
            return menu;
            
        }];
        
        return config;
    
    }
    
    @end
    

    Edit

    Slight modification to allow single-tap (Primary Action):

    #import <UIKit/UIKit.h>
    
    @interface MyControl : UIControl
    @property (strong, nonatomic) UIContextMenuConfiguration *menuCfg;
    @end
    
    @implementation MyControl
    - (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction configurationForMenuAtLocation:(CGPoint)location {
        return self.menuCfg;
    }
    @end
    
    @interface ControlMenuViewController : UIViewController
    @end
    
    @interface ControlMenuViewController ()
    @end
    
    @implementation ControlMenuViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        MyControl *myControl = [MyControl new];
        
        myControl.frame = CGRectMake(100, 200, 200, 50);
        myControl.backgroundColor = UIColor.systemRedColor;
        [self.view addSubview:myControl];
    
        // menu configuration
        UIContextMenuConfiguration* config = [UIContextMenuConfiguration configurationWithIdentifier:nil
                                                                                     previewProvider:nil
                                                                                      actionProvider:^UIMenu* _Nullable(NSArray<UIMenuElement*>* _Nonnull suggestedActions) {
            
            NSMutableArray* actions = [[NSMutableArray alloc] init];
            
            [actions addObject:[UIAction actionWithTitle:@"Stand!" image:[UIImage systemImageNamed:@"figure.stand"] identifier:nil handler:^(__kindof UIAction* _Nonnull action) {
                NSLog(@"Stand!");
                //[self performMenuCommandStand];
            }]];
            [actions addObject:[UIAction actionWithTitle:@"Walk!" image:[UIImage systemImageNamed:@"figure.walk"] identifier:nil handler:^(__kindof UIAction* _Nonnull action) {
                NSLog(@"Walk!");
                //[self performMenuCommandWalk];
            }]];
            [actions addObject:[UIAction actionWithTitle:@"Run!" image:[UIImage systemImageNamed:@"figure.run"] identifier:nil handler:^(__kindof UIAction* _Nonnull action) {
                NSLog(@"Run!");
                //[self performMenuCommandRun];
            }]];
            
            UIMenu* menu = [UIMenu menuWithTitle:@"" children:actions];
            return menu;
            
        }];
    
        // set custom control menu configuration
        myControl.menuCfg = config;
        
        // show menu on single-tap (instead of long-press)
        [myControl setContextMenuInteractionEnabled:YES];
        myControl.showsMenuAsPrimaryAction = YES;
        
    }
    
    @end