Nick Harris

Fast UITableViewCell with a UIWebView

(skip to the code)

The Problem

If you use UITableView’s you know that scrolling through your cells has to be fast.  If you’ve done much research, you’ve probably found the Tweetie approach using one custom view and doing the drawing yourself.  This works great for static text and image cells.  It gets more difficult when you need to add hyperlinking to the text in the cell.  The problem becomes how you add hyperlinking to a UITableViewCell but keep the scroll speed as fast as possible.

First Solution

I have an app that requires this type of functionality.  Its an enterprise social computing app – you can think of it like Facebook or Yammer.  Users can post microblog status messages that other users can “like” and comment on.

The first attempt I made was to subclass ABTableViewCell and in my drawing I would set aside certain regions that were “linkable”.  In this screenshot, the light blue in the cells denotes text that is linkable – usernames in this case.

ActivityStream.png

My tableview cell then waits for touchesBegan and looks to see if a touch is in one of these areas.  If it is, it sets a flag denoting what area is touched and redraws the cell with a highlight behind the text.  When it gets touchesEnded it tells its delegate what function needs to happen.  So for the screenshot above, touching the username would navigate to a view showing that users details.

Its a pretty easy approach but it has its limits such as a status that has multiple links.  I was left with either hand parsing the html and measuring where all the text should go that’s linkable, draw that text in a different color, and remember those regions.  Or I could just strip out any HTML and draw flat text for the status…  I took the easy route.  Worked great for a 1.0 release but of course status linking was the number one feature request from users.

Second Solution

The idea came to me that I could add my own UIWebView to the cell and become its delegate so that I could react to any touches on links.  As you probably know though, adding subviews – especially non-opaque subviews – can cause scrolling to be slow and to skip frames in the animations.  To work around this I decided two things…

1. Only use the UIWebView when the text actually contains a link, otherwise just draw flat text
2. Only show the UIWebView when the tableview isn’t scrolling so that we don’t get in the way

This approach seems to work really well – but there are a few issues that you need to work around.  Most of them I’ve figured out but there are two that I’m still stumped on which I will detail in a bit.

Issues and Workarounds

Issue one – loading an HTML string into a UIWebView takes a second

It takes a little bit for the webview to display your HTML.  My workaround is to draw the HTML stripped version of the status in flat text and keep the webview hidden until you receive webViewDidFinishLoad. At that point you can remove the flat text or draw it in clear color and set hidden = NO on the webview.

Issue two – UIWebViews flash when first loading content

I honestly can’t tell you why this happens, but it does.  And its a bit jarring to the user experience.  The workaround for this is to simply move the code to redraw the cell and display the webview into its own method then use performSelector:withObject:afterDelay to call it.  0.35 second duration seems to work nice.

Issue three – UIWebViews will crash your app if you don’t nil its delegate

This was a weird bug.  Because your popping webviews on and off cells at a pretty quick pace, you’ll run into situations where the webview hasn’t completed loading your HTML before you want to pop the webview off and release it during prepareForReuse (and you always want to pop the webview off and release it when preparing for reuse!) or dealloc.  The problem is that the webview continues to live until its done loading whatever it is it was loading.  When its done it will tell its delegate (your cell) that its done.  If your cell was dealloced… CRASH.  So make sure you set its delegate to nil.

Other Issues I Haven’t Solved

Issue one – UIWebView eats touches

If your app responds to tableView:didSelectRowAtIndexPathto show a details view, you’ll notice that if you touch within the bounds of the webview, this method will never fire.  So the user is forced to touch a part of the cell thats outside the webview.  If you designed your cell well, they’ll have no idea where this boundry is.  Its a very frustrating user experience – one that I would love to get solved before shipping.

Issue two – status bar touches no longer scroll to top

If any of your displayed cells have a webview on them – hidden or not – the scroll to top by touching the status feature of the tableview quits working.  My guess is that the scrollView child of the UIWebView is eating these as well and not passing them up the responder chain.  Its another issue I haven’t resolved yet.

Code

OK – enough talk.  Here’s a sample project that shows how I do this…  FastTableViewCellWithWebView (or you can look at the full Mercurial repository here).  Download it, run it, let me know what you think.  And more important… let me know if you can resolved my two issues!

Written by Nick Harris

June 17, 2010 at 9:53 pm

10 Responses

Subscribe to comments with RSS.

  1. UIWebViews will crash your app if you don’t nil its delegate – This was a weird bug.

    This isn’t a weird bug, always always always nil anything you are a delegate for when you are going to dealloc. With rare occasions, in an effort to avoid circular retains, delegates are almost always assigned rather than retained.

    I think that a webview is a generally bad approach. They are heavy and expensive. If you’ve already figured out how to draw yourself, it’s only a minor bit of extra work to do hit testing on “linkable” areas of your layer.

    Jerry

    June 17, 2010 at 10:50 pm

  2. I haven’t tested it thoroughly, but you could use some javascript / the web view delegate to detect a touch.

    Update NHTableViewCellWithWebViewHtmlTemplate as such: (I added a javascript call to the body’s ontouchend event)

    NSString* NHTableViewCellWithWebViewHtmlTemplate = @”\
    \
    \
    \
    body { webkit-user-select:none;\
    word-wrap:break-word;\
    margin: 0px 0px 0px 0px;\
    font-family: Helvetica;\
    background-color:transparent;\
    color: black;\
    font-size: 14px;\
    font-weight:bold;\
    color:#444444 }\
    a:link {color: #3C91BA;}\
    a:visited {color: #3C91BA;}\
    a:active {color: #3C91BA;}\
    a {text-decoration: none;}\
    \
    \
    %@\
    \
    “;

    Then add the following to – (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

    if(navigationType == UIWebViewNavigationTypeOther) {
    NSString *scheme = [[request URL] scheme];
    if ([scheme isEqualToString:@”celltouch”]) {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@”CellTouch!” message:@”You touched a cell!” delegate:nil cancelButtonTitle:@”OK” otherButtonTitles:nil];
    [alert show];
    [alert release];
    return NO;
    } else {
    return YES;
    }
    }

    You would need to implement create an NHTableViewCellWithWebViewHtmlTemplateDelegate and implement it on your RootViewController to load the detail view. Presumably the delegate method would include the table cell tapped, so you could load the appropriate detail view.

    Again, I don’t know how well this will work in practice, but it could help with issue number 1, or at least give you something to look at.

    Kris Markel

    June 17, 2010 at 11:15 pm

  3. I would certainly never use a web view inside a tableviewcell. Brings more trouble than it solves.

    If you are opting for a webview anyway, why not replace the tableview with one webview entirely? Not the native experience, but probably a lot easier than your current approach.

    I still think the best way to go is to draw it all yourself.

    Joris Kluivers

    June 17, 2010 at 11:27 pm

  4. Your hunch is right: To make scrollsToTop work on your scroll view, iterate through the subviews of the UIWebView and when you find a UIScrollView, send setScrollsToTop:NO. I ran into this problem just yesterday. Works in iOS 4 at least.

    Thanks for the tips!

    Kevin Conner

    June 17, 2010 at 11:47 pm

    • Traversing the view hierarchies is hacky and brittle. Sometimes it gets the job done, sure – but don’t be surprised when it stops working.

      Jerry

      June 17, 2010 at 11:50 pm

      • That’s true. But we’re already on the subject of doing things that the API doesn’t give you any help with…

        I guess, if you wanted to write something robust, you could add a separate button to the cell that follows the link. A bit like the blue disclosure button does a different function from tapping the rest of the cell — but with a clear “link” icon. And when there are multiple links, bring up an action sheet so the user can pick.

        Kevin Conner

        June 18, 2010 at 3:00 am

  5. Could you help me how to make fast and smooth scrolling like Safari in UIWebView and fast zooming also?

    Chanin Sangchan

    November 12, 2010 at 5:31 am

  6. How about core text?

    GianPac

    October 11, 2011 at 6:47 pm

  7. to solve Issue one – UIWebView eats touches

    Set the userInteractionEnabled to FALSE in webView

    Anonymous

    January 6, 2012 at 7:05 am

  8. Did you ever find a way to stop uiwebview from “eating” touches? I have a UIWebView in a collection view and have the same issue.

    Nate

    September 21, 2013 at 4:50 am


Comments are closed.