One of my earliest gaming experiences with a PC was with the classic game DOOM, some time around 1994. DOOM broke new ground for computer games in many ways, from its addictive networked gaming to its popularisation of the First Player Shooter genre. But the thing about DOOM I think about the most is altogether more mundane: its loading sequence.
I played DOOM way too much when I was a kid, so I saw that loading sequence a lot. Unlike the loading screens found in modern games it had no graphics or animation, instead featuring scrolling white text on a black screen. In a way this fitted the game’s low-fi, science fiction aesthetic.
There’s one particular section of the loading sequence text that’s always stuck with me – the line “R_Init: Init DOOM refresh daemon”. Back then I wasn’t aware that daemons are long running processes in the host machine. So instead I assumed this somehow related to the demons that the player would go on to fight in the game.
That mistaken understanding served to keep me entertained as I began hundreds of games of DOOM. What it illustrates, aside from the chronic naïveté of a geeky twelve-year-old, is that people will happily sit through a loading process if it somehow engages with them.
Some twenty years later this early lesson in human-computer interaction would come in handy as we considered how to best communicate progress made in our iOS app’s synchronisation process to its users.
Before we get to that, let’s refresh our memories as to the approaches one could take to articulate a progress view.
Progress bars can function in two different ways.
First there are what we’ll call determinate progress bars, which measure their progress by some discrete, atomic metric. A good example would be a series of calculations to perform – as each calculation is made, the progress bar advances one step through to completion. Assuming a roughly equal amount of work in each calculation, by observing the rate of progress a user can get a fairly accurate sense of how long is left in the overarching task. We’d consider this an ideal user experience.
In contrast, indeterminate progress bars aren’t driven by a discrete metric, and so can’t accurately communicate how long is left until the task completes. Operations involving a network are good examples. One typically sees these in situations where it’s impossible to calculate the length of a task, or where the programmer is too lazy to do so.
A combination of both determinate and indeterminate steps can also drive progress bars. A good example of this is a browser’s attempt to load a web page, and the progress bar it displays while doing so. While some elements of the work involved can be determinate – such as the downloading of the page’s HTML, the size of which is sent to the browser from the web server – some are not. To begin with, the browser has to find the server through a DNS lookup and then wait for it to respond. There’s no way to know how long this step will take, so the browser has to fake progress instead.
Taking the indeterminate concept a stage further are spinners, which abandon the bar in favour of a circular graphic that animates indefinitely until the operation is complete. Popular since the advent of web apps, these are suited to operations which cannot accurately depict their progress but are expected to complete in short order, like an AJAX request. The user is therefore aware that something is happening, even if they can’t gauge when it is likely to finish.
Another common device for communicating progress found in games is the “loading game” concept, where the user can play another game as a distraction while the main game loads in the background. It’s arguably more of a distraction than a progress meter, but that depends on the implementation. These devices are tangential to the thrust of this article, but they are pretty cool so I thought to mention them.
Let’s Get Some Context
And so to the problem at hand.
In the Bipsync Notes iOS app we often need to sync a substantial amount of data to a user’s device. An average sized fund with a few years of research in their database will usually have in the region of 15-20,000 notes, plus the metadata that goes along with them (tags, tickers, files and such). Downloading all this information is a lengthy process.
We think it’s important that our users are aware of what’s happening while this data downloads. We don’t want to cause confusion or frustration.
Earlier we described how determinate progress bars are a good solution for long running processes, because they can accurately reflect the rate of progress. That was foremost in our thinking as we approached this problem. How could we couple our sync process to such a progress bar?
Let’s consider the sync process. It consists of a sequence of steps:
- Download data the app depends on to function – users, groups, teams etc.
- Upload any new notes to the server
- Upload any modified notes to the server
- Persist the deletion of any notes to the server
- Download all new or modified notes from the server
- Delete any notes on the local device that have been deleted on the server
We’ve simplified things a bit, but the that’s the essence of the process. The important thing to note is that each of those steps deals with a quantifiable amount of work, e.g. For step 5 we need to:
- Find the ids of the notes we have on the device
- Send those to the server, which diffs those ids against the notes it knows have changed
- The server returns a list of ids that the app needs to download
- The app breaks those ids up into batches
- The app iterates over each batch of ids, requesting the full content of the note from the server and applying the response to the local database
We can break those steps down even further. What emerges is a sequence of discrete tasks that we can translate into sections of a progress bar. As each task completes, the bar advances.
For us this approach is ideal . We’re not guessing or faking progress, or presenting the user with a spinner that might take an hour to disappear. We can even go one step further and provide some contextual information as text so the user has a clear idea of what the app’s doing. As we download tags we can update the progress view to say just that. As we download notes in batches of twenty we can update a counter that gives the user an explicit quantitive impression of where they are in the process and how much longer it’s likely to take. Something along these lines:
Now we need to implement this approach in iOS. As it turns out, Cocoa Touch has APIs that make implementing a progress bar a relatively straightforward affair.
A Wild NSProgress Appears
We were pleasantly surprised to discover that Apple thoughtfully considered how to store and expose progress within apps. A UI component,
UIProgressView, can draw a progress bar on screen. A Foundation class,
NSProgress, has been designed to represent progress made against a task. These classes are perfect for our needs.
NSProgress conforms to the KVO (Key-Value Observing) pattern which allows our code to subscribe to changes in the object’s properties, like
fractionCompleted. This makes it easy to get updates as progress is made, and use those updates to drive a progress bar such as one provided by
UIProgressView. Here’s a contrived example:
fractionCompleted updates, it triggers a callback which then update the progress view. We’re using Facebook’s
KVOController library to provide the observe/callback functionality; we could do something similar using Apple’s frameworks but it’s not quite as succinct. We use
KVOController quite a lot within our app because it’s nice to work with and it prevents the kind of “message sent to deallocated object” bugs that are easy to introduce when working with KVO.
The second cool thing about
NSProgress is that it works hierarchically. Consider the step in which we download notes in a set of batches. We’ve instantiated an
NSProgress object to track its progress, but the code that actually contains the note download logic is spread across a few different classes in our codebase. We could try to pass a single
NSProgress object around and update it wherever progress is made, but that would require each section of our code to work on the same scale so that all increments are relative and consistent.
The nicer solution is to create more
NSProgress objects and make them children of our “master” object, and allocate each of them a portion of its overall progress. Each subsection of the task has a child progress object and updates it as appropriate, on a scale that makes sense to the task. This results in the overall progress automatically updating in a relative fashion. This way even the most complicated item of work can be broken down to report its progress accurately.
The third and final benefit of using
NSProgress objects is that they are able to store custom metadata. This proves super useful for shuttling data from way down in the hierarchy of progress objects up to the very top, to the object that actually drives the main progress bar that the user sees in the app. As we sketched out earlier, alongside the progress bar we want to show some explanatory text to let the user know exactly what the app’s doing – “Downloading tags”, “Downloading 10 of 500 notes” and the like.
The latter string features variable text such as the number of notes already downloaded and the total remaining to download. The data to populate these strings needs to pass from the code that actually makes batched requests, through the code that sequences the sync operations, and up to the code that displays the progress bar. We can do this transferring the information between
NSProgress instances as each task completes. This allows us to keep a healthy separation of concerns between these modules of code while still maintaining accuracy and immediacy.
We integrated the above approach into our sync engine, which makes extensive use of Foundation’s
NSOperation class to perform the various tasks involved in a sync process. Generally speaking, each operation now has a progress object to work with, which it updates as it goes about its work. These operations, and progress objects, are nested so progress gradually filters back to the master progress object, maintained by the sync engine. This progress object is also made available to the UI to drive the progress bar. Diagrammatically it looks like this:
And here’s our progress bar in action within the app:
There may not be actual demons driving our progress bars, but the 35 year old me is just as enthused by these neat progress APIs as the 12 year old me was with the original DOOM.
OK – maybe not quite as enthused.