24

I'm trying to handle touches on a iPhone's UITextView. I successfully managed to handle taps and other touch events by creating a subclass of UIImageViews for example and implementing the touchesBegan method...however that doesn't work with the UITextView apparently :(

The UITextView has user interaction and multi touch enabled, just to be sure...no no joy. Anyone managed to handle this?

devguy
  • 2,336
  • 5
  • 27
  • 31

7 Answers7

14

UITextView (subclass of UIScrollView) includes a lot of event processing. It handles copy and paste and data detectors. That said, it is probably a bug that it does not pass unhandled events on.

There is a simple solution: you can subclass UITextView and impement your own touchesEnded (and other event handling messages) in your own versions, you should call[super touchesBegan:touches withEvent:event]; inside every touch handling method.

#import "MyTextView.h"  //MyTextView:UITextView
@implementation MyTextView

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"touchesBegan");
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
        [super touchesBegan:touches withEvent:event];
    NSLog(@"touchesMoved");
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"****touchesEnded");
    [self.nextResponder touchesEnded: touches withEvent:event]; 
    NSLog(@"****touchesEnded");
    [super touchesEnded:touches withEvent:event];
    NSLog(@"****touchesEnded");
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{
[super touches... etc]; 
NSLog(@"touchesCancelled");
}
Rog
  • 17,070
  • 9
  • 50
  • 73
  • 1
    Can you elaborate on this? Does your touchesEnded method ever get called? – Ben Scheirman Jul 08 '09 at 15:55
  • 1
    edited to make clearer. Ben - the subclasses touchesEnded gets called and then effectively duplicates the event - once to the superclass (UITextView) which swallows the event and once to the next responder. – Rog Jul 15 '09 at 23:48
  • 2
    This still looks odd to me. Why are you calling the touchesBegan super on the touchesMoved? Wouldn't this mean that you would call touchesBegan over and over again and touchesMoved would never be handled by super? – memmons Oct 20 '10 at 02:38
  • hi..i try to do this things, but no one touches method is called in my app, can somebody tell me why? – R. Dewi Mar 14 '11 at 10:29
  • same here. none touch method get detected & triggered. – Raptor Oct 10 '11 at 06:36
  • @Risma Shivan, The reason methods are called is because UITextView has UIGestureRecognizer and those are probably catching the touches and not calling them. There are situation when they are called thought, for example when long pressing or when scrolling after long pressing it. I am trying to do the same thing w/o interfering with default tap behavior but no luck yet. – nacho4d Nov 18 '11 at 11:26
  • Read my comment on @REALFREE answer – nacho4d Nov 21 '11 at 08:48
  • 1
    Turn off delayContentTouches. The default tap behavior will still fire regardless. –  Oct 06 '12 at 01:01
11

If you want to handle single/double/triple tap on UITextView, you can delegate UIGestureRecongnizer and add gesture recognizers on your textview.

Heres sameple code (in viewDidLoad):

UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap)];

//modify this number to recognizer number of tap
[singleTap setNumberOfTapsRequired:1];
[self.textView addGestureRecognizer:singleTap];
[singleTap release];

and

-(void)handleSingleTap{
   //handle tap in here 
   NSLog(@"Single tap on view");
}

Hope this help :D

REALFREE
  • 4,378
  • 7
  • 40
  • 73
  • The function does not fire even I tap / touch / drag on UITextView. You sure it works? – Raptor Oct 10 '11 at 06:36
  • @ShivanRaptor did you add UIGestureRecognizerDelegate in your header file? Cuz it's what exactly I did in my app and it worked – REALFREE Oct 16 '11 at 22:28
  • okay, I will double check it. Does the delegate have any required functions needed to be implemented? – Raptor Oct 17 '11 at 02:10
  • The `UITextView` has its own gesture recognizers, specially there is one (class=`UITextTapRecognizer`) which handles single taps (action=`oneFingerTap:`). So they are colliding and this `UITextTapRecognizer` is taking precedence over your `UITapGestureRecognizer`. I've tried removing the `UITextTapRecognizer` before adding my own `UITapGestureRecognizer` but the `UITextView` will recreate it again and my handler is never called. Is like Apple doesn't want us to customize UITextView's single tap behavior. – nacho4d Nov 21 '11 at 08:46
  • @nacho4d I'd appreciate your thoughts. But I was able to handle single tap on TextView (single tap to change context of text in textview) with UIGestureRecognizer tho. – REALFREE Nov 21 '11 at 22:36
  • Weird. Did you try it in iOS5? When did you add the recognized to the view? – nacho4d Nov 22 '11 at 00:54
7

Better solution (Without swizzling anything or using any Private API :D )

As explained below, adding new UITapGestureRecognizers to the textview does not have the expected results, handler methods are never called. That is because the UITextView has some tap gesture recognizer setup already and I think their delegate does not allow my gesture recognizer to work properly and changing their delegate could lead to even worse results, I believe.

Luckily the UITextView has the gesture recognizer I want already setup, the problem is that it changes according to the state of the view (i.e.: set of gesture recognizers are different when inputing Japanese than when inputing English and also when not being in editing mode). I solved this by overriding these in a subclass of UITextView:

- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
{
    [super addGestureRecognizer:gestureRecognizer];
    // Check the new gesture recognizer is the same kind as the one we want to implement
    // Note:
    // This works because `UITextTapRecognizer` is a subclass of `UITapGestureRecognizer`
    // and the text view has some `UITextTapRecognizer` added :)
    if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
        UITapGestureRecognizer *tgr = (UITapGestureRecognizer *)gestureRecognizer;
        if ([tgr numberOfTapsRequired] == 1 &&
            [tgr numberOfTouchesRequired] == 1) {
            // If found then add self to its targets/actions
            [tgr addTarget:self action:@selector(_handleOneFingerTap:)];
        }
    }
}
- (void)removeGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
{
    // Check the new gesture recognizer is the same kind as the one we want to implement
    // Read above note
    if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
        UITapGestureRecognizer *tgr = (UITapGestureRecognizer *)gestureRecognizer;
        if ([tgr numberOfTapsRequired] == 1 &&
            [tgr numberOfTouchesRequired] == 1) {
            // If found then remove self from its targets/actions
            [tgr removeTarget:self action:@selector(_handleOneFingerTap:)];
        }
    }
    [super removeGestureRecognizer:gestureRecognizer];
}

- (void)_handleOneFingerTap:(UITapGestureRecognizer *)tgr
{
    NSDictionary *userInfo = [NSDictionary dictionaryWithObject:tgr forKey:@"UITapGestureRecognizer"];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"TextViewOneFingerTapNotification" object:self userInfo:userInfo];
    // Or I could have handled the action here directly ...
}

By doing this way, no matter when the textview changes its gesture recognizers, we will always catch the tap gesture recognizer we want → Hence, our handler method will be called accordingly :)

Conclusion: If you want to add a gesture recognizers to the UITextView, you have to check the text view does not have it already.

  • If it does not have it, just do the regular way. (Create your gesture recognizer, set it up, and add it to the text view) and you are done!.
  • If it does have it, then you probably need to do something similar as above.



Old Answer

I came up with this answer by swizzling a private method because previous answers have cons and they don't work as expected. Here, rather than modifying the tapping behavior of the UITextView, I just intercept the called method and then call the original method.

Further Explanation

UITextView has a bunch of specialized UIGestureRecognizers, each of these has a target and a action but their target is not the UITextView itself, it's an object of the forward class UITextInteractionAssistant. (This assistant is a @package ivar of UITextView but is forward definition is in the public header: UITextField.h).

UITextTapRecognizer recognizes taps and calls oneFingerTap: on the UITextInteractionAssistant so we want to intercept that call :)

#import <objc/runtime.h>

// Prototype and declaration of method that is going be swizzled
// When called: self and sender are supposed to be UITextInteractionAssistant and UITextTapRecognizer objects respectively
void proxy_oneFingerTap(id self, SEL _cmd, id sender);
void proxy_oneFingerTap(id self, SEL _cmd, id sender){ 
    [[NSNotificationCenter defaultCenter] postNotificationName:@"TextViewOneFinderTap" object:self userInfo:nil];
    if ([self respondsToSelector:@selector(proxy_oneFingerTap:)]) {
        [self performSelector:@selector(proxy_oneFingerTap:) withObject:sender];
    }
}

...
// subclass of UITextView
// Add above method and swizzle it with.
- (void)doTrickForCatchingTaps
{
    Class class = [UITextInteractionAssistant class]; // or below line to avoid ugly warnings
    //Class class = NSClassFromString(@"UITextInteractionAssistant");
    SEL new_selector = @selector(proxy_oneFingerTap:);
    SEL orig_selector = @selector(oneFingerTap:);

    // Add method dynamically because UITextInteractionAssistant is a private class
    BOOL success = class_addMethod(class, new_selector, (IMP)proxy_oneFingerTap, "v@:@");
    if (success) {
        Method originalMethod = class_getInstanceMethod(class, orig_selector);
        Method newMethod = class_getInstanceMethod(class, new_selector);
        if ((originalMethod != nil) && (newMethod != nil)){
            method_exchangeImplementations(originalMethod, newMethod); // Method swizzle
        }
    }
}

//... And in the UIViewController, let's say

[textView doTrickForCatchingTaps];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textViewWasTapped:) name:@"TextViewOneFinderTap" object:nil];

- (void)textViewWasTapped:(NSNotification *)noti{
    NSLog(@"%@", NSStringFromSelector:@selector(_cmd));
}
Community
  • 1
  • 1
nacho4d
  • 43,720
  • 45
  • 157
  • 240
  • I've updated my answer :) . For swipe gestures just do as usual: `r = [[UISwipeGestureRecognizer alloc] init... ];` `[textView addGestureRecognizer:r];` ... I hope it helps – nacho4d Feb 27 '12 at 04:22
  • New answer works great! I was even able to use that technique to change default editing from single tap to double tap without having to add a gesture recognizer. – Arie Litovsky Aug 23 '12 at 20:56
2

You need to assign the UITextView instance.delegate = self (assuming you want to take care of the events in the same controller)

And make sure to implement the UITextViewDelegate protocol in the interface... ex:

@interface myController : UIViewController <UITextViewDelegate>{
}

Then you can implement any of the following


- (BOOL)textViewShouldBeginEditing:(UITextView *)textView;
- (BOOL)textViewShouldEndEditing:(UITextView *)textView;

- (void)textViewDidBeginEditing:(UITextView *)textView;
- (void)textViewDidEndEditing:(UITextView *)textView;

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;
- (void)textViewDidChange:(UITextView *)textView;

- (void)textViewDidChangeSelection:(UITextView *)textView;

dizy
  • 7,951
  • 10
  • 53
  • 54
  • 2
    This answer does not address the issue which is that a normal UITextView swallows touch events. – memmons Oct 20 '10 at 02:30
  • I followed that advice. The good news is that it worked fine. The bad news is that this caused some EXC_BAD_ACCESS which did cost me some days to nail it down. I am using ARC and the EXC_BAD_ACCESS did not appear whitn my own code being executed. In some cases it did not come up for minutes, in most cases the app did not crash at all. I am not saying that this solution is wrong. The error may have been within my implementation of it. But if you want to follow that advice then try to stress-test the function and have an eye on sudden crashes that may xome way later. – Hermann Klecker Feb 09 '12 at 21:45
1

I'm using a textview as a subview of a larger view. I need the user to be able to scroll the textview, but not edit it. I want to detect a single tap on the textview's superview, including on the textview itself.

Of course, I ran into the problem that the textview swallows up the touches that begin on it. Disabling user interaction would fix this, but then the user won't be able to scroll the textview.

My solution was to make the textview editable and use the textview's shouldBeginEditing delegate method to detect a tap in the textview. I simply return NO, thereby preventing editing, but now I know that the textview (and thus the superview) has been tapped. Between this method and the superview's touchesEnded method I have what I need.

I know that this won't work for people who want to get access to the actual touches, but if all you want to do is detect a tap, this approach works!

CharlieMezak
  • 5,999
  • 1
  • 38
  • 54
  • this answer is close to solution, but it didn't work during dragging, which is the usual action done by text view users. – Raptor Oct 10 '11 at 06:34
  • In my case I have a editable textView and I cannot catch taps once the textview is the first responder. I've tried with with `textViewDidChangeSelection:` however that is called when using the bluetooth keyboard too and gives me false positives. – nacho4d Nov 21 '11 at 08:57
0

You can also send a Touch Down event. Wire-up this event through the Interface Builder.

enter image description here

Then add code in your event handler

- (IBAction)onAppIDTap:(id)sender {
    //Your code
}
Antony Thomas
  • 3,576
  • 2
  • 34
  • 40
0

How about make a UIScrollView and [scrollView addSubview: textview] which makes it possible to scroll textview?

Hiren
  • 12,720
  • 7
  • 52
  • 72
wagashi
  • 894
  • 3
  • 15
  • 39