public
Last active

Detect a tap on a URL inside a UITextView. Note: the rs_links method isn't included -- you'll need something that takes text and returns an array of detected links. This gist just demonstrates walking through the UITextView characters.

  • Download Gist
gistfile1.m
Objective-C
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
@implementation UITextView (RSExtras)
 
 
static BOOL stringCharacterIsAllowedAsPartOfLink(NSString *s) {
 
/*[s length] is assumed to be 0 or 1. s may be nil.
Totally not a strict check.*/
 
if (s == nil || [s length] < 1)
return NO;
 
unichar ch = [s characterAtIndex:0];
if ([[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:ch])
return NO;
return YES;
}
 
 
- (NSString *)rs_potentialLinkAtPoint:(CGPoint)point {
 
/*Grow a string around the tap until hitting a space, cr, lf, or beginning or end of document.*/
 
/*If we don't check for end of document, then you could tap way below end of text, and it would return a link if the last text was a link. This has the unfortunate side effect that you can't tap on the last character of a link if it appears at the end of a document. I can live with shipping that.*/
 
UITextRange *textRange = [self characterRangeAtPoint:point];
UITextPosition *endOfDocumentTextPosition = self.endOfDocument;
if ([textRange.end isEqual:endOfDocumentTextPosition])
return nil;
 
UITextPosition *tapPosition = [self closestPositionToPoint:point];
if (tapPosition == nil)
return nil;
 
NSMutableString *s = [NSMutableString stringWithString:@""];
 
/*Move right*/
 
UITextPosition *textPosition = tapPosition;
 
BOOL isFirstCharacter = YES;
while (true) {
UITextRange *rangeOfCharacter = [self.tokenizer rangeEnclosingPosition:textPosition withGranularity:UITextGranularityCharacter inDirection:UITextWritingDirectionNatural];
NSString *oneCharacter = [self textInRange:rangeOfCharacter];
 
if (isFirstCharacter) {
/*If first character is cr or lf, then we're off the right hand side of the link. Maybe way outside.*/
if ([oneCharacter isEqualToString:@"\n"] || [oneCharacter isEqualToString:@"\r"])
return nil;
}
 
isFirstCharacter = NO;
if (!stringCharacterIsAllowedAsPartOfLink(oneCharacter))
break;
[s appendString:oneCharacter];
 
textPosition = [self positionFromPosition:textPosition offset:1];
if (textPosition == nil)
break;
}
 
/*Move left*/
 
textPosition = [self positionFromPosition:tapPosition offset:-1];
if (textPosition != nil) {
 
while (true) {
UITextRange *rangeOfCharacter = [self.tokenizer rangeEnclosingPosition:textPosition withGranularity:UITextGranularityCharacter inDirection:UITextWritingDirectionNatural];
NSString *oneCharacter = [self textInRange:rangeOfCharacter];
 
if (!stringCharacterIsAllowedAsPartOfLink(oneCharacter))
break;
[s insertString:oneCharacter atIndex:0];
 
textPosition = [self positionFromPosition:textPosition offset:-1];
if (textPosition == nil)
break;
}
}
 
return s;
}
 
 
- (NSString *)rs_linkAtPoint:(CGPoint)point {
 
NSString *potentialLink = [self rs_potentialLinkAtPoint:point];
if (potentialLink == nil || [potentialLink length] < 1)
return nil;
 
NSArray *links = [potentialLink rs_links];
if (links == nil || [links count] < 1)
return nil;
NSString *firstLink = links[0];
return firstLink;
}
 
 
@end

If you can calculate a UITextRange enclosing the entire link, you can call [self selectionRectsForRange:linkRange] to get a series of NSValue-wrapped CGRects indicating the areas of text to highlight. From there, you can put some translucent highlight boxes over the selection.

Is Vesper iOS 6 only? If so you can use UITextView.attributedText to highlight the link on tap.

Why not use NSDataDetector to find links proactively, e.g.:

NSDataDetector *detector = [NSDataDetector
    dataDetectorWithTypes:NSTextCheckingTypeLink
    error:&error];

NSIndexSet *linkRanges = nil;
NSArray *links = [detector
    matchesInString:string
    options:0
    range:NSMakeRange(0, [string length])];

if ([links count] > 0) {
    NSMutableIndexSet *tempRanges = [NSMutableIndexSet indexSet];
    for (NSTextCheckingResult *match in allMatches) {
        [tempRanges addIndexesInRange:[match range]];
    }
    linkRanges = [[tempRanges copy] autorelease];
}

// linkRanges can quickly check if a character falls within a link, and
// then links can then be searched to find the relevant URL.

Then just check if the tapped location maps to a character in a range known to contain a link. This could be computed on demand, cached, and only invalidated if the text changes.

Brent's talk at altWWDC covered why he didn't use data detectors, but I don't have any notes on it.

Edit: But there are slides! http://inessential.com/downloads/altwwdc-2013-brent.pdf

This is probably pretty darn foolish, but if you're looking to highlight just the text, have you considered creating a second UITextView with just the URL over where the link is? Using the NSString UIKit additions, you can position it over the original URL down to the pixel. (Then again, line wrapping might be a PITA and you might have issues with semi-transparent pixels if there's AA going on.) I remember we used this technique a couple of years ago to quickly color some text for a game.

As @shadowofged mentioned above, if you can pre-parse your text to get an NSRange for all the links (regardless if you use NSDataDetector or not) then it's straightforward to hit test that range in the UITextView:

NSRange range = linkRange;

UITextPosition *beginning = textView.beginningOfDocument;
UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
UITextPosition *end = [textView positionFromPosition:start offset:range.length];
UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
CGRect rect = [textView firstRectForRange:textRange];

if (CGRectContainsPoint(rect, point)) {
    // link was tapped - do something   
}

I'm using this in one of my apps and it works very well.

rs_links implementation?

Unfortunately at the moment I can't characterRangeAtPoint to work in iOS 7 unless the UITextView has or previously had a selection.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.