Recap of My Recent Github Goodies

Under the influence of Bryan Irace, an iOS engineer at Tumblr, I’ve open-sourced a lot of handy iOS utilities lately. These are all polished, single-purpose units of code which should be cake to drop into your projects.

|  10 Apr 2014




Free Kickstarter Idea: Inkjet Typewriter

About ten years ago I experimented with doing all my personal writing on a used electronic typewriter. Although the experiment was a success, it had one crippling drawback; ink ribbons were consumed much faster than I was expecting, and were hard to replace. Only certain stores carried them. It led me to wonder if a product like this could be possible:

An Inkjet Typewriter

What if instead of obsolete typewriter ribbons, you could refill your typewriter using generic ink jet cartridges? Imagine a slim keyboard, like the Apple Wireless Keyboard, with a carriage-roll portion along the back edge, housing an ink jet write head. It would lay down one letter at a time, with a soft whish. It would be thin and portable, perfect for writing anywhere. And since it uses inkjet cartridges, buying refills would be as easy as a quick walk to the drug store.

Perhaps you could have an alternate version that’s just the carriage roll, which would connect to any Bluetooth-enabled keyboard. Either way, if you see an LCD display, they blew it.

If anyone is so inclined to make this thing a reality, I’d happily consult on the project, free of charge, as long as I get one for myself.

|  10 Apr 2014




Designing Unread for iPad Part 4 – The One Screen I'm Sure About

There’s one screen design in Unread for iPad which I’ve been sure about since the beginning:


Click to see full resolution.

The margins and other measurements are subject to final tweaks, but this is what the article view is going to be. The rest of the app’s hierarchy should make this screen feel right at home.

|  10 Apr 2014




Designing Unread for iPad – Part 3

This post is part of my ongoing series of posts1 in which I’m documenting the design of Unread for iPad.

In my previous post in this series, I drew some rough paper sketches of possible layouts, ranging from the typical to the atypical. In thinking through which ones I liked more, I arrived again at a conclusion I’d reached when following the same kind of process for Unread for iPhone: I like apps that are physically comfortable, especially when frequently-used controls can be reached without having to reposition the device in my hand.

Most iPad apps put common controls at the top and bottom of the screen in hard-to-reach navigation bars or tab bars. This sacrifices comfort for the sake of familiarity. I’d rather Unread make the opposite sacrifice. The most comfortable areas to reach with one’s thumbs on an iPad are the outer edges of the screen, vertically-centered. These areas are right next to your hands if you’re holding your iPad in a typical grip. This is true of both one-handed and two-handed operation.

So this is my first design constraint: wherever possible, controls and navigation should be accessible within those comfort areas. Constraints are a necessity if I hope to finish my design in a reasonable amount of time. Like performing a gram stain on an unidentified bacterial culture, a hard design constraint decisively eliminates whole swaths of possibilities, whittling down the project to a more manageable scope.

Every screen in Unread needs a way to present a menu of options. Some screens have few options, others have many. I can only think of a few solutions to this problem within my primary constraint:

Persistent Vertical Toolbar

I could place a persistent vertical toolbar along the righthand edge of the screen. This would consist of a single column of icons without text labels.


Persistent vertical toolbar.

Hovering Options Button

I could place simple hovering button on the right edge of the screen. This could be persistent or transient when scrolling, but would be superimposed over all the other content in the app. Tapping the button would show a temporary options menu overlay (setting aside the options menu layout itself for another day).


Hovering Options Button.

iPhone-Style Options Gesture

I could reuse the sideways pull-to-refresh gesture that I used in the iPhone app, with the transient options menu mentioned above.


iPhone-Style, unobscured content.

The persistent vertical toolbar won’t work for two reasons. First, it’s distracting, which undermines my overall goal of a comfortable reading experience. Second, icons alone aren’t enough to explain what certain options do. An icon button with a text label is more informative than the sum of its parts.

The hovering option button would be better than the persistent toolbar, but its still a distracting smudge on the interface.

So my best choice is to stick with the good solution I’ve already used on the iPhone. I could have intuited this from the beginning and saved a day of work, but it helps to think it through anyway. It’s not enough to have a good design. You have to be able to convince other people why your design is good.


  1. Though I began this series as a video podcast, I think it makes more sense to continue it in whatever form makes sense for each stage. 

|  9 Apr 2014




Cell Height Caching Dilemmas in Unread

The biggest performance challenge in Unread is calculating attributed strings and cell heights for the article summary lists. Those cells are practically just a UIView with some text drawn into it using the NSAttributedString UIKit Additions.

Once an attributed string or cell height is calculated, it is cached (in memory only) and re-used. This vastly improves scrolling performance, for obvious reasons. The challenge isn’t in caching per se, but in the fact that since Unread is an RSS reader, it is not uncommon for a single table view to contain 5,000 or more items.

In the current beta of version 1.2, when an article list is fetched from the local database, I have been pre-calculating all strings and heights on a background queue before popping back up to the main queue (in batches).

In versions 1.0 and 1.1, I didn’t pre-calculate anything. Instead, I used UITableView’s new estimatedHeightForRowAtIndexPath: optional delegate method. It isn’t ideal, but it worked reasonably well on slower devices without significantly increasing complexity.

There is a big drawback to this approach. As soon as a table view’s delegate implements that method, frustrating bugs crop up: tapping the status bar to scroll-to-top is wildly inaccurate, you can’t scroll to a target indexPath with reliable accuracy, etc. This is true even when I try returning an exact pre-calculated height from a call for an estimated height. This means I can’t preserve the semantic content offset when reloading a table view (i.e., when new stuff has been inserted above the current offset). The table view appears to jump around as new content is loaded, which is an irritating experience for the user.

So back to version 1.2. Pre-calculating works great—on newer devices. But older devices (iPods touch and iPhones 4S and earlier) have a hard time calculating 5000+ cell heights when viewing the “All Articles” screen. Even though they’re batched in, the entire sweep can still take 5 to 10 seconds or more. Not ideal. When calculations take that long, the window of exposure to potential race conditions is really wide. What if the user switches themes or font sizes during that interval?

Some Possible Solutions

There are several approaches that have come to mind to get acceptable results on all devices. Here are some solutions I’ve considered:

1) Write Heights to Disk

I could save the pre-calculated heights to disk, so that they’re only every calculated once. So the very first time a set of articles is ever loaded would be slow, but subsequent loads would only need to process the newest articles. But there’s a problem: cell heights change depending on the relative date stamp. Articles published today use the time (11:38 AM), articles published in the last seven days use the day and day number (THU 18), and so forth. So the caching strategy would require keeping track of the date the height was calculated, the current date, and the date the article was published. If I ever allow users to rename feeds, then every cached height for a given feed would need to be invalidated, too. What if that happens while the app is pre-calculating thousands of new articles on a background queue? Thinking of how I’d implement all this makes me queasy.

2) Redesign the Cells

I could write the heights to disk, but eliminate the need for date-based cache invalidation by redesigning the date stamps to never alter the flow of the other text. I really don’t want to do this. I’ve already spent an embarrassing amount of time experimenting with article cells. I’m happy with the current look. Besides, this wouldn’t solve the case where a user can rename a feed.

3) Revert to Estimated Heights

I could revert to the estimated height approach I used in versions 1.0 and 1.1, and just throw out the improvements like semantic content offsets when reloading for new data.

4) Scrolling-Based Batching

I could batch new articles into the table view as you scroll near the bottom of the existing content, thus only ever processing the articles you’re actually going to see. On the surface this sounds like the easiest approach, but note that this adds a new axis of interdependence between the model layer (objects that fetch and sort articles from the database) and the view controller layer. It also means having to think about what should and shouldn’t trigger a load-more. What if the app is restoring the previous interface state on the next launch, and the last-visible article was the 4,000th item in a long list of articles?

What I Think I’ll Do For Now

Since the problem with exact pre-calculation only affects older devices, I’m going to try Option 5:

5) Device-Specific Caching

Newer Devices • Newer devices will not use the estimated row height delegate method, but will instead pre-calculate all strings and heights on a background queue before popping back to the main thread to update the table view. Since I’m not using estimates, I’ll be able to preserve the user’s perceived content offset when reloading for newer offscreen data. Status bar taps will also be reliably accurate.

Older Devices • Older devices will continue to use theestimatedHeightForRowAtIndexPath: method from versions 1.0 and 1.1. Heights will not be pre-calculated. This an acceptable trade-off between complexity and performance, since it’s limited to a minority pool of devices.

But how can I do that? As I stated above, merely implementing the estimated row height method introduces scrolling inconsistencies to a table view. I accomplish this via Dynamic Method Resolution. I provide an implementation like this in my table view’s delegate:

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
    if ([UIDevice unr_supportsPreCalculatedArticleCellHeights] == NO) {
        if (aSEL == @selector(tableView:estimatedHeightForRowAtIndexPath:)) {
            class_addMethod([self class], aSEL, (IMP) unr_tableViewEstimatedHeightForRowAtIndexPath, "f@:@@");
            return YES;
        }
        else if (aSEL == @selector(tableView:estimatedHeightForHeaderInSection:)) {
            class_addMethod([self class], aSEL, (IMP) unr_tableViewEstimatedHeightForHeaderInSection, "f@:@i");
            return YES;
        }
        else if (aSEL == @selector(tableView:estimatedHeightForFooterInSection:)) {
            class_addMethod([self class], aSEL, (IMP) unr_tableViewEstimatedHeightForFooterInSection, "f@:@i");
            return YES;
        }
    }
    return [super resolveInstanceMethod:aSEL];
}

The delegate’s .m file never actually implements the actual protocol methods. Instead, if the current device doesn’t support pre-calculated heights, I resolve those methods by adding custom methods at runtime using class_addMethod. This works because the estimated height methods are optional. The super implementation of resolveInstanceMethod: fails gracefully on newer devices. On older devices, my custom functions (prefixed with unr_ in the code above) will be called instead.

This approach requires the least possible amount of increase in complexity while still improving the app for some users. It also buys me some time while I continue to explore my options.

|  1 Apr 2014