Home | About | Apps | Github | Rss

Detect touches on words in UILabels

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

TextKit is designed based on MVC, with:

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");
	}
}

More posts