The other answers have pointed out that there are possible approximations for how much text fits, there's no real way to get the actual text rendered by a UILabel
because the it doesn't expose any outputs of the rendering process.
If you render your text through something like TTTAttributedLabel
, you can get a look inside the rendering process and inject some code to get information about the text that was rendered.
For example, -[TTTAttributedLabel drawFramesetter:attributedString:textRange:inRect:context:]
renders the actual text with CoreText by generating a CTFrameRef
. CTFrameRef is tasked with laying out text and reporting what text fit and what didn't.
Ignoring truncation:
If you don't care about the fact you're losing characters to truncation, you can call CTFrameGetVisibleStringRange
to get the range of characters that are displayed. The idea behind this function is that things like multi-column text layouts need to know where to begin on their next frame:
CFRange actuallyRenderedRange = CTFrameGetVisibleStringRange(frame);
NSString *actuallyRenderedText = [attributedString.string substringWithRange:NSMakeRange(actuallyRenderedRange.location, actuallyRenderedRange.length)];
Considering truncation:
Truncation is complex and a black box with regards to CoreText. For non-truncated lines, the the label can just render the line from CTFrameGetLines
. For truncated lines, it must use CTLineCreateTruncatedLine
, specifying the truncation line, the truncation point and the truncation token (...). Once you have this line, however, there's no direct way to get what text was chopped off since it could happen at the start, end, or middle of the line. See https://stackoverflow.com/a/5672594/860000.
If you assume end, you can force the truncation to be its own glyph run and get the end position of the second to last text run. If you compare that with the number of characters removed you can get the new string:
Add a dummy attribute to the truncation string:
NSMutableDictionary *truncationTokenStringAttributes = [self.truncationTokenStringAttributes mutableCopy];
if (!truncationTokenStringAttributes) {
truncationTokenStringAttributes = [[attributedString attributesAtIndex:(NSUInteger)truncationAttributePosition effectiveRange:NULL] mutableCopy];
}
truncationTokenStringAttributes[@"DummyAttributeToForceNewRun"] = @1;
After truncatedLine
is created compare the second to last run end position to the normal line length:
CFArrayRef truncatedLineRuns = CTLineGetGlyphRuns(truncatedLine);
CFIndex truncatedLineRunCount = CFArrayGetCount(truncatedLineRuns);
CFIndex numberOfCharactersRemoved;
CFIndex untruncatedLineLength = CTLineGetStringRange(line).length;
if (truncatedLineRunCount > 1) {
CTRunRef lastRealRun = CFArrayGetValueAtIndex(truncatedLineRuns, truncatedLineRunCount - 2);
CFRange lastRunRange = CTRunGetStringRange(lastRealRun);
numberOfCharactersRemoved = untruncatedLineLength - (lastRunRange.length + lastRunRange.location);
} else {
numberOfCharactersRemoved = untruncatedLineLength; // Everything was removed.
}
if (numberOfCharactersRemoved > 0) {
actuallyRenderedText = [actuallyRenderedText substringToIndex:actuallyRenderedText.length - numberOfCharactersRemoved];
}
actuallyRenderedText = [actuallyRenderedText stringByAppendingString:truncationTokenString];
By the time the function completes actuallyRenderedText
will contain the actual text that was rendered, including the truncation symbol. I've tried this for a variety of strings and this has come up with a consistent result.