How to capture touches over a UIWebView

This post was updated with corrections to work with iOS5.

I spent the last few weeks polishing the What They Speak When They Speak to Me (WTS) iOS application I developed with Obx Labs, which I mentioned in a previous post. Developing a working prototype for an iOS application can be done fairly quickly, thanks to the tools provided by Apple and a growing list of libraries and engines, but the more time consuming part of development takes place later, working with and often against the features embedded in the iOS SDK to polish your product. This how-to is about one specific issue that arose during the development of WTS: capturing touches over a UIWebView without losing its functionality.

This short tutorial assumes that you know how to place a UIWebView (or other touch swallowing views) in your app, and that you are at the frustrating point of trying to catch touch events over the Web View to implement some interactive behaviour. In my case, I wanted to swipe the Web View left and right to show or hide it, similarly to how the Twitter for iPad app manages its browser tab.

We’ll start from scratch and go through the following four steps. You can jump to step 4 if you’re looking for the meat of this tutorial.

  1. Create a new XCode project using the “View-based application” template.
  2. Add the standard methods to manage touch events.
  3. Add a UIWebView covering the main view.
  4. Add a custom class that extends UIWindow to capture touch events.

1. Create a new XCode project using the “View-based application” template.

This step is self explanatory. From XCode you select from the main menu: File > New Project, and them “View-based Application” which is under the “Application” template folder. This is here mainly to have a common code base from which to start the tutorial; I named the project “CaptureTouch”.

2. Add the standard methods to manage touch events.

Before we get to the problematic UIWebView, we want to make sure that touch events get to the application’s main standard view. The new project you created in step 1 should contain a view controller named CaptureTouchViewController. In the implementation file of this view controller, add the four standard touch management methods:

- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    NSLog(@"Touches began");
}
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
    NSLog(@"Touches moved");
}
- (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
    NSLog(@"Touches ended");
}
- (void) touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
    NSLog(@"Touches cancelled");
}

At this point, if you build and run the application, you should see a gray background (the view), and clicking anywhere on the background will output “Touch began” to the console.

3. Add a UIWebView that covers the main view.

As mentioned before, we assume you know how the UIWebView works, so we won’t get into too much detail. All we need is the simplest UIWebView covering the application’s main view. We will add the UIWebView in the .xib file created by XCode, but first we need to add an attribute and outlet to the CaptureTouchViewController. Your CaptureTouchViewController.h file should then look like the following:

@interface CaptureTouchViewController : UIViewController {
	UIWebView* webView;
}
@property (nonatomic, retain) IBOutlet UIWebView *webView;
@end

With the outlet created, you can open the CaptureTouchViewController.xib file in Interface Builder. Open the “View” object, and then drag-and-drop a new Web View into it. The Web View should automatically expand to cover the whole view. Right-click on the Web View, and then link a “New Referencing Outlet” with the “webView” attribute you created above by clicking the “New Referencing Outlet”, dragging to the “File’s Owner” object, and then selecting the “webView” from the list that pops up.

At this point, you linked the interface to the “webView” attribute, but it is not loading any html. We will need one last bit of code before we get to the core of this tutorial, which will load a url to make sure the Web View is working correctly. After the application’s main view finished loading, the CaptureTouchViewController’s viewDidLoad: method is called. This is where we add the few lines that will load a url into the web view. Your viewDidLoad: method should look like the following after the changes:

- (void)viewDidLoad {
    [super viewDidLoad];

	//Load the request in the UIWebView.
	NSURL *url = [NSURL URLWithString:@"http://www.wyldco.com"];
	NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];
	[webView loadRequest:requestObj];
}

If you build and run the application, you should see the web page. Everything looks fine, but if you touch anywhere on the screen, you’ll notice that the “Touch began” message from step 2 does not appear in the console anymore. The Web View swallows the touches to manage scrolling and displaying the magnifying glass if you hold down a touch over text, and it blocks touch events from getting to the view. The next step shows how to capture those touch events.

4. Add a custom class that extends UIWindow to capture touch events.

There are different ways to capture touches over a Web View. One would be to extend the UIWebView class, but Apple says you should not, so we will stay away from that solution in case it causes problem later. Instead, we are going to extend the UIWindow class, and capture touch events before they get propagated to the correct view(s). The first thing you’ll need is a new class, let’s call it TouchCapturingWindow, with the following header and implementation files:

#import <Foundation/Foundation.h>

@interface TouchCapturingWindow : UIWindow {
    NSMutableArray *views;

@private
    UIView *touchView;
}

- (void)addViewForTouchPriority:(UIView*)view;
- (void)removeViewForTouchPriority:(UIView*)view;

@end
#import "TouchCapturingWindow.h"

@implementation TouchCapturingWindow

- (void)dealloc {
    if ( views ) [views release];
    [super dealloc];
}

- (void)addViewForTouchPriority:(UIView*)view {
    if ( !views ) views = [[NSMutableArray alloc] init];
    [views addObject:view];
}

- (void)removeViewForTouchPriority:(UIView*)view {
    if ( !views ) return;
    [views removeObject:view];
}

- (void)sendEvent:(UIEvent *)event {
    //we need to send the message to the super for the    //text overlay to work (holding touch to show copy/paste)
    //NOTE: this used to be called at the beginning of this method
    //for the copy/paste and magnifying class overlay to work. As
    //of iOS5, it stopped working, and this code needs to go at
    //the end of this method.    //[super sendEvent:event];    

    //get a touch
    UITouch *touch = [[event allTouches] anyObject];

    //check which phase the touch is at, and process it
    if (touch.phase == UITouchPhaseBegan) {
            for ( UIView *view in views ) {
                //NOTE: I added the isHidden check so that hiding the windows doesn't catch events
                //and changed it checks if the touch is in the frame.
                //if ( CGRectContainsPoint([view frame], [touch locationInView:[view superview]]) ) {
                if ( ![view isHidden] && [view pointInside:[touch locationInView:view] withEvent:event] ) {    
                    touchView = view;
                    [touchView touchesBegan:[event allTouches] withEvent:event];
                    break; //NOTE: this used to be a return in the previous version
                }
            }
    }
    else if (touch.phase == UITouchPhaseMoved) {
        if ( touchView ) {
            [touchView touchesMoved:[event allTouches] withEvent:event];
        }
    }
    else if (touch.phase == UITouchPhaseCancelled) {
        if ( touchView ) {
            [touchView touchesCancelled:[event allTouches] withEvent:event];
            touchView = nil;
        }
    }
    else if (touch.phase == UITouchPhaseEnded) {
        if ( touchView ) {
            [touchView touchesEnded:[event allTouches] withEvent:event];
            touchView = nil;
        }
    }

    //we need to send the message to the super for the
    //text overlay to work (holding touch to show copy/paste)
    [super sendEvent:event];
}
@end

This class is heavily inspired by Michael Tyson’s tutorial, with a few changes and some added notes about the implementation. Here’s how it works. The TouchCapturingWindow overrides the sendEvent: method of UIWindow to check if touch events should be sent to certain views instead of only the top view, which is more or less the default behaviour. If you intend to have multiple views in your application, you probably don’t want to have all of them capture touch events, so the TouchCapturingWindow provides methods (i.e. addViewForTouchPriority: and removeViewForTouchPriority:) to add and remove the specific view(s) you want to touch. Once you’ve replaced the standard UIWindow with an instance of this custom class, touch events will go through the sendEvent: method, and allow you to redirect them to the correct view(s) based on any criteria. In the above case, the only criteria is if the touch falls inside the frame of any of the views that were added to the TouchCapturingWindow.

First, you need to change the default “window” of your application’s delegate to use the new custom class. Open the CaptureTouchAppDelegate.h file, and replace the UIWindow class by TouchCapturingWindow; don’t forget to import the header, which should give you something like this:

#import <UIKit/UIKit.h>
#import "TouchCapturingWindow.h"

@class CaptureTouchViewController;

@interface CaptureTouchAppDelegate : NSObject <UIApplicationDelegate> {
    TouchCapturingWindow *window;
    CaptureTouchViewController *viewController;
}

//NOTE: After updating to iOS5 and the latest XCode, this line started showing
//a warning, so to remove it, simply rename TouchCapturingWindow to UIWindow.
//@property (nonatomic, retain) IBOutlet TouchCapturingWindow *window;
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet CaptureTouchViewController *viewController;

@end

After you changed the window in the code, you’ll need to adjust the MainWindow.xib to also reflect this change. Open MainWindow.xib, and change the class of its window object from UIWindow to the new TouchCapturingWindow.

Now that the window can propagate touch events the way we want, we need to tell it which view to prioritize. In this case, we want the application’s main view to receive the touch events that would normally be blocked by the Web View covering it. To add the view to the priority list, you’ll need to modify the applicationDidFinishLaunchingWithOptions: method of the CaptureTouchAppDelegate. Just after the window is made visible, add the view using the new addViewForTouchPriority: method, which should give you the following:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Add the view controller's view to the window and display.
    [window addSubview:viewController.view];
    [window makeKeyAndVisible];

    //add the view to the touch priority list
   [window addViewForTouchPriority:viewController.view];

    return YES;
}

Conclusion

At this point, if you run the application, you can see that touching anywhere on the Web View outputs the “Touch began” message of step 2. You can now use the touch events to add some touch-based interaction to your application, but be careful not to conflict with the Web View’s features such as scrolling and text selection.

At the beginning I mentioned that I wanted to keep all the Web View’s features. This is accomplished by one short but important line of code. In the sendEvent: method of the new TouchCapturingWindow class, the line [super sendEvent:event] assures that the Web View receives the event before we propagate it to the main view. As of iOS5, placing that line at the beginning of the method stopped working, and it now needs to be at the end of the method. Placing it at the end keeps all the Web View’s features for iOS5, but does not show them for devices with

[super sendEvent:event];

On a final note, if you look at the sendEvent: method, you'll notice that touch events are propagated to a view only if the location of the touch is inside the frame of the view. This is a common behaviour, but there is no reason why you should always stick to it. You might want to check the state of a view to decide if the view should receive touches, control the view by touching outside its visual frame, or send event to a specific view only after the user tapped around up, up, down, down, left, right, left, right...

Download
CaptureTouch Xcode project (pre-iOS5)
CaptureTouch Xcode project (iOS5)

Related links
Michael Tyson's (A Tasty Pixel) trick for capturing all touch input
Satoshi Nakagawa's WebViewTappingHack
Mithin Kumar's post on detecting taps and events on UIWebView – The right way

This entry was posted in code, how to, iOS, obx, software development and tagged , , , , , . Bookmark the permalink.

28 Responses to How to capture touches over a UIWebView

  1. Andy says:

    Life-saver, worked like a charm. A lot of the other ways of doing this are hacky, and no longer work under 4.0. This is clean and lets you do whatever you need to do!

    Thanks!

  2. Prazi says:

    Great solution to detect touches in UIWebView in a right way. Much appreciated. Cheers.

    • Prazi says:

      In step 2. “Add the standard methods to manage touch events.”
      Before we get to the problematic UIWebView, we want to make sure that touch events get to the application’s main standard view. The new project you created in step 1 should contain a view controller named CaptureTouchViewController. In the header file of this view controller, add the four standard touch management methods:

      Just wondering… do you misprinted words “In the the header file …” instead of (.m) “In the implementation file…” to add the touch methods.

      Rest, its a great tutorial to handle touch event in UIWebview. Thanks a lot.

      • bnadeau says:

        Thanks for catching that. My brain is still mostly formed by c++ code, so it went straight to thinking about adding the methods in the header first. Implementation it is.

  3. Sam Takoy says:

    How do you use this great idea to capture gestures like single taps?

    Thanks!

    • bnadeau says:

      What are you trying to do exactly? Do you mean using Apple’s gesture recognizers, or how to parse and track touch events to detect single taps?

  4. Anupam says:

    I am trying to implement this…But the application is

    Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[UIWindow addViewForTouchPriority:]: unrecognized selector sent to instance 0x4b454a0′

    Please can you give me the source code or can you give me the tips to solve these problems………

    • bnadeau says:

      It looks like you initialized your ‘window’ object in your app delegate as a UIWindow instead of TouchInterceptionWindow.

      I update the blog post to include the source code (at the bottom). That must have skipped my mind when I first posted it.

  5. Duncan Payne says:

    I have tried implementing this solution for a UIWebView which is present in a view other than the main one, and have come across some problems.

    The main view, which defines the UIWindow does not contain a UIWebView, but through the click of a button takes the user to a different view. If I implement this as above, I run into all sorts of issues. Would you expect your solution to work for this case, or do you think that there need to be some changes?

    The web view is placed inside a UIScrollView, and I have tried to adapt your ideas to subclass the UIScrollView, but have also not been successful in this. Rather than getting an error message in the console, though, I am getting the following:
    warning: Unable to restore previously selected frame.

  6. Phoenix says:

    Very nice. But I have some problem.
    When I long touch on UIWebView, glass icon shows and I never catchs touchesEnd event?
    Can I solve this problem?

    • bnadeau says:

      Sorry for the slow reply. Are you having problems using this code with iOS5? I’ve had to make a few small changes since the iOS update, but haven’t had time to update this post.

  7. Ash Cairo says:

    Thanks for also including the sample code.

    It’s great being part of such a nice programming community :)

  8. Attila says:

    Can you throw some light on how this would work when Storyboard is used and there is no XIB. Do we only have to replace the UIWIndow to TouchCapturingWindow in the AppDelegate.h and everything else remains the same.

    • bnadeau says:

      I haven’t had the need to use Storyboard yet, so I’m only guessing here. It looks like Storyboard groups many XIB files under one roof. In theory, you should be able to follow this tutorial, make sure you use the CaptureTouchViewController when you build your storyboard, use the TouchCapturingWindow when you define your window attribute in the delegate (the property type can/should stay as UIWindow*), and then add the view(s) you want to receive touches with the addViewForTouchPriority. As long as your storyboard is linked with a UIViewController object that you can access, you should be able to pass that controller’s ‘view’ to the TouchCapturingWindow.

  9. Douglas Schmidt says:

    Very nice and clean solution. But unfortunately it doesn’t work if you want to put the UIWebView into a UIScrollView. Even a scrollview subclass doesn’t work as expected. The touchesBegan and company are all called, but for some weird reason the scroll view ignore it all and don’t scroll.

    If someone had accomplished to make the uiscrollview actually scroll, please let me know! I’m really tried every thing I could imagine.

    • Bruno Nadeau says:

      I wasn’t exactly sure what you are trying to do, but I was curious. I took a shot at it. Here’s an updated Xcode project: CaptureTouch Xcode project (with UIScrollView). The Xib file is only for iPhone/iPod, but the concept is the same for the iPad. Let me know if that fixes your problem.

      • Douglas Schmidt says:

        Hey! Thanks for the reply, but I can’t download the file from the link you posted. Could you please send it directly to my email?

        What I’m trying to do is to have a horizontal paginated uiscrollview with various elements, and one of the “pages” are uiwebviews. I actually have managed to pass the touches to a UIScrollView subclass, but couldn’t make the scrollview “behave” like it should and scroll normally.
        The touchesBegan is called and from there I’m changing the contentoffset manyally, but it doesn’t seams right and my scrolling implementation is very buggy.

        Will be very happy if you managed to do this!

  10. Chris says:

    Thanks for this article, I’m eager to try it out! I’m thinking about trying this method to duplicate mobile safari’s touch-hold to download images on a uiwebview. Could this method be used with a UILongpressgesture? Otherwise, any other way to have the touch-hold functionality? Thank you!

    • Bruno Nadeau says:

      That’s a good question. You could probably tweak the code of the TouchCapturingWindow to pass it a UIGestureRecognizer as (I think) they use the same set of touchesBegan to touchesEnded methods, or you might just want to implement it from scratch using a timer. Touch, start timer, check if touch has ended or moved too much when timer is triggered, if not you have your long pressure. To implement the download image feature, you would also to find the position of the image in the UIWebView with a javascript function and adjusting for the scroll position; I can’t think of another way to get an element’s position in a UIWebView. If you tweak the code of this example to make it work for what you need, sent it my way, and I could add it to this post if you want.

      • Douglas Schmidt says:

        Thanks for the prompt response.

        I forgot to tell you that some web pages scroll ok inside the scrollview, but others don’t scroll at all. Specially pages using touch apis like Sencha Touch.

        Check on your example the page: http://touchstyle.mobi/app

        And increase the contentsize area to allow scrolling.

        What I have manage to do until now is to pass the touches to a subclass of UIScrollView and force the scroll using the contentOffset prop. But of course it is buggy because it doesn’t has the kinect effect uiscrollviews have normally.

        Maybe I’m overlooking this, maybe there is a way to call the default scroll behavior of scrollview on it beginstouches handler…

        • Bruno Nadeau says:

          Thanks for pointing this out. I never thought of testing pages that use Javascript frameworks like Sencha Touch. I must admit that original feature I used this code for was fairly simple, it was a static internal UIWebview without scrolling. That snippet of code might be starting to show the extent of what it can do without significant tweaks.

          For controlling the scrollview, I’ve always had trouble passing touch events forward through the standard touch methods. One way I can think of, although that’s a bit like reinventing the wheel, is to control the scroll through the setContentOffset method, but that probably means implementing the bouncing too. Unless setting the offset outside the bounds automatically bounces the scroll view, I never tried, but I feel that’s unlikely.

  11. Pete says:

    Hey all!

    I wanted to add this here for you all. I’ve been struggling with exactly this for a couple of days. I found the tutorial on mithin, which is nicely written, but doesn’t match my needs, I needed all touch events to be passed, not just where the user touched. Thankfully, this tutorial was exactly what I wanted, so thank you!

    My trouble with this tutorial is that I’m new to iOS programming, only been doing it a couple of months! Unfortunately, that means the projects I’ve been working on are all iOS SDK 5.1, using storyboards and ARC (I know I should learn about memory management in more depth, but I haven’t the time right now).

    I couldn’t get this code working, as I didn’t know how to change the class of UIWindow without access to the .xib file. I’ve solved that now, and can now parse touch events!

    Here’s what I did:

    The first thing to do is change a line in CaptureTouchAppDelegate.h, from:
    @property (retain, nonatomic) IBOutlet UIWIndow *window;

    to:
    @property (strong, nonatomic) IBOutlet TouchCapturingWindow *window;

    I got no error having my custom UIWindow sublass here, although it did complain about retain, strong fixed that. Also in CaptureTouchAppDelegate.h I had to add #import “CaptureTouchViewController.h”

    Next, in the AppDelegate.m file, (It’s worth noting that you need to add the @synthesize statement for viewController, as the code above doesn’t show this) I had to add the following method as a replacement for changing the window’s subclass via interface builder:

    -(TouchCapturingWindow *)window
    {
    static TouchCapturingWindow *staticWindow = nil;
    if (!staticWindow) {
    staticWindow = [[TouchCapturingWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    }
    return staticWindow;
    }

    Nearly there! The last thing I had to do was figure out how to get the view controller! Without this last line of code, the app would crash out, complaining it couldn’t add a nil object to the views NSMutableArray.

    In -(BOOL)application:didFinishLaunchingWithOptions: add the following line before
    [self.window addSubview:viewController.view];

    viewController = [[UIStoryboard storyboardWithName:@"MainStoryboard" bundle:[NSBundle mainBundle]] instantiateInitialViewController];

    That’s it! now I get Touches began/ended/etc. messages appearing in the console! Now to process the touches how I like :)

    • Pete says:

      Hi All, I want to post a quick update to this for you. I was having no end of trouble in that I could not get an image to display on the uiwebview where I clicked. It turns out the code at the end of my previous comment is wrong! You do NOT want to instantiate a new view controller as I have done there. Instead you want to use this code:

      viewController = self.window.rootViewController;

      And there we go, that’s solved my day of banging my head against the wall :D I hope someone finds this useful!

  12. Randeep says:

    Thanks it helped me a lot. Great tutorial.

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>