When NSLineBreakByTruncatingTail and the default ellipsis doesn’t quite cut it for us, here are three methods to truncate a string to a constraining size and append custom text to the truncated string:
subtraction until it fits
addition until it doesn’t
binary search, because who doesn’t like log(N)
Read on, or go ahead and grab them as a category, and enjoy!
Let’s say you have a lot of text with variable length and only a 320x100 frame to display it. You don’t want to leave the user with a crude cut in characters without any indication that there’s more to read. What do you do?
iOS offers an easy fix:
The UILabel’s’ lineBreakMode property is set by default to NSLineBreakByTruncatingTail, resulting in a simple but powerful ... to signal the end:
The ellipsis is the universal solution to this problem, and a fine one at that for signaling the intentional omission of a word. If you want to also signal that there is some way to interact with your truncated text – for example, let’s say you want to make a truncated UILabel tappable for an expanded view – you might want to add a custom call to action: “see more”, “expand”, or even an ellipsis with a different color.
Adding some color does the trick
Here’s the rub: as far as I can tell, there is no way to reach under the hood and override iOS truncation to append your own version of an ellipsis. You can, however, write a decent workaround with a fit function using NSString’s [boundingRectWithSize:options:attributes:context] and mutate a string until it does fit. With a little research, I wrote something to the effect. First, the fit function, as a category on NSString:
Using [boundingRectWithSize:options:attributes:context:], we can check if the maximum height that a string would take up in a constraining width is less than or equal to the constraining height. Our fit function takes a parameter, trailingString, so that we can call the fit function on the final form of the text of the label: one part truncated string, one part custom string to indicate omission.
Now we can implement our first solution.
Truncation Method: Subtraction
Mutate the string, subtracting characters until [willFitToSize:trailingString:attributes:] returns YES.
We can create a mutable copy of the string and subtract one character at a time, stopping when height of bounds fits the constraining height. We can use that index to reconstruct string that fits.
Performance is O(N), where N is the length of the string. This naive implementation isn’t so bad if you know that your strings are limited to a certain character length, but if your string is a million characters and your frame fits 100, you might want to look at the next implementation.
Truncation Method: Addition
We add one character at a time from the string and stop when height of bounds exceeds the constraining height. We use the index to reconstruct string that fits.
Performance: constant in the size parameter.
While this implementation should be sufficient for most cases (and the most efficient, for super long strings), I was excited to try one more implementation: binary search. I’ve known in theory that binary search is a useful algorithm for sorting problems, but I’ve never encountered the fabled Algorithm in the wild.
This problem presented a rare opportunity to implement an algorithm outside the laboratory of computer science assignments.
Truncation Mode: Binary Search
Using 0 and N as starting indices, where N is the length of the string, we perform a binary search that maintains the invariants that:
height at minIndex <= size.height
height at maxIndex > size.height
We return minIndex when minIndex and maxIndex are adjacent. Performance: log(N).
Hopefully, this saves you some heartache the next time you want to add a fancy “more” indicator. You can check out how the truncation works here.