Skip to content


Customize the contextual menu of UIWebView

When you tap and hold your finger on a link in a UIWebView object for about a second, a contextual menu will open and provides a few choices: copy the URL to the pasteboard, open the link and a button to close the menu again. Unfortunately there’s no API available to add additional menu items to this contextual menu. But if you look at iCab Mobile, you’ll notice that there are a lot of additional menu items here. How is this done?

First of all, you really can’t add additional menu items to the default ones of the standard contextual menu. But you can switch off the contextual menu using a certain CSS property. So the solution would be to switch off the default menu and implement your own from scratch. And to implement your own contextual menu, you have to first catch the tab-and-hold gesture, get the coordinates of the finger on the screen, translate these coordinates into the coordinate system of the web page and finally look for the HTML elements at this location. Based on the HTML elements, you can then decide which menu items to include in the contextual menu, and then create and display the contextual menu.

The first step is to switch off the default contextual menu of UIWebView. This is easy because we only need to set the CSS property “-webkit-touch-callout” to “none” for the body element of the web page. We can do this using JavaScript in the UIWebView delegate method “webViewDidFinishLoad:”…

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
   [webView stringByEvaluatingJavaScriptFromString:@"document.body.style.webkitTouchCallout='none';"];
}

Now, the default Contextual menu won’t open anymore.

The next step is to catch the “tap-and-hold” gesture. If your App only needs to run on iOS 3.2, iOS 4.0 or newer, you can simply use the new UIGestureRecognizer API. But if you still want to address the 1st generation iPod Touch and iPhone devices (where you can install at most iOS 3.1) where these UIGestureRecognizer classes are not available, you need to do a little bit more. I’ll show here a solution which will work with iOS 3.0 as well, so it’s compatible to all iPhone, iPad and iPod Touch devices.

A big problem when you need to catch gestures within UIWebView is that UIWebView internally consists of many nested views, and only the inner ones do respond to events and gestures. Which means, if you subclass UIWebView and overwrite the methods “touchesBegan:withEvent:”, “touchesMoved:withEvent:” etc., you’ll notice that these methods won’t be called. The events are delivered directly to these private inner views which are actually doing all the work. And if you overwrite “hitTest:withEvent:”, which is used by the iOS to find the view to which the events will be delivered, you would be able to get the touch events, but then you would have to deliver the events to these inner views yourself so these can still do their work. And because these inner views are private and the relationships between these views are unknown, it can be extremely dangerous to mess with the events here.

A much easier way would be to subclass UIWindow and overwrite the method “sendEvent:” of the UIWindows class. Here you’ll get all events before they are delivered to the views. And we can deliver the tap-and-hold events using the notification manager to the rest of the app. Each object that is interested in this gesture can listen to this notification.

Recognizing the tap-and-hold gesture is not very complicated. What we need to do is to save the screen coordinates of the finger when the finger first touches the screen. At that time we will also start a timer which fires after about a second. As soon as another finger touches the screen, the finger moves or the touch event is canceled, we invalidate the timer because then it can not be a simple tap-and-hold gesture anymore. If the timer fires, we can be sure that a single finger has touched the screen for about a second without moving and then we’ve recognized the “tap-and-hold” gesture. We post the gesture as notification.

This is the implementation of the UIWindow subclass:

MyWindow.h:

@interface MyWindow : UIWindow
{
   CGPoint    tapLocation;
   NSTimer    *contextualMenuTimer;
}
@end

MyWindow.m:

#import "MyWindow.h"

@implementation MyWindow

- (void)tapAndHoldAction:(NSTimer*)timer
{
   contextualMenuTimer = nil;
   NSDictionary *coord = [NSDictionary dictionaryWithObjectsAndKeys:
             [NSNumber numberWithFloat:tapLocation.x],@"x",
             [NSNumber numberWithFloat:tapLocation.y],@"y",nil];
   [[NSNotificationCenter defaultCenter] postNotificationName:@"TapAndHoldNotification" object:coord];
}

- (void)sendEvent:(UIEvent *)event
{
   NSSet *touches = [event touchesForWindow:self];
   [touches retain];

   [super sendEvent:event];    // Call super to make sure the event is processed as usual

   if ([touches count] == 1) { // We're only interested in one-finger events
      UITouch *touch = [touches anyObject];

      switch ([touch phase]) {
         case UITouchPhaseBegan:  // A finger touched the screen
            tapLocation = [touch locationInView:self];
            [contextualMenuTimer invalidate];
            contextualMenuTimer = [NSTimer scheduledTimerWithTimeInterval:0.8
                        target:self selector:@selector(tapAndHoldAction:)
                        userInfo:nil repeats:NO];
            break;

         case UITouchPhaseEnded:
         case UITouchPhaseMoved:
         case UITouchPhaseCancelled:
            [contextualMenuTimer invalidate];
            contextualMenuTimer = nil;
            break;
      }
   } else {                    // Multiple fingers are touching the screen
      [contextualMenuTimer invalidate];
      contextualMenuTimer = nil;
   }
   [touches release];
}
@end

Some remarks for the UITouchPhaseMoved phase: it can be sometimes useful to allow small movements on the screen. You can add some code to check for the distance the finger has moved and if it is within a certain range, you just don’t abort the timer. This helps users which have difficulties to hold the finger still for about a second.

Another important thing you have to do when the App window is created by a NIB file: you have to change the UIWindow class of the window within the NIB file in Interface Builder to the new subclass MyWindow. This way the window is created with our subclass, which is important.

The next step is to listen for the “TapAndHoldNotification” notification within the UIWebView delegate and when this notification is received, we need to check which HTML element was touched.

When initializing the UIWebView delegate, we need to add the delegate as observer for the notification…

   [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextualMenuAction:) name:@"TapAndHoldNotification" object:nil];
}

And here’s the method “contextualMenuAction:”…

- (void)contextualMenuAction:(NSNotification*)notification
{
   CGPoint pt;
   NSDictionary *coord = [notification object];
   pt.x = [[coord objectForKey:@"x"] floatValue];
   pt.y = [[coord objectForKey:@"y"] floatValue];

   // convert point from window to view coordinate system
   pt = [webView convertPoint:pt fromView:nil];

   // convert point from view to HTML coordinate system
   CGPoint offset  = [webView scrollOffset];
   CGSize viewSize = [webView frame].size;
   CGSize windowSize = [webView windowSize];

   CGFloat f = windowSize.width / viewSize.width;
   pt.x = pt.x * f + offset.x;
   pt.y = pt.y * f + offset.y;

   [self openContextualMenuAt:pt];
}

The method “scrollOffset” and “windowSize” are implemented as category for the UIWebView class. “scrollOffset” is required to make sure the coordinates are also correct when the web page was scrolled. The “windowSize” returns the visible width and height of the HTML document from the point of view of the HTML document. So based on the windowsSize of the “HTML window” and the view size of the UIWebView, you can calculate the zoom factor, and the zoom factor is necessary to transform and scale the screen coordinates to the correct HTML coordinates.

Here’s the implementation of “scrollOffset” and “windowSize”…

WebViewAdditions.h:

@interface UIWebView(WebViewAdditions)
- (CGSize)windowSize;
- (CGPoint)scrollOffset;
@end

WebViewAdditions.m:

#import "WebViewAdditions.h"

@implementation UIWebView(WebViewAdditions)

- (CGSize)windowSize
{
   CGSize size;
   size.width = [[self stringByEvaluatingJavaScriptFromString:@"window.innerWidth"] integerValue];
   size.height = [[self stringByEvaluatingJavaScriptFromString:@"window.innerHeight"] integerValue];
   return size;
}

- (CGPoint)scrollOffset
{
   CGPoint pt;
   pt.x = [[self stringByEvaluatingJavaScriptFromString:@"window.pageXOffset"] integerValue];
   pt.y = [[self stringByEvaluatingJavaScriptFromString:@"window.pageYOffset"] integerValue];
   return pt;
}
@end

Finally, we need to implement the method “openContextualMenuAt:” for the UIWebView delegate, which first checks for the HTML elements that are at the touch locations and the creates the contextual menu. Checking for the HTML elements at the touch location must be done via JavaScript…

JSTools.js

function MyAppGetHTMLElementsAtPoint(x,y) {
   var tags = ",";
   var e = document.elementFromPoint(x,y);
   while (e) {
      if (e.tagName) {
         tags += e.tagName + ',';
      }
      e = e.parentNode;
   }
   return tags;
}

This JavaScript function simply collects the tag names of all HTML elements at the touch coordinates and returns the tag names as string list. The JavaScript file must be added as “resource” to your XCode project. It can happen that Xcode treats JavaScript file as normal code and tries to compile and link it instead of adding it to the resources. So make sure the JavaScript file is in the “Copy Bundle Resources” section within the XCode project target and not in the “Compile Sources” or “Link Binaries” section.

- (void)openContextualMenuAt:(CGPoint)pt
{
   // Load the JavaScript code from the Resources and inject it into the web page
   NSString *path = [[NSBundle mainBundle] pathForResource:@"JSTools" ofType:@"js"];
   NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
   [webView stringByEvaluatingJavaScriptFromString: jsCode];

   // get the Tags at the touch location
   NSString *tags = [webView stringByEvaluatingJavaScriptFromString:
            [NSString stringWithFormat:@"MyAppGetHTMLElementsAtPoint(%i,%i);",(NSInteger)pt.x,(NSInteger)pt.y]];

   // create the UIActionSheet and populate it with buttons related to the tags
   UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:@"Contextual Menu"
                  delegate:self cancelButtonTitle:@"Cancel"
                  destructiveButtonTitle:nil otherButtonTitles:nil];

   // If a link was touched, add link-related buttons
   if ([tags rangeOfString:@",A,"].location != NSNotFound) {
      [sheet addButtonWithTitle:@"Open Link"];
      [sheet addButtonWithTitle:@"Open Link in Tab"];
      [sheet addButtonWithTitle:@"Download Link"];
   }
   // If an image was touched, add image-related buttons
   if ([tags rangeOfString:@",IMG,"].location != NSNotFound) {
      [sheet addButtonWithTitle:@"Save Picture"];
   }
   // Add buttons which should be always available
   [sheet addButtonWithTitle:@"Save Page as Bookmark"];
   [sheet addButtonWithTitle:@"Open Page in Safari"];

   [sheet showInView:webView];
   [sheet release];
}

This method injects the JavaScript code which looks for the HTML elements at the touch location into the web page and calls this function. The return value will be a string with a comma-separated list of tag names. This string will start and end with a comma so we can simply check for occurrences of a substring “,tagName,” if we want to find out if an element with a certain tag name was touched. In our example, we simple add some buttons if an “A” tag was hit and some other buttons if and “IMG” tag was hit. But what you’re doing is up to you. Also the information that is returned from the JavaScript function (in this example “MyAppGetHTMLElementsAtPoint()”) is up to you. In iCab Mobile it returns the HREF and SRC attributes of A and IMG tags, so the URLS can be directly processed.

The example doesn’t include the UIActionSheet delegate method which is called when you tab on one of the buttons in the contextual menu. But I think you should already know how to handle this. Also a few other details might be missing, but I think you should be able now to implement your own custom contextual menu for UIWebView objects with the information from the blog post.

Posted in iPhone & iPod Touch, Programming.

Tagged with , , , , .


159 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. Mohsin says

    @Alexander..
    Can you please post working code link…?

  2. Victor says

    Thanks. Great blog.

    There is one serious issue to fix. Consider that you have another dialog view (like UITableviewController view) on top of UIWebView, touch on the dialog view will be sent to tapAndHold notification listener. The handler for the UIWebView will crash.

    The notification should include the origin view of the touch. Then the notification handler will be able to filter out those touches not from the UIWebView or its descendant.

  3. lrx says

    This doesnt work for me :(

    - (void)webViewDidFinishLoad:(UIWebView *)webView
    {
       [webView stringByEvaluatingJavaScriptFromString:@"document.body.style.webkitTouchCallout='none';"];
    }
    
  4. Alexander Alexander says

    @Victor
    In general you have to take care to add or remove the notification observer according to the context of the App. In case the WebView is covered or hidden by another view controller, then you should simply remove the observer that listens to the contextual menu notification. And when the WebView becomes active again, you can add the observer again. This way the web view will only get the contextual menu touches when it is prepared to respond to them.

    The problem with the origin “view” is that the WebView consists of a complex internal and private view hierarchy, so if you try to detect the touched view, you’ll likely get an internal view of the WebView, which might not help you to identify the webView or other views.

  5. Alexander Alexander says

    @Irx
    Are you sure that the delegate method is actually called? Which means: have you set the delegate for the WebView correctly?

  6. Yoffa says

    This is a great tutorial, but I’m afraid it isn’t working for me. And I’d bet I’m putting methods in the wrong places:

    I’m using a test project, and I’ve created a UIWebView (via code) called myWebView which is initialised in viewDidLoad of my ViewController.m file. After I set myWebView’s delegate to self, I register the NSNotification as said in the tutorial. I then load Apple.com.

    However, none of the methods that I’ve added from your tutorial (to my ViewController.m) actually run, with exception to webViewDidFinishLoad. I’ve also placed break points in MyWindow.h/m and nothing in there triggers either despite my having changed the window variable in AppDelegate.h to MyWindow (instead of UIWindow).

    I’ve got no compiler warnings or errors, and the app runs. I tap and hold, but I get nothing.

    I’m hoping you can shed some light on my problem here. Thanks.

  7. Alexander Alexander says

    @Yoffa
    Please make sure that your App really creates an instance of your UIWindow subclass instead of just an instance of UIWindow. If your app doesn’t create an instance of the subclass which overwrites the “sendEvent:” method, this method can never be called. Normally you need to change the class of the window in the main XIB file of the App to the class of your UIWindow subclass.

  8. Sylver says

    Hi,
    for people wanting to get a full working project implementing this (also with image detection), have a look at my open source project here :
    https://github.com/sylverb/CIALBrowser

    BTW, It’s seems that there is something not working well with iOS 5.0 beta ! I’ll try to find what’s wrong …

  9. Sylver says

    If you want to make things working with iOS5, change the following in – (void)contextualMenuAction:(NSNotification*)notification :

            CGFloat f = windowSize.width / viewSize.width;
            if ([[UIDevice currentDevice].systemVersion doubleValue] >= 5.) {
                point.x = point.x * f;
                point.y = point.y * f;
            } else {
                // On iOS 4 and previous, document.elementFromPoint is not taking
                // offset into account, we have to handle it
                CGPoint offset  = [webView scrollOffset];
                point.x = point.x * f + offset.x;
                point.y = point.y * f + offset.y;
            }
    
  10. Alexander Alexander says

    @Sylver
    You’re right, the problem is that under iOS 4 the coordinates for the function elementFromPoint() must be relative to the origin of the document while under iOS 5 the coordinates must be relative to the viewport (the visible area of the document). iOS 5 is doing it according to the JavaScript specification; elements outside of the viewport can not be found anymore and elementFromPoint() would return null.

    In iCab I’ve used a different approach to solve the problem. Instead of checking the version of the iOS, iCab is doing everything in JavaScript. The fact that elementFromPoint() returns null for coordinates outside of the viewport can be used to distinguish between the old behavior and the new one. When the document is not scrolled, both coordinates origins are the same and there would be not problem in the first place. When looking for the element at the coordinates
    (0, window.pageYOffset + window.innerHeight -1) or (window.pageXOffset + window.innerWidth -1, 0), the coordinates would be always outside of the viewport when the document is scrolled. So if elementFromPoint() returns null for these coordinates, the new iOS 5 behavior is present, otherwise the iOS 4 behavior (at least the BODY or HTML element will be found). And then we know if we have to correct the coordinates, like you’ve done this, though everything can be done in the JavaScript code and does not rely on any iOS version number. This can be important when the code or at least elementFromPoint() is used under MacOS as well. This makes it easier to share code between MacOS and iOS Apps.

  11. Yoffa says

    @Alexander

    Thanks for your reply, I think that’s the problem. The only change I have made for that is this: @property (strong, nonatomic) MyWindow *window; (compared to just UIWindow)
    And that’s in my AppDelegate.h file. I’ve found no where else that I can change anything to do with the window. I don’t actually have any .XIB files, I’ve got a storyboard and I’ve looked all through it and found nothing. Thanks.

  12. max says

    i have added a uinavigationbar into my web view scrollview (ios5 has the scroll view, don’t know about 4.x) and the code posted by @Sylver works, except if i scroll up to see the nab bar, then anything i touch is null for the url. if i scroll down so i don’t see it, it works perfect. i just add the nab to the web view, and have the webscrollview content inset for uiedgeinsetsmake(54,0,0,0). how do i fix?

  13. Alexander Alexander says

    @max
    Maybe have a look at
    http://www.icab.de/blog/2011/10/17/elementfrompoint-under-ios-5/
    where I posted a more general solution for this issue.

  14. Brad says

    My app is not properly getting the HTML tags. it wont show choices for links or pictures

  15. Alexander Alexander says

    @Brad
    Please check that you’re correctly adding the JS file as resource file to your Xcode project. Otherwise it will be missing in the final App.

  16. Brad says

    It is bundled with the resources. I have two webviews and im having the contextualMenuAction check to see which webview the point is in. could this be causing an issue?

  17. Alexander Alexander says

    @Brad
    Just make sure that you get the origin of the coordinates right. The method “convertPoint:fromView:” would do this already. But you have to make sure that you call this for the correct objects. If you call it for the wrong UIWebView object, then the coordinates are all wrong.

  18. Zachary Glazer says

    Hello,
    This is a great tutorial. I have a problem though. When i run my app and hold down my finger over links everything works perfectly. But when i do it for images it does not work. I have only tested it on a few sites, like the regular Facebook site (not the stupid phone version), yahoo, and twitpic.com and they do not work. When i use the regular safari it works fine for all of these places. Is there any reason it would work for links but not images? It could be me doing something stupid so keep that in mind. Also I only have it implemented for the latest software version.

  19. Zachary says

    Wait… I know whats wrong. webViewDidFinishLoad: is not being called… You mentioned to someone else that this might be because I did not set the delegate for the WebView correctly. I’m not sure I did… could you please show me the correct way to set it?

  20. Zachary says

    Sorry. Nevermind again. I got around using it by just disableing the default contextual menu when i recognized a tap followed by a long press. But now i figured out that my coordinate calculations are wrong. Because i am using only iOS5 i did not use scrollOffset as someone said above. Is this wrong? If not is there another reason my coordinates would be off?
    P.S. sorry for making so many posts

  21. Zachary Glazer says

    Ok! I figured it out haha please feel free to delete any of my posts that you don’t want. :) this was my final solution to the problem. It is much simpler seeing as how i don’t account for any phone that can’t use UILongPressGestureRecognizer. Although now its opening two action sheets for some reason…

    - (IBAction)pressFound:(id)sender {
        [self.facebookWebView stringByEvaluatingJavaScriptFromString:@"document.body.style.webkitTouchCallout='none';"];
        CGPoint tapLocation;
        tapLocation = [sender locationInView:self.facebookWebView];
        CGSize viewSize = [self.facebookWebView frame].size;
        CGSize windowSize = [self windowSize];
        CGFloat f = windowSize.width / viewSize.width;
        if ([[UIDevice currentDevice].systemVersion doubleValue] >= 5.0) {
            tapLocation.x = tapLocation.x * f;
            tapLocation.y = tapLocation.y * f;
        } else {
            CGPoint offset = [self scrollOffset];
            tapLocation.x = tapLocation.x * f + offset.x;
            tapLocation.y = tapLocation.y * f + offset.x;
        }
        [self openContextualMenuAt:tapLocation];   
    }
    
  22. John says

    Hello,
    This is a great thing to know and I thank you very much. I get everything except one thing. This may sound stupid but how do you actually set your MyWindow subclass as the window? Do you do it in the AppDelegat.h? If so then how and if not then where and how?

  23. John says

    Also note that I did try to #import “MyWindow.h” into AppDelegate.h and then changed the @synthesize window = _window to @synthesize window = MyWindow in AppDelegate.m. It compiled fine and the program ran fine but the Action sheet did not come up. I figured out that contextualMenuAction: never got called so I’m assuming it has to do with setting MyWindow as the window but I could be wrong.

  24. Alexander Alexander says

    @John
    If the window is loaded from a XIB file, you need to change the UIWindow class for the window object in Interface Builder to the “MyWindow” class. This can be done in the “Identity inspector” panel within the Interface Builder in Xcode.

    In case the window is not loaded from a XIB file but instead created programmatically in your code, then simply create an object of the class MyWindows instead of one of the class “UIWindow”.

  25. John says

    The window is not loaded with a XIB file that I know of. In AppDelegate I changed “@property (strong, nonatomic) UIWindow *window;” to “@property (strong, nonatomic) MyWindow *window;” and again the app loaded fine and ran fine except that the action sheet did not come up. Very confused

  26. Alexander Alexander says

    @John
    Please make sure that when creating the window object (alloc/init) you create an object of MyWindow instead of one of UIWindow. Assigning an object to a property does not create one. It just stores the reference to an object. Which means the @property does not need to be changed at all.

  27. John says

    That is my problem. No where in my project, that i can find, allocates or initialized a UIWindow. And when i try to do it myself using MyWindow instead of UIWindow I get a bunch of errors saying i have multiple interfaces and properties in my NavigationController.h file (i am using a navigation controller by the way).

  28. Alexander Alexander says

    @John
    I can assure you that somewhere in your project, the window is created. There’s either a XIB file where the window is defined (in this case you have to change the class for the window object to MyWindows here), or the window is created programmatically in the code (and then you have to change the class here). All Apps do hat a window where they live in, so this window must be created somewhere. The new Xcode templates for Apps create the window within the “application:didFinishLaunchingWithOptions:” method of the Application delegate. In older Xcode releases the templates did usually use an XIB file.

  29. John says

    I know that the window is made SOMEWHERE. I just can’t find where. My AppDelegate.h file looks like this:
    #import
    @interface AppDelegate : UIResponder
    @property (strong, nonatomic) UIWindow *window;
    @end
    and AppDelegate.h looks like this (up until didFinishLaunchingWithOptions):
    #import “AppDelegate.h”
    @implementation AppDelegate
    @synthesize window = _window;
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    // Override point for customization after application launch.
    return YES;
    }
    as you can see the window is never allocated or initialized even thought the property is made. So this means that it must be made in a .XIB file somewhere but I cannot find that file. The only way to be able to open the identity inspector in the interface builder is through the MainStoryboard.storyboard file and I cannot figure out any way to find where the window is given the class UIWindow so that I can change it to MyWindow. Again I know the window has to be created somewhere. I just cannot find where.

  30. Alexander Alexander says

    @John
    OK, you’re using Storyboard. In this case I can’t help, I’m sorry. I’ve never done anything with Storyboard so far. And it is clear that if the window is not created programmatically nor created from within a XIB file, it must be created somewhere in the Storyboard.

  31. Daniel says

    @Alexander
    I’m at a complete loss as to how I could use IB with your CIALBrowser. Can Xcode build it from your code or do I need to create a new one and point your implementations at it. I tried the MyWindow method but running just calls up the default CIALBrowser. I have no idea where my window would be the one to call up. I’m very new to this and have created a great looking browser but the action sheet and bookmarks are what I was hoping to get from your project. Any help would be great. Thank you.

  32. Alexander Alexander says

    @Daniel
    If you define your main window in a XIB file, then you have to change the class of the window object in IB from UIWindow to MyWindow. This can be done in one of the Inspector panels of IB. If you create the window programmatically in your code, you need to create an object of MyWindow instead of one from UIWindow. In case you’re using the new Storyboard, I can’t help you at the moment, I haven’t used Storyboard yet.

  33. Eric says

    Hi Alexander
    Thank you,I have successed by your tutorial .But I found that if i commented off the UIActionSheet in the openContextualMenuAt function ,when i hold my finger on a link for about a second, the webView also load the link which by my clicked. I don’t want use the ActionSheet menu ,I want use a new view. I don’t kown what has happened. Any help would be great.Thank you.

  34. julian says

    Thanx for your tutorial. I Have used for code for opening the contextual menu on the
    m.beemp3.com. but the problem is that the contextual menu does not appear. Please help me.

  35. Alexander Alexander says

    @Eric
    You need to set the CSS property “-webkit-TouchCallout” to “none” to prevent that the original contextual menu will appear. See the very first code snippet in the blog post. Make sure you set the delegate for the UIWebView correctly, so this code snippet is actually called.

  36. Alexander Alexander says

    @julian
    Then you’ve probably missed something. Please use the debugger to find out which part of the code is actually called and which is not, to narrow down where you’ve done something wrong. Check the content of variables etc. to find out if they have the expected values.
    Also read other blog posts, like for example the following one, which explains a common problem: http://www.icab.de/blog/2011/08/02/adding-javascript-files-as-resources-to-an-xcode-project/

  37. Eric says

    Alexander says
    @Eric
    You need to set the CSS property “-webkit-TouchCallout” to “none” to prevent that the original contextual menu will appear. See the very first code snippet in the blog post. Make sure you set the delegate for the UIWebView correctly, so this code snippet is actually called.
    ——————————————————————————————————————
    I have set the CSS property “-webkit-TouchCallout” to “none” in webViewDidFinishLoad and the delegate of the UIWebView has called. But it also has happend in ios SDK4.0. BTW ,I want to know if the webView have not finish loading and webViewDidFinishLoad delegate method is not called,the contextual menu of ios’webView will show?

  38. Alexander Alexander says

    @Eric
    The default contextual menu of the iOS will show if “webkitTouchCallout” is not yet set to none for the element.

    If this is not working for you, you’re either have done something wrong (typo?), or the document is not a HTML document and doesn’t have a “body” element. In my code snippets I simply assume that the document is a HTML document with a body element.

    For web pages with frames or for XML documents, you need to modify the code so it will
    also look into frames etc.

  39. Lawrence says

    @Alexander
    Thanks for the post!
    After some tweaking I got most of the stuff to work. The problem is that I’m not very familiar with JS and don’t know how to extract the URL from an element. Can you give me a few pointers on this? Thanks in advance!

  40. Alexander Alexander says

    @Lawrence
    Only certain elements (like links or images) do have a URL. And accessing this URL is easy, you just need to access the element property whose name is equal to the HTML attribute where this URL is assigned to. So for an image tag the URL of the image is defined in the “src” attribute, so you can access this URL via JS using the following code:
    url = imgElement.src

    And for links (“a” tag) the HTML attribute is “href”, so you can access the Url this way:

    url = aElement.href

    This will also resolve relative URLs, so you always get the full URL here, even if the HTML code uses only relative URLs.

    Alternatively you can access the values of the attributes this way:

    url = element.getAttribute(“href”)

    but this will only return the value as it is giving in the HTML code, so a relative URL is not automatically completed to a full URL.

  41. ML says

    Thanks for this great tutorial. I managed to override UIWebView and capture the long press gesture without resorting to overriding UIWindow. I personally think this is a more elegant solution. Here’s my code for your reference. I could only test this on iOS 5 & 6. Would appreciate if someone can test the code on iOS 4. Thanks!

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
        UIView *targetView = [super hitTest:point withEvent:event];
    
        // remove any previously registered targets
        for (UIGestureRecognizer *r in targetView.gestureRecognizers) {
            [r removeTarget:self action:nil];
        }
    
        // register with the first UILongPressGestureRecognizer
        for (UIGestureRecognizer *r in targetView.gestureRecognizers) {
            if ([r isKindOfClass:[UILongPressGestureRecognizer class]]) {
                ((UILongPressGestureRecognizer*)r).minimumPressDuration = 0.5; // optional
                [r addTarget:self action:@selector(_handleGesture:)];
                break;
            }
        }
        
        return targetView;
    }
    
    - (void)_handleGesture:(UILongPressGestureRecognizer *)sender {
        if (sender.state == UIGestureRecognizerStateBegan) {
            CGPoint point = [sender locationInView:self];
            [self _contextualMenuAction:point];
        }
    }
    
  42. Alexander Alexander says

    @ML
    Thanks for your solution.

    To hook your own code into the already existing long press gesture recognizer looks more elegant, that is correct. The only downside is that you’re relying on a certain structure of the private internals (private API) of the UIWebView object.

    I haven’t tested it yet, but I’m not sure if your solution can’t mess up anything. Especially when you remove the targets of the existing gesture recognizers of the target view – which can be an internal private sub view within the UIWebView object – you might break something, because you can not know for what these gesture recognizers are used for. Also it seems that you rely on a long press recognizer that already exists within the UIWebView, in a private subview of this object. And this can be dangerous, because the internal structure of UIWebView can change any time.

  43. ML says

    @Alexander

    Can’t agree more. This method certainly relies on the fact that the internal view uses UILongPressGestureRecognizer to handle touch events, which can easily change in future releas of iOS. So far it works for for iOS 5 & 6.

    As for removing the targets of existing gesture recognizer by accident, this can be fixed by changing the line to

    [r removeTarget:self action:@selector(_handleGesture:)];
    

    This removes only the target added by us.

  44. Alex says

    Hi, Alexander, Thanks for your post, There is still a problem to me: Is there anyway to show customise contextual menu for cross-domain iFrame in UIWebView? Thanks.

  45. Alexander Alexander says

    @Alex
    No, I’m sorry, this is not possible. The “same origin policy” of the web engine prevents that a web site from one domain can access the content of another domain. And because the UIWebView API only allows us to hook into the web page through the main document, our injected script always runs in the scope of the main document’s domain, so the content of frames from other domains is not accessible.

  46. Rahaman says

    I have created a callback on my uiwebview to trap the longpress gesture, but it is not getting called. I am able to trap the tap/pan gesture but no luck with longpress.. anyone has any idea?

    This is my code
    UILongPressGestureRecognizer *longGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self
    action:@selector(handleCatapultLongPress:)];
    longGesture.numberOfTapsRequired = 1;
    longGesture.numberOfTouchesRequired = 1;
    longGesture.minimumPressDuration = 0.5;
    longGesture.delegate = self;
    longGesture.allowableMovement = 50;

    [self.webView addGestureRecognizer:longGesture];

  47. sabari says

    Thanks for this great tutorial.

    I got one problem. when i rotate the device, UIActionSheet display position is changing.

  48. Alexander Alexander says

    @sabari

    You should use the method “showFromRect:inView:animated:” of the UIActionView class to define the location where the ActionSheet is positioned.

    This blog post is already “old”, it was written before I got my first iPad, so it still uses the old “showInView:” method to open the ActionSheet, which does not keep track of an anchor (which is fine for the iPhone, but not for the iPad).

    Please note that it can be unavoidable that the ActionSheet can be moved to the wrong location when rotating the device. A link which is visible in portrait orientation can be outside of the screen when the device is rotated to landscape orientation for example. If this is an issue in your case, you may close the ActionSheet programmatically or move it to another location after the rotation event.

  49. sabari says

    @Alexander.
    Thankyou for your speed reply.

    When device is rotate to landscape I closed the UIActionSheet. Now i am finding to get new position for display actionsheet. i dont know how to do this.if u have any idea ps help me.

  50. Alexander Alexander says

    @sabari
    In order to place the UIAlertSheet in the first place you must have calculated the coordinates within the web site, based on the coordinates from the native iOS user interface. If you keep the coordinates within the web site stored somewhere, you can do the reverse and calculate the native iOS UI coordinates based on these web site coordinates again.

    Though most of the time this shouldn’t be even necessary. The iOS usually places the UIActionSheet correctly automatically when rotation the device.

1 2 3 4



Some HTML is OK

or, reply to this post via trackback.