Don’t Block The Main Thread – Adventures in Concurrency

PicTapGo has always been pretty snappy. This was a result of some design decisions we made at the outset – we render images at the minimum size needed, do our best to limit the depth of the filter chain at any one point, and “cheat” with the strength slider by using an alpha blend in the UI, instead of re-rendering the image constantly. However, there was still some unnecessary lag in a few places, and in version 3.0, I made a point of cleaning them up.

First, anytime we pushed a new View Controller, we blocked the UI while rendering the images we needed for that screen. It only amounted to about a second most of the time, but in today’s world, a second feels like forever to wait for the UI to respond. For instance, when pulling up the crop screen from the main edit screen, PicTapGo had to re-render the current document preview at 1.5x the usual UI resolution (for the crop preview). Or when moving from the photo library browser (the Choose screen) to the Edit screen, PicTapGo has to load the full-res image, render a preview-sized version of the image we’re about to edit, and finally, render a blurred version of that image to use in the background. And we did all this synchronously in the prepareForSegue call, or the viewDidLoad call in the target view controller, so everything would grind to a halt for a second.

Now, I like GCD as much as the next guy, but around this time, I also discovered PromiseKit, which is a promises implementation for Objective-C and Swift. It turned out to be a great fit, and now I’m a fan. The resulting code is just pleasant and easy to read, which can be a problem with Objective-C’s pyramid of death with inline block callbacks. For instance, this is the snippet that handles the loading of the preview image for the Crop tool, using PromiseKit. It’s simple, clean, readable, and most of all, doesn’t make the user wait:

    self.document.toolsQuickPreviewPromise.then(^(UIImage* tempPreview){
        [self.transformView setImage:tempPreview duration:0.25];
        [UIView promiseWithDuration:0.25f
                              delay:0.0f
                            options:UIViewAnimationOptionCurveEaseInOut
                         animations:^{
                             self.spinner.alpha = 0.0;
                         }].then(^(){
                             self.spinner.hidden = YES;
                         });
        return self.document.toolsPreviewPromise;
    }).then(^(UIImage* toolsPreview) {
        [self.transformView setImage:toolsPreview duration:0.25];
    });

In a nutshell, this kicks off a background task to produce a quick, low-res preview (based on the Edit screen’s preview), and when that promise fulfills, it executes a block that fades the temporary preview in, and fades the loading spinner out. In practice, this renders nearly instantly, so by the time the view has animated in, we’re already displaying something sensible. Next, that block kicks off another background task to render the larger, 1.5x resolution crop screen preview (we need this so you don’t see a blurry image when you zoom the crop rectangle in). One that executed, the temp image fades, nearly imperceptibly, to the final, full-res image.

This refactor cut seconds off the time the user had to wait when calling up the crop tool, and unless you’re mashing buttons really quickly, and looking really carefully for that loading spinner, you’ll probably never notice the difference.

Thumbnail rendering was also a wee bit problematic. Though the PicTapGo thumbnails render so darned fast on modern hardware that you almost never see a spinner, one piece of the original architecture never really sat well with me, and I fixed that in 3.0 as well. Originally, PicTapGo had a thumbnail renderer class, which was responsible for managing the render of 80+ image thumbnails in the background, and then hanging onto them for any UI elements that wanted them. The problem was, what if the thumbnail you wanted to display wasn’t ready yet? In the original design, PicTapGo would just return a spinner image. The UI was responsible for asking again once the thumbnail was rendered, but how to know when? The thumbnail renderer would emit notifications as thumbnails rendered, but there was no machinery in place to correlate those messages with cells in the view. Complicating things was that UICollectionView reuses cells, so by the time you got the notification that a thumbnail was ready, you might even be displaying that image anymore. So until v3.0, PicTapGo just set a timer, and reloaded the entire UICollectionView every few seconds, which is simultaneously slower than necessary, and also more expensive than necessary.

So my change was to create an array of data objects describing the name and filter the cell needed to represent, along with a promise-based mechanism for rendering the thumbnail. I moved all of this out of the document model, and into the view controller itself, since as far as I’m concerned, thumbnail renders for the UI aren’t properly a part of the model anyway. The resulting code displays rendered thumbnails as soon as they’re rendered, keeps view-specific thumbnail data out of the model, and has a prettier spinner to boot.

Long story short – don’t ever make the user wait for a response to their input. Change the UI state to reflect their request, and then start doing the heavy lifting that’s required to actually display it. Asynchronous code – it’s not just for http requests 🙂