4

This seems to be one of the most frequently discussed topics here but I couldn't find a solution which actually works. I'm posting this question to share a solution which I found as well as hoping to find a better/cleaner solution

Description of situation:

  • There is a UIWebview in my application

  • There is text input/area in the webview

  • Long pressing on the text area/input brings up a context menu with 'cut', 'copy', 'define' etc.

We need to disable this menu without disabling user input.


What I've tried so far (Stuff that doesn't work) :

Override canPerformAction

This solution tells us to add canPerformAction:withSender: to either subclass of UIWebview or in a delegate of UIWebview.

- (BOOL) canPerformAction:(SEL)action withSender:(id)sender
{
 if (action == @selector(defineSelection:))
 {
    return NO;
 }
 else if (action == @selector(translateSelection:))
 {
    return NO; 
 }
 else if (action == @selector(copy:))
 {
    return NO;
 }

return [super canPerformAction:action withSender:sender];
}

Does not work because the canPerformAction: in this class is does not get called for menu items displayed. Since the sharedMenuController interacts with the first responder in the Responder chain, implementing canPerformAction in the container skipped select and selectAll because they had already been handled by a child menu.

Manipulating CSS

Add the following to CSS:

html {
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color:rgba(0,0,0,0);
}

This does work on images and hyperlinks but not on inputs. :(

Shayan RC
  • 3,152
  • 5
  • 33
  • 40

2 Answers2

7

The root cause of the first solution not working is the subview called UIWebBrowserView. This seems to be the view whose canPerformAction returns true for any action displayed in the context menu.

Since this UIWebBrowserView is a private class we shouldn't try to subclass it (because it will get your app rejected).

So what we do instead is we make another method called mightPerformAction:withSender:, like so-

- (BOOL)mightPerformAction:(SEL)action withSender:(id)sender {


NSLog(@"******Action!! %@******",NSStringFromSelector(action));


  if (action == @selector(copy:))
  {
      NSLog(@"Copy Selector");
      return NO;
  }
  else if (action == @selector(cut:))
  {
      NSLog(@"cut Selector");
      return NO;
  }
  else if (action == NSSelectorFromString(@"_define:"))
  {
      NSLog(@"define Selector");
      return NO;
  }
  else if (action == @selector(paste:))
  {
      NSLog(@"paste Selector");
      return NO;
  }
  else
  {
      return [super canPerformAction:action withSender:sender];
  }


}

and add another method to replace canPerformAction:withSender: with mightPerformAction:withSender:

- (void) replaceUIWebBrowserView: (UIView *)view
{

//Iterate through subviews recursively looking for UIWebBrowserView
for (UIView *sub in view.subviews) {
    [self replaceUIWebBrowserView:sub];
    if ([NSStringFromClass([sub class]) isEqualToString:@"UIWebBrowserView"]) {
        
        Class class = sub.class;
        
        SEL originalSelector = @selector(canPerformAction:withSender:);
        SEL swizzledSelector = @selector(mightPerformAction:withSender:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(self.class, swizzledSelector);
        
        //add the method mightPerformAction:withSender: to UIWebBrowserView
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        //replace canPerformAction:withSender: with mightPerformAction:withSender:
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
            
        } else {
            
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }
}
}

And finally call it in the viewDidLoad of the ViewController:

[self replaceUIWebBrowserView:self.webView];

Note: Add #import <objc/runtime.h> to your viewController then error(Method) will not shown.

Note: I am using NSSelectorFromString method to avoid detection of private API selectors during the review process.

Shayan RC
  • 3,152
  • 5
  • 33
  • 40
  • it seem to work fine, thank you! did you submit your app to the store without problems? – laucel Sep 26 '14 at 09:43
  • 1
    I submitted it today, it's still in review – Shayan RC Sep 26 '14 at 10:50
  • thanks @Shayan RC, can you please update here after the review? – laucel Sep 26 '14 at 12:29
  • 1
    If it passes review, I'll definitely update the answer. Also, instead of using the private api selector's directly, I'm using NSSelectorFromString. Hope this is enugh to bypass detection. Everything else looks fine. – Shayan RC Sep 26 '14 at 14:22
  • 1
    Just found out method_exchangeImplementations, might be a problem as well! :( – Shayan RC Sep 26 '14 at 14:32
  • did they reject you app? :( – laucel Sep 29 '14 at 06:43
  • 1
    The app has been approved by Apple. Hurray!! =) Sorry for the delay @laucel – Shayan RC Nov 24 '14 at 17:47
  • Great solution @Shayan RC You saved my life =) But still there is one strange thing - this call `[super canPerformAction:action withSender:sender];` always returns `YES`. Is there a way to extract default presence value for menu items? Thanks. – BorisR Oct 01 '15 at 16:14
  • 1
    @rubanbs I think `canPerformAction:withSender:` (or the method we replace it with) is called for every possible menu item when a long press is detected. It returns `YES` for the items which are displayed. So, you can check which `action` returns `YES`. If all the menu items return 'YES', that means all the menu items are supposed to be shown for this UIElement PS: I haven't worked on this in a while, so I might be wrong. Please double check! – Shayan RC Oct 13 '15 at 07:40
  • @ShayanRC great solution you saved me a lot of time. And i am did small change added this line #import to remove the error by Method. – vishnu Nov 01 '16 at 20:04
2

Also you can hide menu:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(menuWillBeShown:) name:UIMenuControllerWillShowMenuNotification object:nil];

...

- (void)menuWillBeShown:(NSNotification *)notification {
    dispatch_async(dispatch_get_main_queue(),^{
        [[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
    });
}

The essential trick here is dispatch_async.

Kirill Belonogov
  • 414
  • 4
  • 14