-
-
Save brentsimmons/5810992 to your computer and use it in GitHub Desktop.
@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 |
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.
Is Vesper iOS 6 only? If so you can use UITextView.attributedText to highlight the link on tap.