devguy
devguy

Reputation: 2386

(iPhone) How to handle touches on a UITextView?

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?

Upvotes: 24

Views: 30845

Answers (7)

Antony Thomas
Antony Thomas

Reputation: 3686

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
}

Upvotes: 0

wagashi
wagashi

Reputation: 894

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

Upvotes: 0

nacho4d
nacho4d

Reputation: 45118

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));
}

Upvotes: 7

REALFREE
REALFREE

Reputation: 4396

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

Upvotes: 11

CharlieMezak
CharlieMezak

Reputation: 5999

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!

Upvotes: 1

Ignacio Enriquez
Ignacio Enriquez

Reputation:

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");
}

Upvotes: 14

dizy
dizy

Reputation: 7959

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;

Upvotes: 2

Related Questions