What I love about working at Hulu is having the opportunity to solve complex problems with creative solutions that improve our user experience. A few months ago, our customer support team received an email from a Hulu Plus subscriber requesting VoiceOver support on our iPhone and iPad app so users that are visually impaired can access Hulu content.
Watch the full story here:
Enabling VoiceOver would have been extremely simple for apps using simple UILabels to display texts. But in our scenario, it was much less trivial.
The Hulu Plus app had styled text in multiple places, and some of it required complicated alignment. For example, the description next to a video thumbnail has a bold show title (which can be one or two rows), a regular video title, and detailed information in gray. Depending on the video's expiration date, there may also be a line containing an expiration notice, which begins with an icon. And depending on whether captions exist for the video, a cc icon may appear on the last row. This group of metadata is vertically center-aligned.
The code dealing with the rich texts was a bit clunky. The text and icons were rendered in -drawRect: methods of UIView subclasses, with alignment based on multiple -sizeWithFont: calculations and if statements. The logic varied from place to place, so there were similar but unsharable implementations at various locations (e.g. the video page, the show page, thumbnails, and message popovers).
When we received the VoiceOver request, we figured it would be a good opportunity to refactor our text-rendering code base. And that led to the birth of GSFancyText.
What is GSFancyText?
First off, why the “GS” prefix? Because Hulu + iOS = green apple. And what do you think of when you hear “green apple”? Granny Smiths! GSFancyText is the first Granny Smith project of many that will come in the future.
GSFancyText is a rich text drawing library that allows users to format styled text with an HTML/CSS-like markup system. For example, the big chunk of code that we used to format and align the video description can now be simply defined with a simple line like “<p><strong>Family Guy</strong></p><p>Death has a shadow</p><p class=detail>S. 1 : Ep. 1 (22:31)</p>”, with the help of GSFancyText.
It follows the syntax of CSS and HTML, includes some CSS-like attributes (“text-color”, “font-size”, “text-align”, etc) and provides several tags (“<p>”, “<span>”, etc) into which you can insert styles. It is not a true subset of CSS/HTML largely due to the differences between mobile apps and the web.
Of course we considered other options such as directly using HTML in a UIWebView or using NSAttributedString. But we decided to make our own style/markup parsing and rich-text drawing system, because of the following advantages:
It’s faster and consumes less memory than UIWebViews.
We can reuse the styles and parsing results in many places.
We can easily modify the style or text of a small part of a paragraph.
We can potentially extend the system with more fancy features, like Quartz 2D features, animations, gestures, etc.
It makes localization simple. For example, a phrase marked as bold might be at a different position in the sentence in Japanese. In this case we can use a single NSLocalizableString to represent a sentence with various styles.
It’s easy to extract the plain text, on which we can enable the VoiceOver support.
How does it work?
The following example demos how simple it is to use GSFancyText:
Beyond that, the GSFancyText’s killer feature is the ability to insert any image or native iOS drawing code anywhere inside the styled paragraph. For example, if we want to insert a TV icon between Hulu and Plus, we can simply do:
The lambda tag is magical. You can draw images, call CoreGraphics methods, draw text interlaced with images – virtually anything you can do with Objective-C code.<p></p>
Often a UI designer will ask for consistent, custom styles across the whole app. To support this, we’ve made GSFancyText styles and parsing results reusable via a global stylesheet. Styles are parsed only once, then can be used anywhere, for every GSFancyText object. In many cases, a markup text structure can be reused too. One typical example is a table with many cells based on styled texts. All cells have the same format, but the text and attributes (e.g., color) differ. In this scenario, we can keep a global, static copy of the parsed structure and replace the text and styles inside certain tags. For example we have a GSFancyText object (let’s call it fancyText) based on the markup string “<p id=title_line>the dummy title</p>”, we can simply call:
[fancyText changeToText:@"the real title" forID:"title_line"];
This changes “the dummy title” to “the real title,” leaving the object’s markup structure intact.<p></p>
Fascinated? Hoping to use this in the next version of your app? Check it out on our Github page:
Finally, we want to share some of the challenges this project posed. Parsing style sheets and markup text isn’t all that difficult. But what about line breaks? It may be trickier than you think at first. Line-breaking rules vary among natural languages. In English we typically wrap words by separating words by spaces, and we generally don’t break in the middle of a word. But in Chinese, we can place line breaks between any two characters, as long as a line doesn’t begin with certain punctuation marks. And there are many languages that none of us at Hulu knows well enough to comment on, so we can’t make assumptions about their rules. To solve this problem, we left the burden of rule determination to Apple’s -sizeWithFont: method and designed the following algorithm that is otherwise universal across natural languages:
Take enough characters from the beginning of the string to fill up a little more than one line.
Get the size of the substring taken in (1) (with the width limit) and set the calculated width to our target width.
Remove characters from the end of the substring until the height of the substring (with the width limit) is equal to one line and its width is equal to the target width.
Form a line with the current substring.
Go back to 1 and start from the first character after the ones we used to form the last line.
We also put some thought into designing the data structure for storing parsed markup. This structure has to facilitate searching and text/style replacement. We used a tree structure with two kinds of nodes: container nodes and content nodes. A container node is based on a markup tag. It stores an array of child nodes as well as the styles defined by its class. A content node can either be a piece of text or a lambda block. It inherits the styles of its parent container node. The root node of a tree is a special kind of container node. In addition to its array of children, it also stores two hash maps for fast search of a given ID or a given class name. Each node also stores a reference to its parent. So when we append a new subtree under container node A, all styles along the ancestral path of node A are passed onto the new subtree (container nodes in the new subtree can either reject or accept a style that is passed down based on whether this style is defined in itself already).
We didn’t just assume the performance of this code. We have constructed different test cases to compare the rendering speed of GSFancyText to some other solutions. Testing results vary from case to case, but in general, if we reuse the parsing result of GSFancyText, its speed is quite similar to (just a little slower than) directly drawing text in a customized UIView, while using a UIWebView is normally 10 times or more slower. In our real-world example (the Hulu Plus featured video table), GSFancyText takes about ~6 milliseconds to render a table cell on iPhone 4S, while directly interacting with the drawRect method takes ~5 milliseconds (the code for this implementation was quite ugly). The extra millisecond is mainly consumed by replacing the text in the pre-parsed structure. We made several optimizations to improve performance, like skipping the line-break logic if we have already reached the line-count limit and skipping the -sizeWithFont: calculation when assigning the space for the last segment in a line.
We are constantly improving the code, and we look forward to seeing your fancy app with GSFancyText making big money in the App Store.
<div class="blog-body2">Bao Lei (aka “The THUNDER STORM”) is a software developer in the mobile team who works on our iOS platform. </div>