9

I have a UIWebView with a navigation bar and toolbar that I want to auto hide. I'm doing that already, but I want to show them when the user taps on the UIWebView.

My problem is that the UIWebView captures all the touches events, and I cannot intercept the one I need.

Is there any workaround for this?

pgb
  • 24,813
  • 12
  • 83
  • 113

7 Answers7

8

Taken from a blog entry (doesn't load, but thankfully it's Google Cache'd) by Mithin Kumar.

(Mithin, I hope you don't mind me reposting it here; if you do, please let me know in the comments and I will remove it.)

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.

Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
brian.clear
  • 5,277
  • 2
  • 41
  • 62
  • 1
    I've tried this a bunch of ways (javascript, transparent overlay views, method swishing of internal touch selectors) but this (that mithin.in link) seems to be the best, non-private way to do it. – Jason Jan 12 '10 at 22:15
  • That site has been shutdown - you don't happen to have the code lying around from it do you? – Lee May 08 '13 at 15:39
  • Huge thanks for recovering this piece of knowledge. For those having problems or crashes: http://stackoverflow.com/a/19549249/423171 – cprcrack Oct 23 '13 at 18:11
  • Is there a swift implementation of this? – Babiker Oct 13 '17 at 23:43
8

You can very simply use a UITapGestureRecognizer to detect tap gestures on a UIWebView. You must implement a UIGestureRecognizerDelegate method to allow the simultaneous recognition however.

- (void)viewDidLoad{
    [super viewDidLoad];

    UITapGestureRecognizer *targetGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    targetGesture.numberOfTapsRequired = 2;
    targetGesture.delegate = self;
    [self.webView addGestureRecognizer:targetGesture];

}

// called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
// return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
//
// note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    NSLog(@"%@", otherGestureRecognizer);
    //if you would like to manipulate the otherGestureRecognizer here is an example of how to cancel and disable it
    if([otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]){

        UITapGestureRecognizer *tapRecognizer = (UITapGestureRecognizer*)otherGestureRecognizer;
        if(tapRecognizer.numberOfTapsRequired == 2 && tapRecognizer.numberOfTouchesRequired == 1){

            //this disalbes and cancels all other singleTouchDoubleTap recognizers
            // default is YES. disabled gesture recognizers will not receive touches. when changed to NO the gesture recognizer will be cancelled if it's currently recognizing a gesture
            otherGestureRecognizer.enabled = NO;

        }

    }

    return YES;

}

-(void)handleTap:(id)sender{



}
Brody Robertson
  • 8,506
  • 2
  • 47
  • 42
  • I suspect that it could not work in ealier versions of iOS and because of this the other answer is accepted. Could somebody verify it ? (The question is so old, I think back then the Gesture Recognizers could be not available) – Paweł Brewczynski Jan 02 '15 at 13:04
3

I had several issues trying to use brian.clear's answer (copied from a extinct post by Mithin Kumar) in my iOS 5-7, universal, storyboard-based, totally ARC project so I had to make several changes. I also improved some things and made it easier to understand (at least for me). If you are having trouble trying to use that answer from 2009 maybe you should try my update. Detailed instructions:

1. Create a new TapDetectingWindow class

TapDetectingWindow.h

//  Created by Cristian Perez <cpr@cpr.name>
//  Based on https://stackoverflow.com/a/1859883/423171

#import <UIKit/UIKit.h>

@protocol TapDetectingWindowDelegate

- (void)userDidTapView:(CGPoint)tapPoint;

@end

@interface TapDetectingWindow : UIWindow

@property (nonatomic) UIView *tapDetectingView;
@property (nonatomic) id <TapDetectingWindowDelegate> tapDetectedDelegate;

@end

TapDetectingWindow.m

//  Created by Cristian Perez <cpr@cpr.name>
//  Based on https://stackoverflow.com/a/1859883/423171

#import "TapDetectingWindow.h"

@implementation TapDetectingWindow

@synthesize tapDetectingView;
@synthesize tapDetectedDelegate;

- (id)initWithFrame:(CGRect)frame
{
    return [super initWithFrame:frame];
}

- (void)sendEvent:(UIEvent *)event
{
    [super sendEvent:event];
    if (tapDetectingView == nil || tapDetectedDelegate == nil)
    {
        return;
    }
    NSSet *touches = [event allTouches];
    if (touches.count != 1)
    {
        return;
    }
    UITouch *touch = touches.anyObject;
    if (touch.phase != UITouchPhaseEnded)
    {
        return;
    }
    if (touch.view != nil && ![touch.view isDescendantOfView:tapDetectingView])
    {
        return;
    }
    CGPoint tapPoint = [touch locationInView:tapDetectingView];
    NSString *tapPointStr = NSStringFromCGPoint(tapPoint);
    if (touch.tapCount == 1)
    {
        [self performSelector:@selector(notifyTap:) withObject:tapPointStr afterDelay:0.4];
        // Make the afterDelay value bigger in order to have more chances of detecting a double tap and thus being able to cancel the single tap event, or make it smaller if you don't care about double taps and want to get the tap event as soon as possible
    }
    else if (touch.tapCount > 1)
    {
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(notifyTap:) object:tapPointStr];
    }
}

- (void)notifyTap:(NSString *)tapPointStr
{
    CGPoint tapPoint = CGPointFromString(tapPointStr);
    [tapDetectedDelegate userDidTapView:tapPoint];
}

@end

2. Check you have a window declared in your app delegate

You should have something like this in YourAppDelegate.h. Don't change the name of the property!

@interface YourAppDelegate : UIResponder <UIApplicationDelegate>
{
    // ...
}

// ...

// The app delegate must implement the window property if it wants to use a main storyboard file
@property (nonatomic) UIWindow *window;

@end

3. Override the window property of your app delegate

Just like this, which should be in YourAppDelegate.m. Again, don't change the name of the method!

// Replace the default UIWindow property with a TapDetectingWindow
- (TapDetectingWindow *)window
{
    static TapDetectingWindow *tapDetectingWindow = nil;
    if (!tapDetectingWindow)
    {
        tapDetectingWindow = [[TapDetectingWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    }
    return tapDetectingWindow;
}

4. Set the delegate protocol in your view controller

Be sure to adopt the tap handling protocol in your MainViewController.h

#import "TapDetectingWindow.h"

@interface MainViewController : UIViewController <TapDetectingWindowDelegate, ...>
{
    // ...
}

// ...

@end

5. Set up the tap detecting window and tap event handler

Specify your webview (actually any UIView should work) and tap event handler in your view controller's (void)viewDidLoad method

- (void)viewDidLoad
{
    // ...

    // Allow tap detection in webview
    TapDetectingWindow *tapDetectingWindow = (TapDetectingWindow*)[(YouTubeRPAppDelegate*)[[UIApplication sharedApplication] delegate] window];
    tapDetectingWindow.tapDetectingView = self.webView; // Your UIWebView
    tapDetectingWindow.tapDetectedDelegate = self;
}

6. Handle the tap event as you wish

Just implement the userDidTapView method in your view controller

- (void)userDidTapView:(CGPoint)tapPoint
{
    NSLog(@"Tap detected in webview at %@", NSStringFromCGPoint(tapPoint));
}
Community
  • 1
  • 1
cprcrack
  • 17,118
  • 7
  • 88
  • 91
1

Most of the approaches deal with a complicated pair of UIView and UIWebView subclasses and overrode -touchesBegan:withEvent: etc. methods.

This JavaScript-based approach intercepts touches on the web DOM itself, and it seems like a clever way to sidestep the more complex process. I haven't tried it myself, but I'm curious to know the results, if you give it a shot.

Community
  • 1
  • 1
Alex Reynolds
  • 95,983
  • 54
  • 240
  • 345
1

I'm not sure of the exact implementation details but you need to subclass UIWindow and override sendEvent:. Then you can capture tap events and handle them accordingly, before they get down to the web view. Hope this points you in the right direction!

Michael Waterfall
  • 20,497
  • 27
  • 111
  • 168
1

Create a UIView sub class containing the whole UIWebView. Then the event hitTest will be fired on touching the webview. Note: Don't put anything to the container except the webView.

@interface myWebViewContainer : UIView
... ...
@end

Then override the hitTest event:

-(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"Hit test %@", [event description]);
    UIView * returnView = [super hitTest:point withEvent:event];
    [delegate userDidTapWebView];// notify the delegate that UIWebView is being touched by some naughty user :P


    return  returnView;
}
Sayeed S. Alam
  • 445
  • 2
  • 6
  • This kind of works depending on what you need, but the method is called also for pan gestures (scrolling) and it's called twice per touch. Definitely not ideal. – cprcrack Oct 23 '13 at 14:46
0

A much simpler approach since iOS 3.2 is simply to use a UITapGestureRecognizer.

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    UITapGestureRecognizer* tapGestureRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self 
                                                                                        action:@selector(userDidTapWebView)] autorelease];
    [self.pdfWebView addGestureRecognizer:tapGestureRecognizer];

}

- (void)userDidTapWebView {
    NSLog(@"TAP");
}
DonnaLea
  • 8,643
  • 4
  • 32
  • 32
  • This doesn't work with web views because they contain their own, higher priority gesture recognizers. – Jeff Oct 04 '13 at 13:26
  • This approach can work, though you need to implement UIGestureRecognizerDelegate methods. See my answer. – Brody Robertson Mar 20 '14 at 16:39
  • @BrodyRobertson thanks for filling in the blanks! I knew I had implemented it this way in a project and it was working, just hadn't had the time to go back and check what was missing. – DonnaLea Mar 20 '14 at 20:10