Detecting taps and events on UIWebView – The right way

August 26, 2009 at 10:25 am 56 comments

Recently, I was working on a project which required detection of tap and events on the UIWebView. We wanted to find out the HTML element on which the user taps in the UIWebView and then depending on the element tapped some action was to be performed. After some Googling, I found out the most of the users lay a transparent UIView on top of the UIWebView, re-implement the touch methods of UIResponder class (Ex: -touchesBegan:withEvent:) and then pass the events to the UIWebView. This method is explained in detail here. There are multiple problems with the method.

  1. Copy/Selection stops working on UIWebView
  2. We need to create a sub-class of UIWebView while Apple says we should not sub-class it.
  3. A lot other UIWebView features stop working.

We ultimately found out that the right way to implement this is by sub-classing UIWindow and re-implementing the -sendEvent: method. Here is how you can do it.

First, create a UIWindow sub-class

#import <UIKit/UIKit.h>
@protocol TapDetectingWindowDelegate
- (void)userDidTapWebView:(id)tapPoint;
@end
@interface TapDetectingWindow : UIWindow {
    UIView *viewToObserve;
    id <TapDetectingWindowDelegate> controllerThatObserves;
}
@property (nonatomic, retain) UIView *viewToObserve;
@property (nonatomic, assign) id <TapDetectingWindowDelegate> controllerThatObserves;
@end

Note that we have variables which tell us the UIView on which to detect the events and the controller that receives the event information. Now, implement this class in the following way

#import "TapDetectingWindow.h"
@implementation TapDetectingWindow
@synthesize viewToObserve;
@synthesize controllerThatObserves;
- (id)initWithViewToObserver:(UIView *)view andDelegate:(id)delegate {
    if(self == [super init]) {
        self.viewToObserve = view;
        self.controllerThatObserves = delegate;
    }
    return self;
}
- (void)dealloc {
    [viewToObserve release];
    [super dealloc];
}
- (void)forwardTap:(id)touch {
    [controllerThatObserves userDidTapWebView:touch];
}
- (void)sendEvent:(UIEvent *)event {
    [super sendEvent:event];
    if (viewToObserve == nil || controllerThatObserves == nil)
        return;
    NSSet *touches = [event allTouches];
    if (touches.count != 1)
        return;
    UITouch *touch = touches.anyObject;
    if (touch.phase != UITouchPhaseEnded)
        return;
    if ([touch.view isDescendantOfView:viewToObserve] == NO)
        return;
    CGPoint tapPoint = [touch locationInView:viewToObserve];
    NSLog(@"TapPoint = %f, %f", tapPoint.x, tapPoint.y);
    NSArray *pointArray = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%f", tapPoint.x],
    [NSString stringWithFormat:@"%f", tapPoint.y], nil];
    if (touch.tapCount == 1) {
        [self performSelector:@selector(forwardTap:) withObject:pointArray afterDelay:0.5];
    }
    else if (touch.tapCount > 1) {
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(forwardTap:) object:pointArray];
    }
}
@end

Implement the sendEvent method in the above way, and then you can send back the information you want back to the controller.

There are few things that one needs to keep in mind. Make sure in your MainWindow.xib file, the window is of type TapDetectingWindow and not UIWindow. Only then all the events will pass through the above re-implemented sendEvent method. Also, make sure you call [super sendEvent:event] first and then do whatever you want.

Now, you can create your UIWebView in the controller class in the following way

@interface WebViewController : UIViewController<TapDetectingWindowDelegate> {
    IBOutlet UIWebView *mHtmlViewer; 
    TapDetectingWindow *mWindow;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    mWindow = (TapDetectingWindow *)[[UIApplication sharedApplication].windows objectAtIndex:0];
    mWindow.viewToObserve = mHtmlViewer;
    mWindow.controllerThatObserves = self;
}

Remember you’ll need to write the method userDidTapWebView in your controller class. This is the method that is called in order to send the event information to the controller class. In our case above we are sending the point in the UIWebView at which the user tapped.

Hope this was useful. Please let me know your suggestions and feedback.

About these ads

Entry filed under: Geeky stuff. Tags: , .

Freelancers versus Employees: The Difference 5 Simple ways to increase productivity

56 Comments Add your own

  • [...] Original Link:   Detecting taps and events on UIWebView – The right way [...]

    Reply
  • 2. alex  |  October 19, 2009 at 5:40 pm

    What is OverlayedWebWindow?

    Can you share source code of the project?

    Thanks!

    Reply
    • 3. mithin  |  October 19, 2009 at 7:29 pm

      Alex: There were some extra lines in the code. I have removed them now. Thanks for notifying. All the code you need is in the tutorial above. Just create the required files and copy paste the code. It should work. Let me know if you still have any questions.

      Reply
      • 4. N  |  March 10, 2010 at 12:45 am

        A source code of this project would be appreciated. I don’t know where to create what type of class…

        How do I have to begin? With an window based application?
        “First, create a UIWindow sub-class” how do I so? create file > Objective C class > UIView?

        I have some VERY basic questions that could be answered seeing the source code.

  • 5. Kimcha  |  November 27, 2009 at 5:33 am

    Thanks a lot, this helped me a ton!

    Reply
  • 6. Alex Reynolds  |  December 18, 2009 at 4:33 pm

    Thank you very much for posting this. This integrated pretty easily into my larger project.

    Reply
  • 7. YoungJoon Chun  |  December 30, 2009 at 9:08 pm

    Nice work. Thanks for the info. Is there a way to set the custom window not using InterfaceBuilder?

    Reply
    • 8. YoungJoon Chun  |  December 30, 2009 at 9:37 pm

      Plz Nevermind:) I was confused before actually trying it.

      Reply
  • [...] Detecting taps and events on UIWebView – The right way « The Spoken Word. [...]

    Reply
  • 10. nad  |  January 8, 2010 at 11:37 pm

    Great! I googled a lot in order to find a solution for detecting a tap on a UITextView.
    Thanks

    Reply
  • 11. Jason  |  January 13, 2010 at 3:33 am

    Pure genius. Nice work!

    I’ve been trying 1000 ways to do this and the best I ever came up with was method swishing for the internal touch method (on WebDocument). Got rid of that for fear Apple would reject it. But this method works for me nearly as well and shouldn’t get rejected!

    Reply
  • 12. Jason  |  January 13, 2010 at 3:42 am

    One other thought: I also added this to the -(void)viewDidUnload selector (in WebViewController) to clean up:

    mWindow = (TapDetectingWindow *)[[UIApplication sharedApplication].windows objectAtIndex:0];
    mWindow.viewToObserve = nil;
    mWindow.controllerThatObserves = nil;

    Don’t want to leave any “listeners” going after the ctrl dies off – plus that view being watched needs to get released.

    Reply
  • 13. spottygrades  |  January 15, 2010 at 9:42 am

    hey thanks for the workaround!

    Reply
  • 14. Monika  |  February 7, 2010 at 6:03 am

    Great job, very useful indeed!

    Now how would one go about sending the word where the tap happened instead of the point?

    Reply
  • 15. Jack  |  February 9, 2010 at 11:38 pm

    First of all, thanks a lot for this. Secondly – can someone explain to me what this piece of code does:

    if ([touch.view isDescendantOfView:viewToObserve] == NO){
    return;
    }

    Thanks

    Reply
    • 16. Jobs  |  March 21, 2010 at 9:33 pm

      to capture events for webView(var:viewToObserve)

      Reply
  • 17. AlexT  |  March 8, 2010 at 5:52 pm

    Thanks for this post. Would you show me how to write the -userDidTapWebView method?

    Reply
  • 18. dRine  |  March 11, 2010 at 2:49 am

    Hi,

    Thanks for this very useful tutorial. I’ve searched a lot to do that. But now I need to handle other events. I would like to detect swipes. Could you help me ?
    I know how to detect swipes in a “classic” view, I just don’t know how to do in yours.

    Thanks in advise

    Reply
  • 19. dRine  |  March 11, 2010 at 2:20 pm

    Ok, I’ve separated the three phases :

    In UITouchPhaseBegan, I put in memory the touch position.
    For phases != UIPhaseBegan && != UITouchPhaseEnded, I test the direction of the touch with the memorized start touch position. I test if the direction stays the same all long (only from the start touch position).
    If the direction is the same all long, in the TouchPhaseEnded, I transmit the action to do.

    Thanks a lot for the article, very helpful !

    Reply
  • 20. Matt Long  |  March 27, 2010 at 3:20 am

    Thanks for this. It works well and without any private API hacks. I appreciate it.

    -Matt

    Reply
  • 21. Harry  |  April 20, 2010 at 4:54 am

    Massively helpful, thanks- worked fine first time.

    Reply
  • 22. Chris  |  April 29, 2010 at 11:03 pm

    I’ve implemented this and it works. However, the copy and paste functionality seems to have disappeared? Is this the behavior I should expect? I’d like to keep Copy and Paste working in the web view.

    Reply
    • 23. Chris  |  April 30, 2010 at 12:52 am

      Never mind. It seems when you load up nytimes.com in Safari, it doesn’t let you copy and paste for some strange reason.

      Reply
  • 24. Saving an Image from UIWebView | Steili.com  |  May 1, 2010 at 8:37 am

    [...] a huge PITA to handle touches in a UIWebView? Like to the point where you’re either having to sub-class UIWindow to trap touches or you’re having to add a view above the UIWebView. I’m going to leave [...]

    Reply
  • 25. Markus  |  May 31, 2010 at 8:53 pm

    Hi!

    First things first: thanks for posting your way of doing it.

    I tried your solution but autorotation stopped working for me.

    Did i do something wrong or is it supposed to be that way if i subclass UIWindow?

    Any ideas how to solve this issue?

    best regards

    Reply
  • 26. loosy  |  June 22, 2010 at 6:55 pm

    Hi, Thanks for code. But I’m getting SIGBAR error due to indexOutOfRange at

    mWindow = (TapDetectingWindow *)[[UIApplication sharedApplication].windows objectAtIndex:0];

    Any help will be appreciated.

    Reply
    • 27. Alan Moore  |  July 12, 2010 at 7:27 am

      Did you ever figure this out? I’m getting this same error when I test on iPad simulator 3.2.

      Reply
      • 28. dkilmer  |  July 20, 2010 at 9:59 pm

        For anyone who’s experiencing this problem:

        – Open MainWindow.xib in Interface builder
        – In the attributes of the Window, make sure “Visible at launch” is checked.

        This solved the problem for me.

        Thanks for posting the article — this approach gives me a much better feeling.

  • 29. Ernie  |  June 24, 2010 at 9:24 pm

    Great article. I used one of the other workarounds before (add clear subview to UIWebView and forward touchs to its scrollview).. and now it doesn’t work in iOS 4.0. I just got it all working again using this method. Intimidating at first, but pretty straightforward once you figure it all out. Thanks.

    Reply
  • 30. Abraham Neben  |  June 27, 2010 at 11:24 pm

    Thanks for the tip. Subclassing UIWindow does seem to be the most natural thing to do.

    Reply
  • 31. Joe  |  June 29, 2010 at 5:57 pm

    Excellent! Question for you: Can this be twisted a bit to detect when the user has scrolled to the end of a given UIWebView’s content? I’m thinking we’d have to check scroll events and see if they hit a hard limit of some sort, but that already sounds very fragile. Hmm …

    Reply
  • 32. Nyx0uf  |  July 2, 2010 at 8:01 pm

    Excellent post ! Thanks for this tip which save me lot of time :)

    Reply
  • 33. Jorge  |  July 9, 2010 at 4:23 pm

    I’m trying to use this code but the class casting is failing.


    mWindow = (TapDetectingWindow *)[[UIApplication sharedApplication].windows objectAtIndex:0];

    After doing this cast mWindow class is UIWindow and then the app crashes and I get this error:


    *** -[UIWindow setViewToObserve:]: unrecognized selector sent to instance 0x4a26b90

    Any Idea why?

    Thank you!

    Reply
    • 34. nvhieu  |  November 24, 2010 at 7:49 am

      I have the same question.

      Any idea? Thanks!

      Reply
      • 35. Alan Moore  |  November 24, 2010 at 8:21 am

        See Jeff Tang’s answer to me below — that should solve the problem — # 38.

  • 36. Alan Moore  |  July 12, 2010 at 2:19 am

    Is it a stupid question to ask how to implement a swipe using this? Can anybody give me a sample of this code which breaks down touchesbegan, touchesMoved, touchesEnded events?

    Reply
  • 37. Jeff Tang  |  July 21, 2010 at 12:07 am

    Thanks Mithin. It works very well.

    Alan, I had the exact same question, so it can’t be a stupid question:) And it’s actually quite easy to break down:

    1) add these after userDidTapWebView in TapDetectingWindow.h:
    – (void)userTouchBegan:(id)tapPoint;
    – (void)userTouchMoved:(id)tapPoint;
    – (void)userTouchEnded:(id)tapPoint;

    2) in sendEvent of .m, comment out
    if (touch.phase != UITouchPhaseEnded)
    return;
    and add these:
    if (touch.phase == UITouchPhaseBegan)
    [self performSelector:@selector(forwardTouchBegan:) withObject:pointArray afterDelay:0.5];
    else if (touch.phase == UITouchPhaseMoved)
    [self performSelector:@selector(forwardTouchMoved:) withObject:pointArray afterDelay:0.5];
    else if (touch.phase == UITouchPhaseEnded)
    [self performSelector:@selector(forwardTouchEnded:) withObject:pointArray afterDelay:0.5];

    Also add these:

    – (void)forwardTouchBegan:(id)touch {
    [controllerThatObserves userTouchBegan:touch];
    }
    – (void)forwardTouchMoved:(id)touch {
    [controllerThatObserves userTouchMoved:touch];
    }
    – (void)forwardTouchEnded:(id)touch {
    [controllerThatObserves userTouchEnded:touch];
    }

    Then implement the same way as you would do with touchesBegan, touchesMoved, touchesEnded:

    – (void)userTouchBegan:(id)tapPoint;
    – (void)userTouchMoved:(id)tapPoint;
    – (void)userTouchEnded:(id)tapPoint;
    in the viewcontroller that has webview.

    I just went thru this and tested and it works just like other non-webview touches.

    Jeff

    Reply
  • 38. Alan Moore  |  July 21, 2010 at 6:57 pm

    Jeff, thanks for your helpful reply. Unfortunately, it seems that this still doesn’t work on iOS4. I ended up using the Gesture Recognizers and that does work on iPad and iOS4 so I’m happy!

    Reply
    • 39. Jeff Tang  |  July 21, 2010 at 11:38 pm

      Hi Alan,
      My Deployment Target was set to iPhone OS 3 (Base SDK 4.0) yesterday and I just changed the Deployment Target to iPhone OS 4.0 and tested my app again on iPhone 3GS running iOS4 and it works fine. So I think either sendEvent returns too early or it’s related to iPhone 4 – are you using this or iPhone 3 running iOS4?

      Then I tested it (for the first time) on iPad and just like you said it didn’t work – I did some debugging and after moving the code from controller’s viewDidLoad to AppDelegate’s (put after
      [window addSubview:viewController.view];
      [window makeKeyAndVisible];
      ) – it works on iPad too:

      viewController.mWindow = (TapDetectingWindow *)[UIApplication sharedApplication].keyWindow;
      viewController.mWindow.viewToObserve = viewController.wbvText;
      viewController.mWindow.controllerThatObserves = viewController;

      But I’m happy that you got it work with Gesture Recognizers. And thanks for the info – I haven’t used it before but from Apple’s doc it seems to be the better way. However it’s only available since iOS 3.2. So if one wants to support 3.0 or 3.1.x version, the technique here should be better.

      Regards, Jeff

      Reply
      • 40. Alan Moore  |  July 22, 2010 at 2:14 am

        Hi Jeff,

        You’re right, it was the iPad that broke it… I forgot which one it was!

        Too bad I didn’t get your fix sooner, but I’m happy with the Gesture Recognizer solution. I honestly don’t know how to build anything to support pre 3.2 on the new XCode anyway… I get the impression Apple would just as soon you didn’t!

        Thanks,
        Alan

  • 41. GammaPoint  |  July 30, 2010 at 4:41 pm

    Good tutorial, Mithin.

    Very useful in recreating browser like features, go backward on a page, etc.

    Reply
  • 42. djk  |  August 10, 2010 at 8:39 pm

    Thanks for this excellent tut.

    And how do you detect which html element the user taped?

    Reply
  • 43. CharlieC  |  September 1, 2010 at 8:05 am

    Can I use swipe with UIWebView? There is scroll in UIWebView already. No any conflict.

    Reply
  • 44. JasonM  |  September 14, 2010 at 9:55 am

    Your code worked great and is easier than any other method I could find or think of. Thanks for sharing!

    Reply
  • 45. Bill Atkinson  |  September 17, 2010 at 2:12 am

    How would you allow links to be followed without firing the tap detection code?

    Right now, I’m hiding and showing a toolbar when you tap in the webview, but if you tap on a link, that follows the link and shows or hides the toolbar.

    I’d like to not show or hide the toolbar if the user tapped on a link, but I’m not sure how to do that.

    Reply
  • 46. Bill Atkinson  |  September 20, 2010 at 6:48 am

    To answer my own question, I added a method to TapDetectingWindow.m:


    - (void)ignoreNextEvent
    {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    }

    Then in my webview’s shouldStartLoadWithRequest I call ignoreNextEvent, and all is well. Links get followed, the toolbar stays hidden.

    Reply
  • 47. Bob  |  September 22, 2010 at 2:31 pm

    You, sir, saved my life!
    I have been messing around with this for over a week now, and I was getting pretty tired of it. This method seems to work great!
    Again: thanks! I will sacrifice a small goat to honor you

    Reply
  • 48. Tommy Herbert  |  September 24, 2010 at 12:00 am

    Worked for me too. Thanks very much.

    Reply
  • 49. andy  |  October 14, 2010 at 2:55 pm

    It doesn’t work for me. I use base sdk as 4.0 and deployment target as 3.0 . It crashes at the point

    [UIWindow setViewToObserve:]: unrecognized selector sent to instance 0x733f4e0

    When debugging it stops at
    mWindow.viewToObserve = mHtmlViewer;

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

    Reply
    • 50. Ko Ko  |  October 15, 2010 at 6:14 am

      Same here. It doesn’t work. Seems like Class Casting is a problem here.

      Reply
    • 51. Alan Moore  |  October 15, 2010 at 7:39 am

      There might be better solutions, what I did was use UIGestureRecognizer interface for iOS 4 and this interface for pre-iOS4 deployment — this can be determined by checking to see if UIGestureRecognizer selector is available. I can dig up the code if you like…

      Reply
  • 52. Alan Moore  |  October 15, 2010 at 7:41 am

    Sorry, that should be pre-iOS 3.2!

    Reply
  • 53. Ko Ko  |  October 15, 2010 at 9:29 am

    Finally, it worked. I just needed to change the type of the MainWindow to the Subclass. Previously it was UIWindow. Thanks for the great tips, anyway!

    Reply
    • 54. andy  |  October 15, 2010 at 10:02 am

      @ Ko Ko
      can you please explain what you changed to make it work..

      Reply
  • 55. tana  |  October 20, 2010 at 4:06 pm

    Maybe, Make sure that in your MainWindow.XIB, the class reference of your window is set to Tap Detecting Window.
    (see [Tools]-[Identity Inspector]-[class] in UIWindow of mainwindow.xib)

    Reply
  • 56. stephan  |  October 25, 2010 at 2:17 pm

    This also works on a MKMapView! You are the man ;-)

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Trackback this post  |  Subscribe to the comments via RSS Feed


TinyTweets

  • RT @prasannavishy: Mahavira the last of the Jain Tirthankaras, attained Nirvana or Liberation on this day at Pavapuri on a Deepavali dawn 1 day ago
  • RT @Moneylifers: +1 MT @nooreshtech: One shd spend more money & time on appreciating assets (equities,RE,FD, etc) than on depreciating fads… 1 week ago
  • RT @GappistanRadio: Nobel Peace Prize announcement ke baad India-Pak should stop fighting... Yaar tumne apne ghar ka bulb badla LED ke liy… 1 week ago

Feeds


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: