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 , , , , .


161 Responses

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

  1. nicola says

    Hello Alexander

    how would you deal with the user touching close enough to a link that UIWebView highlights it, but not really on the link, so that tapLocation does not fall into the tag and your javascript does not return it?
    I can’t imagine a way of doing it other than growing the bounding boxes of the links and then do a manual comparison; the problem is, you cannot be sure you’re doing it the same way UIWebView does and end up selecting the wrong link nearby.

    Thanks

  2. Alexander Alexander says

    @nicola
    This situation isn’t a big deal. If the link is not really hit by the user, the contextual menu won’t open. And the iOS selecting text for Copy&Paste actions isn’t unusual, so the user shouldn’t be confused.

  3. Kory says

    I’m sure I am probably missing something simple, but- how do you determine the tapped link in those cases where it is an image, but you are trying to get the link target? The src attribute only points to the URL of the image, but I need to get the actual link. How can this be done on the element that is tapped and held on?

    Thanks

  4. Alexander Alexander says

    @Kory
    The above example code lists the full list of elements that were hit, from the “leaf” node (which would be the IMG tag in your case) up to the root element (the HTML element). SO if you need the “link”, then just go to the root element, starting from the IMG tag until you find an “A” element and use its HREF attribute for the link URL. Waling through the HTML tree from a leaf node to the root is shown in the source code above. Only my code doesn’t do any useful (it just lists the tags it will find). You simply have to do something similar, but also look for “A” elements instead of collecting the tag names.

  5. Kory says

    Thanks Alexander. I checked for the “A” tag and set a variable to e.href (using a variation on your script) and that worked. I think I need to take a course on JavaScript!

    Your blog is amazing, thanks to you, iOS apps using UIWebView are 100X better than they would be without your help :)

  6. Sal says

    Hi Alexander,
    thanks for the tip! Highly informative. I have a very simple question. How did you learn about the CSS property “document.body.style.webkitTouchCallout=’none’;”? It (as well as any other CSS property unique to iOS UIWebView) I can’t find any of this in the Apple documentation for UIWebView

  7. Alexander Alexander says

    @Sal
    The information about these properties are located in the general Safari and WebKit web design documentations. For example here

  8. Sal says

    Thanks for the info Alexander. It’s no wonder I couldn’t find it. I was looking for info only in the iOS developer area!

    On a slight unrelated note, do you think it’s feasible to insert native controls (such as a custom UITableView for example) into a UIWebView and have it NOT be rejected by Apple? I know it is possible to invoke native Objective-C code from JavaScript and I’ve had some success embedding simple UIView objects into a UIWebView, but I had to make certain assumptions about the webview’s view hierarchy, and that’s a big no-no.

    Thanks again!

  9. Alexander Alexander says

    @Sal
    The UIWebView is also just a UIView object, so you can add subviews to it. This is probably fine. But I guess this is only useful if you need a general “overlay” over the UIWebView that does not need to directly interact with the web content. For content that needs to interact with the web content, it’s probably better to implement this via HTML, CSS and JavaScript directly within the web content.

    And it’s definitely a bad idea to access the internal view hierarchy of the UIWebView, because it can change with each iOS update.

  10. Sam says

    Hey,

    I am a newbie to start off. I am trying to create a navigation based app with at least three views. Could you please present some tutorial for me. I know there are a bunch of ‘em out there……but i am looking for something like, say, i have 10 states on RootViewController, if user selects one state, then i display 10 cities in FirstViewController and when a city is selected i display multiple schools in that city….so on…

    Is it possible for you to present something like this on bare bones.

    Any help is greatly appreciated.

    Thanks,
    Sam.

  11. Sam says

    I am sorry for being off the topic here……..

    Regards,
    Sam.

  12. Alexander Alexander says

    @Sam
    For the basics you should probably check out the examples from Apple. I think the X-Code installer should have already installed some of these in the folder “/Developer/Examples/iPhone/”, but you should also find these examples on the developers web page from Apple. This way you can find sample code for almost all important iPhone elements and components, like the navigation controller.

  13. Laurens says

    Where do I place this?

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

  14. Alexander Alexander says

    @Laurens
    You place this line somewhere in the initialization code of the object, which will open and handle the contextual menu (which could be the UIWebView delegate), which is the same object for which you’ve implemented the method “openContextualMenuAt:”. This line has to be called once, and then the object will be notified whenever the tap-and-hold gesture is recognized.

  15. Allen says

    When i do this to show a popover controller on iPad. The popover did show, but the link is still activated. How to disable that link action?

  16. Alexander Alexander says

    @Allen
    When exactly do you show the contextual menu? You should never show it for short taps
    because these will always open the link. You should only show the contextual menu when
    the finger is pressed on the screen for a longer time (a second or so).

  17. Allen says

    YES, I did it after a long pressed like exactly what you wrote in this article. After a long press, my contextual menu show up, when my finger leave the screen, the link is activated.

  18. Allen says

    BTW, my contextual menu is a popoverController , not a ActionSheet. Dos it have something to do with my problem?

  19. Allen says

    As my further test, If I show a UIActionSheet, the link will not be activated anymore. But the popoverController is what I need. I see icabMobile for iPad is using the popoverController. Did you use UILongpressGestureRecognizer instead? If you do , where did you add the gestureRecongize? The UIWebview or it’s parent view? Thx a lot.

  20. Alexander Alexander says

    @Allen
    In iCab I’m using an ActionSheet for the contextual menu. On the iPad these actionSheets do look very similar to a popoverController.

    Anyway, if you want to prevent that the link is opened, you can implement the delegate method “webView:shouldStartLoadWithRequest:navigationType:” and return NO in case your contextual menu is open. The delegate method is called each time the user taps
    on a link, so you can always prevent that the link will be opened.

  21. Jame says

    Hi Alexander,
    Thank you for your sharing!
    I’m using your code and it’s great. And I have one question related document.elementFromPoint(x,y) function. When I tap on center of button, app is captured button’s link. But when I tap on the area around the button, don’t capture the link. The document.elementFromPoint(x,y) function returned other element, not the button while it’s still clicking button on web view . What can I do in this case? Thank a lot!

  22. Alexander Alexander says

    @Jame
    I guess depending of the Stylesheet, the clickable area could be smaller then the visual area of an element. But this can also be related to the actual zoom factor. The more you’ve zoomed out, the less likely it is that you can hit the target element easily.

    In case you want to use a larger area for the hit testing, you have to write your own replacement function for “elementFromPoint(x,y)”. What you can do is to walk through the DOM tree and calculate the coordinates of each element yourself, add an additional boundary and check if this element is hit by the touch coordinates. This is not that difficult, but you have to be careful to look for invisible or hidden elements (CSS properties “display” and “visibility”), layered elements (CSS properties “z-index”, “position”), etc. which can make the whole stuff complicated again.

  23. Jame says

    I think that this problem is caused by a few attributes of the web view. For example my button: suppose the coordinate of the button is: left=50, top=50, right=200, bottom=200 on html window. When I click at point (x=150,y=150), it’s fine. “elementFromPoint(x,y)” function returns the button and web view know that it’s clicking button event too. And then,when I click at other point (x=210,y=210) – out of button’s area, the function returns a “span” element. Yes, this’s true! But in this case, web view is still know that it’s clicking button event. I write a JavaScript function for “onmousedown” event to show “event.clientX, event.clientY” properties on alert view. And I see that although I click at point(210, 210) but web view returns event.clientX=150,event.clientY=150.
    I mean that web view automatically scan a area around clicking point and returns a reasonable element, not exactly element at clicking point. It cause non-matching of “elementFromPoint(x,y)” function and “onmousedown” event. What is solution to “elementFromPoint(x,y)” function run as “onmousedown” event?
    Thank you so much!

  24. Alexander Alexander says

    @Jame
    You might have done something wrong when calculating the coordinates. Or maybe UIWebView does indeed takes a larger area into account to make touching buttons easier. The iOS does this in other areas as well. For example on the iPad when the keyboard is open and you’re typing very fast, you can tap about 1 cm above the backspace key (clearly missing the key) and the iOS will nevertheless behave as you would have hit the backspace key (but only when typing fast).

  25. Jame says

    Hi Alexander,
    Thank you for your answers!
    I understood my problem and I’m finding a solution.
    And one more question. How to disable Quicktime Plugin when I click a audio or movie link file?
    Thank you so much again!

  26. Alexander Alexander says

    @Jame
    I think you can’t disable them. If you know in advance that a link will open a movie or audio file because the URL itself includes a know file extension for video or audio files, you could detect this in the delegate method “webView:shouldStartLoadWithRequest:navigationType:” and by returning “NO” in this method you can prevent that the UIWebView will load this file.

  27. David Schiefer says

    This works, but in my case the alert is being cut off half the way. The action sheet’s height seems to be incorrect, even though I didn’t change it.

    Any ideas?

  28. David Schiefer says

    Turns out that this visual bug can be fixed by removing the cancel buttton by setting: cancelButtonTitle:nil

    However, the coordinates returned for the alert aren’t correct.

    [sheet showFromRect:CGRectMake(pt.x, pt.y, 0, 0) inView:browser animated:YES];

    returns an incorrect location.

    Any ideas?

  29. Alexander Alexander says

    @David Schiefer
    Please note that there are different coordinates involved here. The tap coordinates are measured relative to the window or view and are converted into coordinates relative to the HTML document. You can’t use the HTML coordinates to position the ActionSheet or Popover View. The HTML coordinates do include the zoom factor and scroll offset of the HTML document, but the popover view or action sheets needs the coordinates without these HTML-specific offsets and scaling.

    My simple example code only passes the HTML coordinates to the method “openContextualMenuAt:”, which is all you need on an iPhone. On an iPad you need both coordinates, the HTML coordinates for the JavaScript code and the view-based coordinates for the popover or action sheet.

  30. David Schiefer says

    @Alex

    First of all, thanks for your prompt response! I gathered this after a while. How would I get the view coordinates?

  31. Alexander Alexander says

    @David Schiefer
    Look at the method “contextualMenuAction:” which receives the coordinates for the UIWindow via notification and transforms them into the view coordinates of the UIWebView first and then into the HTML coordinates.

  32. Danilo Unite says

    Thanks very much for this article. This is exactly what I needed for my project. I appreciate all your effort in helping the developer community.

  33. Josh says

    This is great post but rather than completely doing this again from scratch just to simply customize the menu could you not just set the menuItems on the UIMenuController?

    This allows you to add extra items to the popup without having to rewrite it all yourself.

  34. Alexander Alexander says

    @Josh
    The UIMenuController is used for the Copy&Paste menu. So adding items to UIMenuController you add items to the copy & paste menu only, you can not customize the contextual menu of the UIWebVies this way. You have to do it from scratch when customizing the contextual menu.

    iCab Mobile is adding search and translation items to the copy & paste menu as well (via UIMenuController), but this is a different topic.

  35. Josh says

    @Alexander
    Apologies Alexander I misunderstood your post and thought you were talking about the menu rather than the contextual menu you get when holding your finger on a link.

    One quick glance at the first line of this post made me feel like a utter idiot. Apologies.

  36. max says

    mine wont show? okay so inside the view with the web view i put the contextual menu code in there and the dont show apples, then made the mywindow and webviewaddition and jstool.? dont and the observers in the view with a webview.

  37. Alexander Alexander says

    @max
    Very important: If you use a NIB/XIB file to define the main window for your App (which is the default for all standard App templates for iPhone and iPad Apps), you need to change the UIWindow class of the window object in the NIB/XIB file to MyWindow. Otherwise the touch events would be never delivered to your own window subclass because when loading the NIB, the App would still create a UIWebView instead of a MyWindow object.

  38. Cullen SUN says

    Wow, Thanks. Master of UIWebView!!
    I almost found all what I need for my project in your blog.

  39. Cullen SUN says

    Hi Alexander,

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextualMenuAction:) name:@”TapAndHoldNotification” object:nil];
    @selector should be (openContextualMenuAt:), right?

  40. Alexander Alexander says

    @Cullen SUN
    No “contextualMenuAction:” is correct here. This method will calculate the coordinates for the web page and then call openContextualMenuAt: with these coordinates.

  41. Cullen SUN says

    @Alexander
    Thanks for your confirmation. I missed out something.:D

  42. Cullen SUN says

    Hi Alexander,
    After doing some self_trying, I have some doubt in mind:

    #The recommended time point to inject javascript to the UIWebView is – (void)webViewDidFinishLoad:(UIWebView *)webView;
    Is this the only or best place for injection? If the user is loading a html page with 100 images, it may take minutes to load all the images. Can we program it in a way that user can save one of the loaded images while the page is still loading the rest of images (like safari)?

    Thanks a lot.

  43. Alexander Alexander says

    @Cullen SUN
    You can do this, though this can have some issues in general, though for the contextual menu I guess there’s nothing that can go wrong. For other tasks, for example those which modify page content, it can be important to make sure that the page has finished loading.

    For example you could inject the code at the time when you tap-and-hold your finger on the screen and the contextual menu should appear.
    .

  44. Cullen SUN says

    Hi Alexander,
    You mentioned “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.”
    I want to try UIGestureRecognizer API. However, when i add it to UIWindow subclass, the gesture handler function didn’t get called? Any hints?
    Thanks a lot.

  45. Cullen SUN says

    I realized that I should add the gesture recognizer to view controller.view.
    Thanks.

  46. Alexander Alexander says

    @Cullen SUN
    Yes, gesture recognizers are usually added in those views you want to catch the gesture. But I guess for UIWebView this can be still tricky, because internally the UIWebView consists of a complex (private) view hierarchy, which already uses several gesture recognizers for the various gestures the UWebView supports. So depending of what you try to achieve, it can be easy or maybe even impossible to use a gesture recognizer.

  47. Cullen SUN says

    Hi Alexander,
    Thanks a lot for your reply.
    I am able to get the gesture event from a view that contains the UIWebView as its subview. and set

    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
        
        return YES;
    }
    

    However, I am having a headache now because I am not very clear about the HTML document page size, pageOffset stuff. struggling with finding the right way to calculate the coordinate.

    The view that add Longpress guesture is called containerView, and the UIWebView is called theWeb. theWeb has exactly the same frame size as the containerView, however theWeb has a contentInset by setting (iOS5):
    theWeb.scrollView.contentInset=UIEdgeInsetsMake(60, 0, 0, 0);

    I use the following function to calculate the HTML page element coordinate. I have been struggling here for almost ten hours, just couldn’t get the right tag as expected. Appreciate a lot if you can give me some hint.

    - (void)contextualMenuAction:(CGPoint)hitPositioninContainerView
    {
        CGFloat adjustment;
        if (theWeb.scrollView.contentOffset.y<0) {
           adjustment =theWeb.scrollView.contentOffset.y;
        } else{
           adjustment =0.0;
        }
        NSLog(@"pointAddressin adjustment is :%f",adjustment);
        CGPoint pt;
        
        // convert point from view to HTML coordinate system
        CGPoint offset  = [theWeb scrollOffset];
        CGSize viewSize = [theWeb frame].size;
        CGSize windowSize = [theWeb windowSize];
        NSLog(@"hit point is :%f, %f",hitPositioninContainerView.x, hitPositioninContainerView.y);
        NSLog(@"webviewoffset is :%f, %f",offset.x, offset.y);
    
        NSLog(@"window calculated size is w:%f, h: %f", windowSize.width, windowSize.height);
        NSLog(@"webview Frame size is w:%f, h: %f", viewSize.width, viewSize.height);
    
        CGFloat f = windowSize.width / viewSize.width;
        pt.x = hitPositioninContainerView.x * f + offset.x;
        pt.y = (hitPositioninContainerView.y+adjustment) * f + offset.y;
        NSLog(@"result is :%f, %f",pt.x, pt.y);
        [self openContextualMenuAt:pt];
    }
    
  48. Cullen SUN says

    Hi Alexander,
    I am getting closer to the point now. I just found that when I click the same text in a webpage but with different zoom in/out scale and different scrolling position of the webView, the output of the function is about the same. That told me that my calculation is correct.

    Then I couldn’t get the image tag when clicking to the same image sometime might because my javascript function to find the image SRC. My function is as below. Could you tell where I can improve? Expert. Thanks a lot.

    function MyAppGetHTMLElementsAtPoint(x,y) {
        var IMG_URL = "";
        var e = document.elementFromPoint(x,y);
        while (e) {
             var temURL=e.getAttribute('src');
            
            if(temURL){
                if (temURL.indexOf("http")==0&&(temURL.indexOf("jpg")>0||temURL.indexOf("png")>0||temURL.indexOf("gif")>0||temURL.indexOf("bmp")>0)){
                    return temURL;
                }
                
            }        
            e = e.parentNode;
        }
        return IMG_URL;
    }
    
  49. Alexander Alexander says

    @Cullen SUN
    The transforming of the UIView coordinates to the web coordinates should be more or less identical with both approaches (gesture recognizer and my approach from the blog post).

    Finding the IMG tag should be done a little bit different. You should check for an element with a tagName “IMG” (or “img”) and then directly read the “src” property (element.src) instead of reading the “attribute” value via getAttribute(“src”).

    “element.src” and “element.getAttribute(‘src’)” do not return the same value. While getAttribute() returns the exact value of the attribute that is defined in the HTML code, “element.src” will also resolve the URL and will return an absolute URL, even if the HTML code only defined a relative
    one.

    An example:
    <img src=”image.jpg”>

    imgElement.src will return something like “http://www.domain.tld/path/image.jpg” while imgElement.getAttribute(‘src’) will only return “image.jpg”.

    So your test for “http” can easily fail, even if you’ve found the img element.

  50. Cullen SUN says

    Thanks Alexander for your great answer. I will improve my code with your suggestions.

1 2 3 4



Some HTML is OK

or, reply to this post via trackback.