Subscripts and Superscripts in a UITableView

February 28, 2010, 11:47 pm

I have written a handful of iPhone apps now and used UITableView extensively in one of them. Until recently, I had never needed to put any rich text into a UITableViewCell.

The data I am using for this project is basically an index list of mini HTML documents, with an index term for each document. I use the UITableView to render the index. Most of the index terms are in plain text, but there are handful of them that include HTML tags to indicate sub-scripted or super-scripted characters:

Glucose (C6H12O6)

My initial reaction to cases like this was to create a UITableViewCell that had a UIWebView as a subview. I could just stick my HTML in the sub view and voila! my index would be rendered perfectly:

// get the term so we can check what is in it NSMutableString * term = [[[NSMutableString alloc] initWithFormat:@"%@", [dataController objectInListAtIndex:[termIndex intValue]].term] autorelease]; // need to work out what sort of cell this needs to be NSRange textRange =[term rangeOfString:@"<"]; bool foundHtml = NO; if(textRange.location != NSNotFound) foundHtml = YES; NSString * cellIdentifier = [[[NSString alloc] initWithFormat:@"%@", (foundHtml ? @"HtmlCell" : @"Cell") ] autorelease]; // Dequeue or create cell of the appropriate type. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (cell == nil) { // didn't find a reusable cell, create one based on the type if (foundHtml) { // Grab the UITableViewCell from the HtmlCell XIB NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"HtmlCell" owner:self options:nil]; cell = [topLevelObjects objectAtIndex:0]; } else { // This cell doesn't have any markup, just use a regular cell cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease]; cell.textLabel.textColor = [UIColor blackColor]; cell.textLabel.highlightedTextColor = [UIColor whiteColor]; } cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; } // set the text if (foundHtml) { // grab the htmlView from the cell and set it's content HtmlCell * htmlCell = (HtmlCell*) cell; [htmlCell.htmlView loadHTMLString:[NSString stringWithFormat:@"%@", term] baseURL:nil]; } else { // set the cell text normally cell.textLabel.text = term; } return cell;

I had a nagging feeling about approaching the problem this way and after the writing the code and seeing the woeful performance of UIWebView I remembered what it was. Months ago I listened to this mobile orchard interview with the Joe Hewitt, creator of the Facebook iPhone app and the open source Three20 Project. Seeing the slugglish rendering of my beautifully marked up index terms, I recalled that the unsuitability of UITableView for web content, was one of the motivations for the Three20 project (or it's original implementation in the Facebook app).

I decided against using Three20 partially because of an unreasonable case of not invented here syndrome, but also because I didn't have the time to invest in learning a new framework. I also felt like my problem was a pretty simple subset of the rich text rendering problem (just the sub-scripts and super-scripts), so there must be a simple solution.

It occurred to me that UITableView does a great job of rendering unicode characters (both Ballet Index and Karate Index render unicode characters in the table and they look great). So maybe there are sup-scripted and super-scripted unicode characters for the small set of characters that I needed (+,- and the numbers)?

Yep, there is! All I needed to do was recurse my way through each of the HTML elements in my index and replace them with the appropriate unicode character. Problem solved:

- (unichar) mapCharacter:(char)sourceChar forElement:(NSString*)element { if ([element localizedCaseInsensitiveCompare:@"sup"] == NSOrderedSame) { if (sourceChar == '2') return 0xB2; if (sourceChar == '3') return 0xB3; if (sourceChar >= '0' && sourceChar <= '9') return (0x2070 + (sourceChar - '0')); if (sourceChar == '+') return 0x207A; if (sourceChar == '-') return 0x207B; if (sourceChar == -82) return 0x00AE; } if ([element localizedCaseInsensitiveCompare:@"sub"] == NSOrderedSame) { if (sourceChar >= '0' && sourceChar <= '9') return (0x2080 + (sourceChar - '0')); if (sourceChar == '+') return 0x208A; if (sourceChar == '-') return 0x208B; if (sourceChar == -82) return 0x00AE; } return sourceChar; } // N.B. This code works for a very specific set of data. It doesn't handle // nested html tags for instance. - (NSMutableString*) processMarkupCell:(NSMutableString*) cellText { NSRange openRange =[cellText rangeOfString:@"<"]; if (openRange.location == NSNotFound) return cellText; // found an angle bracket ... need to remove the elements and map the sub/sup characters NSRange closeRange =[cellText rangeOfString:@">"]; NSRange elementRange = openRange; elementRange.location++; elementRange.length = closeRange.location - elementRange.location; // we get the element so we can use the appropriate mapping for each character (sup or sub) NSString * element = [[[NSString alloc] initWithFormat:@"%@", [cellText substringWithRange:elementRange]] autorelease]; // set up the new cell text NSRange prefixRange; prefixRange.location = 0; prefixRange.length = openRange.location; // stick everything before the element in the newCellText NSMutableString * newCellText = [[[NSMutableString alloc] initWithFormat:@"%@", [cellText substringWithRange:prefixRange]] autorelease]; // find out where we need to stop NSRange stopRange; stopRange.location = closeRange.location + 1; stopRange.length = [cellText length] - (closeRange.location + 1) ; NSRange nextOpenRange = [cellText rangeOfString:@"<" options:(NSStringCompareOptions)0 range:stopRange]; NSRange nextCloseRange = [cellText rangeOfString:@">" options:(NSStringCompareOptions)0 range:stopRange]; NSRange postfixRange; postfixRange.location = nextCloseRange.location + 1; postfixRange.length = [cellText length] - (nextCloseRange.location + 1); // start mapping the first character after the closed element for (int currentPosition = closeRange.location + 1; currentPosition < nextOpenRange.location; currentPosition++) { // append each mapped character in the newCellText [newCellText appendFormat:@"%C", [self mapCharacter:[cellText characterAtIndex:(NSUInteger)currentPosition] forElement:element]]; } // stick everything after the closing element in newCellText (other tags will be resolved recursively) [newCellText appendFormat:[cellText substringWithRange:postfixRange]]; // recurse through the string .. getting all the elements return [self processMarkupCell:newCellText]; }

Permalink - Tags: Development,iPhone