Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save bryanjclark/7036101 to your computer and use it in GitHub Desktop.
Save bryanjclark/7036101 to your computer and use it in GitHub Desktop.
My three current contenders for solving a tricky UILabel question: given an NSRange and a UILabel that displays attributed text, can you find the rect of the text in that range? Link to my related Stack Overflow question: http://stackoverflow.com/questions/19417776/how-do-i-locate-the-cgrect-for-a-substring-of-text-in-a-uilabel
- (CGRect)rectForSubstringWithRange:(NSRange)range
{
/* Core Text methods that seem somewhere in the neighborhood of useful:
CTRunGetPositionsPtr
CTRunGetPositions
CTRunGetImageBounds
CTLineGetStringRange
CTLineGetOffsetForStringIndex
CTLineGetImageBounds
*/
/*
Assumptions:
1. The text label is an attributed string, with word wrap enabled. You could modify this for a plaintext label, too, but that's not something I need at the moment.
2. We're just looking for the range of a single word.
3. The word is shorter than a single line of text. (Why? Because I'm building an App.net client, and I only need the rect of a <20 char string, and my UILabel is wider than 20-char.
4. We're writing this method on a category or subclass of UILabel. UITextView and UITextField seem to have this issue beat, but UILabel doesn't... yet it's part of TextKit. Color me confused or misguided on this one.
So, with those assumptions in mind, here's what I'm thinking:
*/
//First, confirm that the range is within the size of the attributed label
if (range.location + range.length > self.attributedText.string.length) {
return CGRectZero;
}
//Second, get the rect of the label as a whole.
CGRect textRect = [self textRectForBounds:self.bounds limitedToNumberOfLines:self.numberOfLines];
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, textRect);
CTFrameRef frame = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, [self.attributedText length]), path, NULL);
if (frame == NULL) {
CFRelease(path);
return CGRectZero;
}
CFArrayRef lines = CTFrameGetLines(frame);
NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
if (numberOfLines == 0) {
CFRelease(frame);
CFRelease(path);
return CGRectZero;
}
CGRect returnRect = CGRectZero;
CGPoint lineOrigins[numberOfLines];
CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
//NOW, THE FUN BEGINS: start slicing up the attributed string by line, until we get to *THE* line that contains our word. (Remember assumption #3 above?)
// FIRST ATTEMPT:
// This one felt *so close*, but for some reason, I run into a bug (?) with the CTLineGetOffsetForStringIndex, because I don't have "string access"
// Here's what the docs have to say about "string access":
// https://developer.apple.com/library/Mac/documentation/Carbon/Reference/CTLineRef/Reference/reference.html#//apple_ref/c/func/CTLineGetOffsetForStringIndex
for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
CGPoint lineOrigin = lineOrigins[lineIndex];
CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
CFRange lineRange = CTLineGetStringRange(line);
if ((lineRange.location <= range.location) && (lineRange.location + lineRange.length >= range.location + range.length)) {
NSLog(@"The text we're looking for is on line %ld", lineIndex);
CFIndex charIndex = range.location - lineRange.location; // That's the relative location of the line
CGFloat secondary;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, charIndex, &secondary);
NSLog(@"Found the first glyph! It's at x = %f", xOffset);
// //OK, here's where I really start to lose it - it feels *really* wrong and desperate, though.
// //The CTLineGetOffsetForStringIndex was just returning 0, which sucks! That means I don't have "string access", which I just don't understand
// UILabel *lineLabel = [UILabel new];
// NSAttributedString *lineBeforeCharAttrStr = [self.attributedText attributedSubstringFromRange:NSMakeRange(lineRange.location, charIndex)];
//
// //HA! Now I have a UILabel that's the exact width of everything up and until the glyph. BOOM.
// lineLabel.attributedText = lineBeforeCharAttrStr;
// [lineLabel sizeToFit];
//
// CGFloat xPosOfFirstLetter = lineLabel.frame.size.width;
// NSLog(@"BOOM MOFOs: we've found the xPos, and it's %f", xPosOfFirstLetter);
//
// UILabel *entityLabel = [UILabel new];
// NSAttributedString *glyphAttrStr = [self.attributedText attributedSubstringFromRange:range];
// [entityLabel sizeToFit];
//
// // Get bounding information of line
// CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
// CGFloat width = (float)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
// CGFloat yMin = (float)floor(lineOrigin.y - descent);
// CGFloat yMax = (float)ceil(lineOrigin.y + ascent);
//
// returnRect = CGRectMake(lineOrigin.x + xPosOfFirstLetter, yMin, entityLabel.frame.size.width, entityLabel.frame.size.height);
//
// break;
}
}
// SECOND ATTEMPT:
// This is the iOS7-friendly, reasonable, sane way of doing things, but since it only works on the UILabel's *text*,
// I don't think it'll work on the UILabel's *attributedText*, right?
// CGRect returnRect = [self.text lineFragmentRectForGlyphAtIndex:range.location effectiveRange:nil];
// THIRD ATTEMPT:
// This one consistently doesn't work for me, but I *feel* like it should work.
// Not sure what's going on, but it seems to get the line incorrect most every time,
// and seems to return weird stuff.
// It's based on this page: http://danielgorst.wordpress.com/2012/07/30/embedding-a-hyperlink-into-coretext/
CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
CGPoint lineOrigin = lineOrigins[lineIndex];
CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
CFRange lineRange = CTLineGetStringRange(line);
if ((lineRange.location <= range.location) && (lineRange.location + lineRange.length >= range.location + range.length)) {
NSLog(@"The text we're looking for is on line %ld", lineIndex);
CFIndex charIndex = range.location - lineRange.location; // That's the index of the first character in the range, relative to the CTLineRef.
// Go through the glyph runs in the line
CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
CFIndex glyphCount = CFArrayGetCount(glyphRuns);
CTRunRef foundRun;
BOOL runFound = NO;
NSLog(@"There are %ld runs in the line", CFArrayGetCount(glyphRuns));
for (CFIndex glyphIndex = 0; glyphIndex < glyphCount; glyphIndex++) {
CTRunRef tempRun = CFArrayGetValueAtIndex(glyphRuns, glyphIndex);
CFRange tempRunRange = CTRunGetStringRange(tempRun);
if (tempRunRange.location == charIndex) {
NSLog(@"Found the range! The loc is %ld", tempRunRange.location);
foundRun = tempRun;
runFound = YES;
}
}
if (runFound) {
CGFloat ascent;
CGFloat descent;
returnRect.size.width = CTRunGetTypographicBounds(foundRun, CFRangeMake(0, 0), &ascent, &descent, NULL);
returnRect.size.height = ascent + descent;
// The bounds returned by the Core Text function are in the coordinate system used by Core Text. Convert the values here into the return rect.
returnRect.origin.x = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(foundRun).location, NULL);
returnRect.origin.y = self.frame.size.height - lineOrigin.y - returnRect.size.height;
NSLog(@"Run bounds is x: %0.0f y: %0.0f w:%0.0f h:%0.0f", returnRect.origin.x, returnRect.origin.y, returnRect.size.width, returnRect.size.height);
returnRect = returnRect;
}
}
}
CFRelease(frame);
CFRelease(path);
return returnRect;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment