Skip to content

Instantly share code, notes, and snippets.

@brentsimmons
Last active January 3, 2021 02:22
Show Gist options
  • Save brentsimmons/5810992 to your computer and use it in GitHub Desktop.
Save brentsimmons/5810992 to your computer and use it in GitHub Desktop.
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.
@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
@beccadax
Copy link

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.

@an0
Copy link

an0 commented Jun 19, 2013

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

@shadowofged
Copy link

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.

@myacavone
Copy link

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

@archagon
Copy link

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.

@mluisbrown
Copy link

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.

@sudhanshu10
Copy link

rs_links implementation?

@scottymac
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment