0

I think I've encountered a bug in UITextView in iOS 7, but I haven't been able to find any reference to it or discussion, so trying to confirm here that I'm not missing something:

The Bug

I have a UITextView setup as follows:

UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(20, 100, 280, 140)];
textView.editable = NO;
textView.dataDetectorTypes = UIDataDetectorTypeLink;
textView.backgroundColor = [UIColor lightGrayColor];
textView.delegate = self;
textView.text = [NSString stringWithFormat:@"This is a UITextView in a UIViewController. It has editable %d, selectable %d, dataDectorTypes %d (UIDataDetectorTypeLink), and a link! http://www.google.com.\n\nIf you click the link, it works normally. If you press, then drag your finger away to cancel the touch, the highlight goes away but the action still fires. Shouldn't the action not fire?", textView.editable, textView.selectable, textView.dataDetectorTypes];
[self.view addSubview:textView];

The textview text contains a link, which if you click works as expected: it calls back to the delegate, which just pops up an alert:

- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange{

    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"You clicked the link!" message:@"You clicked the link. Maybe you tried to move away to cancel the touch, but you clicked it anyway." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [alertView show];
    return NO;
}

The problem is if you press the link, then try to drag away to cancel the touch: the link's highlight state goes away but the delegate fires as soon as your finger leaves the screen.

I haven't been able to find a way to tell the textview not to fire when it should have cancelled, or to detect that it should have cancelled.

I'm convinced this is a bug because the visual press/cancel state and the action-firing don't match.

1 Answers1

0

Work-around (horrible horrible invasive work-around)

I got around this by subclassing UIApplication to track all screen touches. By holding on to the last touch location, I can, in the delegate, map the location to the link to determine whether or not it should be considered valid:

MyCustomApplication.m

@implementation MyCustomApplication

static CGPoint lastTouchVar;

- (void)sendEvent:(UIEvent *)event {
    if (event.type == UIEventTypeTouches) {
        lastTouchVar = [((UITouch*)event.allTouches.anyObject) locationInView:[[[UIApplication sharedApplication] delegate] window]];
    }
    [super sendEvent:event];
}

+ (CGPoint)lastTouch {
    return lastTouchVar;
}

@end

(use it in main.m)

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, @"MyCustomApplication", NSStringFromClass([MyAppDelegate class]));
    }
}

And map it...

- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange {

    CGPoint lastTouchWRTTextView = [[[[UIApplication sharedApplication] delegate] window] convertPoint:[MyCustomApplication lastTouch] toView:textView];

    lastTouchWRTTextView.x -= textView.textContainerInset.left;
    lastTouchWRTTextView.y -= textView.textContainerInset.top;

    if (CGRectContainsPoint(CGRectMake(0, 0, textView.frame.size.width, textView.frame.size.height), lastTouchWRTTextView)) {

        int characterIndexForPoint = [textView.textContainer.layoutManager characterIndexForPoint:lastTouchWRTTextView inTextContainer:textView.textContainer fractionOfDistanceBetweenInsertionPoints:nil];
        if (NSLocationInRange(characterIndexForPoint, characterRange)) {
            // It's a real tap! Do something!
        }
    }
    return NO;
}