Ben Scheirman

These fickle bits...

Menu

Detecting a Tap on a UITextView

I’ve been working on a little side project (that I’ll announce soon).  It is an iPhone application that I intend to sell.  Being a .NET guy, I’m certainly a bit lost when it comes to troubleshooting problems with Objective-C and Cocoa.  Hopefully this post will help someone else out that ran into a similar issue.

In Cocoa, any class that derives from UIResponder (which means UIView and all of it’s subclasses) can get touch events by implementing these 4 optional methods:

1
2
3
4
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;

With this raw data you can construct any number of touch schemes, such as knowing when to scroll and when to select something, as is the case with UITableView.

UIControl derivatives (such as UIButton) get a simpler abstraction:  they use that data & translate it to different events such as < code>and `touchUpInside`.  These don’t work for all views, so sometimes to detect a tap you have to roll your own.

In general, you can just look at the touchesEnded event and check the tapCount property to see if the user tapped an element:

1
2
3
4
5
6
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject]; //assume just 1 touch
if(touch.tapCount == 1) {
//single tap occurred
}
}

 

I wanted to use this on a view that contained a full-screen UITextView. 

Unfortunately, as of the iPhone SDK 3.0, UITextView swallows this event completely.  I imagine because of the new feature where you can select text to copy & paste.  You can get touchesBegan, touchesMoved, and touchesCancelled, but no touchesEnded.  (In fact, the touch actually gets cancelled by the UITextView).  Even by subclassing UITextView I could not get this event.

Luckily I found a work-around.  It’s not exactly pretty, but it works for me.

First I needed to subclass UIWindow so that I can get control at the very beginning of every touch.

Next I needed to change my MainViewController.xib, which creates the window, to point to my new class.

Then, in the sendEvent method, I need to check for 3 things:

  • is this touch ending?
  • is this touch hitting my custom text view?
  • is this touch a single tap?

Here’ is the code for my custom window:

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
//CustomWindow.h
@interface CustomWindow : UIWindow {
}
@end
//CustomWindow.m
#import "CustomTextView.h"
@implementation CustomWindow
-(void)sendEvent:(UIEvent *)event {
//loop over touches for this event
for(UITouch *touch in [event allTouches]) {
BOOL touchEnded = (touch.phase == UITouchPhaseEnded);
BOOL isSingleTap = (touch.tapCount == 1);
BOOL isHittingCustomTextView =
(touch.view.class == [CustomTextView class]);
if(touchEnded && isSingleTap && isHittingCustomTextView) {
CustomTextView *tv = (CustomTextView*)touch.view;
[tv tapOccurred:touch withEvent:event];
}
}
<span class="Apple-style-span" style="font-family: Arial; white-space: normal; ">
<div class='bogus-wrapper'><notextile><figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class=''><span class='line'>[super sendEvent:event];</span></code></pre></td></tr></table></div></figure></notextile></div>

</span>}
@end

If all the 3 BOOLs turn out to be true, then I simply call a method I defined on my CustomTextView.

This CustomTextView then calls a similar method on it’s tapDelegate (which I created).  My view controller then adheres to the TapDelegateProtocol (which contains this method) and can now detech taps over a UITextView!

Now my textview becomes fullscreen when you tap it, and another tap brings back the tab bar at the bottom and navigation bar at the top.

Technorati Tags: ,,

Comments