Home | About | Apps | Github | Rss
Detecting touches on UIViews is quite trivial. But trying to recognize touches on individual words or attributed strings can be not so much, which was exactly what I wanted to do.
There are 2 approaches one can take
TextKit is designed based on MVC, with:
NSTextStorage
acts as the data provider and can own multiple NSLayoutManagers
. For the layout manager to display text, it needs an instance of NSTextContainer
, which essentially defines the area of visible text.NSTextContainer
has 2 main properties defining the layout - the bounds and the exclusion paths. Bounds is the outline rect, this is required. Optionally one can pass a bezier path to define which areas are not to be layouted - allowing one flow text around images.
Here is some code that detects touch on “Read More” string of a UILabel*.
/*
Some initialization
*/
- (void)viewDidLoad {
// The full string
NSMutableDictionary *attributesForString = [[NSMutableDictionary alloc] init];
attributesForString[ NSFontAttributeName ] = [UIFont systemFontOfSize:13];
self.attrString = [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor set amit"];
// The "Read More" string that should be touchable
attributesForString[ NSFontAttributeName ] = [UIFont boldSystemFontOfSize:13];
self.moreString = [[NSAttributedString alloc] initWithString:@"READ MORE" attributes:attributesForString];
[self.attrString appendAttributedString:self.moreString];
// Store range of chars we want to detect touches for
self.moreStringRange = [self.attrString.string rangeOfString:self.moreString.string];
self.textLabel.attributedString = self.attrString;
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self selector:@selector(didTap:)];
[self.textLabel addGestureRecgonizer:tapRecognizer];
}
/*
Simple touch recognition.
Could be setup during initialisation.
*/
- (void)didTap:(UITapGestureRecognizer *)gesture {
// Storage class stores the string, obviously
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attrString];
// The storage class owns a layout manager
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
// Layout manager owns a container which basically
// defines the bounds the text should be contained in
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.textLabel.frame.size];
// For labels the fragment padding should be 0
textContainer.lineFragmentPadding = 0;
// Begin computation of actual frame
// Glyph is the final display representation
// Eg: Ligatures have 2 characters but only 1 glyph.
NSRange glyphRange;
// Extract the glyph range
[layoutManager characterRangeForGlyphRange:self.moreStringRange actualGlyphRange:&glyphRange];
// Compute the rect of glyph in the text container
CGRect glyphRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
// Final rect relative to the textLabel.
NSLog( @"%@", glyphRect );
// Now figure out if the touch point is inside our rect
CGPoint touchPoint = [gesture locationOfTouch:0 inView:self.textLabel];
if( CGRectContainsPoint(glyphRect, touchPoint) ) {
NSLog( @"User tapped on Read More. So show something more");
}
}