This is an annoying problem that related to my recent project. The goal is simple:
handle single touch event in
UIWebViewand let it handle other touch events as defaults.
However this is really hard to achive…
I found this post:DETECTING TAPS AND EVENTS ON UIWEBVIEW – THE RIGHT
He handles touch event via
- (void) sendEvent:(UIEvent*) event instead of
UIGesutreRecognizer. His approach is more general and less hacky than mine. Have
a look at it also!
First we need to review cocoa event responder chain, aka event delivery paths. According to apple Event Handling Guide for iOS:
The window object uses hit-testing and the responder chain to find the view to receive the touch event. In hit-testing, a window calls
hitTest:withEvent:on the top-most view of the view hierarchy; this method proceeds by recursively calling
pointInside:withEvent:on each view in the view hierarchy that returns
YES, proceeding down the hierarchy until it finds the subview within whose bounds the touch took place. That view becomes the hit-test view.
For example, we have a single view application with a
UIWebView in it. The
touch event on UIWebView will be delivered in following order:
- Container view
- Custom view controller
- Singleton application delegate
Even if we added a
UIGestureRecognizer on top of
UIWebVIew in Interface
Builder, it would not be on the event deliver path.
The standard way to add touch event listener goes as follows:
- alloc and init
UITapGestureRecognizer, setup tap count and delegate
- attach the recognizer to container view
<UIGestureRecognizerDelegate>protocol and return
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGesture Recognizer:(UIGestureRecognizer *)otherGestureRecognizer
1 2 3 4 5 6 7 8 9 10 11 12 13 14
The last step we let cocoa event system pass the event through every possible
handler in the responder chain. Thus both
UITapGestureRecognizer will handle the touch event. Now we only have to make
sure it handles single tap event, not long touch, double tap, or drag event.
At first, I tried to implement my event handler with cocoa target action
mechanism, and use
requireGestureRecognizerToFail to tell cocoa that I don’t
want it to trigger double tap nor triple tap events.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
However, it just don’t work!! It always triggered the
when I tapped twice or more. I need to find another way to fix the problem.
There are two useful instance method in
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touchreturn
YES(the default) to allow the gesture recognizer to examine the touch object, NO to prevent the gesture recognizer from seeing this touch object. This method is called before
touchesBegan:withEvent:is called on the gesture recognizer for a new touch.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizerreturn
YESto allow both gestureRecognizer and otherGestureRecognizer to recognize their gestures simultaneously. The default implementation returns
NO—-no two gestures can be recognized simultaneously. This method would be called frequently during long touch, drag, double tapped or any other kinds of events.
(UITouch *) touch in first method has property
tapCount, which is what
we want. Sadly this method is called immediately as user tap on the device.
That is, if we log out the message like this:
1 2 3
And tap twice, it will print out
gestureRecognizer shouldReceiveTouch: tapCount = 1 gestureRecognizer shouldReceiveTouch: tapCount = 2
gestureRecognizer shouldReceiveTouch: will be triggered every time
you touch the screen.
The way to differ single tap and others is to use a
NSTimer that triggers the
handler later and cancel the timer if
tapCount >= 2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
The timer can solve multiple taps issue, but it cannot recognize long touch. The
long touch cannot be recognized in
method, but can be detected in
shouldRecognizeSimultaneouslyWithGestureRecognizer method. If the touch went
too long, the
gestureRecognizer.state would be
UIGestureRecognizerStateFailed. Thus I record the state and check it when the
There is one more thing that I should mentioned:
shouldRecognizeSimultaneouslyWithGestureRecognizer may not be called as
frequently as we might expected. If we tap once, wait, and long touch the
handleSingleTap may still recognize the event to be a quick tap
self.gestureState may still be
the time. The way to prevent this result is to reset
every time we start the timer.
You can find the source code on my github page.