Shayan RC Shayan RC - 1 month ago 12
Objective-C Question

Disable long press menu in text area/input UIWebview

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.
:(

Answer

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.

This seems to be working fine in iOS7 with Xcode 5, if anyone can find any issues in this please let me know how I can improve it..