Detecting taps and events on UIWebView – The right way
August 26, 2009
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.
- Copy/Selection stops working on UIWebView
- We need to create a sub-class of UIWebView while Apple says we should not sub-class it.
- 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(forwardTapwithObject: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.
Entry Filed under: Geeky stuff. .
39 Comments Add your own
Leave a Comment
Some HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
Trackback this post | Subscribe to the comments via RSS Feed
1. iPhone SDK Tutorial: Detecting taps and events on UIWebView | Iphone Development Exchange | September 24, 2009 at 7:21 pm
[...] Original Link: Detecting taps and events on UIWebView – The right way [...]
2.
alex | October 19, 2009 at 5:40 pm
What is OverlayedWebWindow?
Can you share source code of the project?
Thanks!
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.
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!
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.
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?
8.
YoungJoon Chun | December 30, 2009 at 9:37 pm
Plz Nevermind:) I was confused before actually trying it.
9. Detecting taps and events on UIWebView « Brainwash Inc. – iPhone/Mobile Development | January 8, 2010 at 4:36 am
[...] Detecting taps and events on UIWebView – The right way « The Spoken Word. [...]
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
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!
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.
13.
spottygrades | January 15, 2010 at 9:42 am
hey thanks for the workaround!
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?
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
16.
Jobs | March 21, 2010 at 9:33 pm
to capture events for webView(var:viewToObserve)
17.
AlexT | March 8, 2010 at 5:52 pm
Thanks for this post. Would you show me how to write the -userDidTapWebView method?
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
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 !
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
21.
Harry | April 20, 2010 at 4:54 am
Massively helpful, thanks- worked fine first time.
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.
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.
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 [...]
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
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.
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.
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.
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.
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 …
32.
Nyx0uf | July 2, 2010 at 8:01 pm
Excellent post ! Thanks for this tip which save me lot of time
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!
34.
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?
35.
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
36.
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!
37.
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
38.
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
39.
GammaPoint | July 30, 2010 at 4:41 pm
Good tutorial, Mithin.
Very useful in recreating browser like features, go backward on a page, etc.