Skip Navigation LinksHome > View Post

Building your app version defense strategy – Part I

A few weeks back I shipped v1 of my first ever iOS app doto, with accompanying mini-site, video, the lot. The goal was two-fold: build an app for my wife and I that would be actually useful and, more importantly, walk in the footsteps of the iOS developer – soup to nuts. It’s one thing to play around building apps and experimenting, but entirely another to go end-to-end and actually ship and support an app.

image

http://doto.mobi

To ensure I got this all right, I found as many iOS app store checklists as I could and wanted to get my app through certification first time, no silly mistakes. And I’m pleased to report that I succeeded, 7 days later – the app arrived in the store. However, I can see in hindsight there were a couple of things missing from those checklists. Things I knew would be a problem but I chose to ignore it anyway, in the heated excitement of shipping. Of course, that was building an app versioning strategy.

One of the challenges of modern app development is how little control you have over people updating to the latest version. I’ve released two subsequent minor updates to doto since launch yet almost 50% of the install base remain on 1.0.0, which is fine, until I start to make server changes. There are two scenarios I want to consider:

  • I make a non-breaking change to the server and want to ‘downgrade’ the server behavior for older clients (backwards compatibility)
  • I make a breaking change to the server and want to force older clients to upgrade

In this post, we’ll look at my solution for the latter. In the next post, we’ll look at how I added code to help me support older clients where backwards compatibility is possible.

Forcing the upgrade

In iOS, there’s no way to actually *force* somebody to upgrade. However, I could make the client very aware of the need to upgrade or they’ll experience issues. To do this, I decided to implement a startup request that posts information to the service about the client (version, build etc) and the server can send back a set of messages. Each message can contain three properties:

  • Title – the title of the alert box
  • Message – the text to display to the user
  • Link [optional] – an optional link that if present, will be invoked.

The idea behind the link is I can force the app to open the appstore making it even easier for them to upgrade to the latest version (and it’s free!).

To do this, I created a ‘virtual table’ which I discussed in my recent posted video that gave an overview of the Mobile Services HTTP API (it’s worth watching and is less than 20 minutes long). I called the table vLoadMessages (the v prefix is a convention I use for virtual tables) and it had a single script on insert:

function insert(item, user, request) {

var messages = []; // if less than 1.2, request to upgrade if (compareVersions(item.version, "1.2") < 0) { messages.push({ title: "Please upgrade", message: "Please upgrade to the latest version of doto available in the store", link: "http://itunes.apple.com/us/app/doto/id590291737?mt=8&uo=4" }); }

request.respond(200, { messages : messages }); }

// this function helps me compare version numbers, such 1.0 and 1.2.3 function compareVersions(a, b) { var i, cmp, len, re = /(\.0)+[^\.]*$/; a = (a + '').replace(re, '').split('.'); b = (b + '').replace(re, '').split('.'); len = Math.min(a.length, b.length); for( i = 0; i < len; i++ ) { cmp = parseInt(a[i], 10) - parseInt(b[i], 10); if( cmp !== 0 ) { return cmp; } } return a.length - b.length; }

Notice how we only send messages back if the version of the client is < 1.2 (which is my new version, that will go out shortly). Now all I have to do is load the messages when the application starts up. The first thing, is to build the payload I’ll send to the server to identify the client:

NSDictionary *bundleInfo = [[NSBundle mainBundle] infoDictionary];
NSDictionary *clientDescription = @{
                                    @"version" : [bundleInfo objectForKey:@"CFBundleShortVersionString"],
                                    @"build" : [bundleInfo objectForKey:@"CFBundleVersion"],
                                    @"platform" : @"iOS",
                                    @"region" : [bundleInfo objectForKey:@"CFBundleDevelopmentRegion"],
                                    @"bundle" : [bundleInfo objectForKey:@"CFBundleIdentifier"],
                                    @"platformname" : [bundleInfo objectForKey:@"DTPlatformName"],
                                    };

Next, I ‘insert’ it into the vLoadMessages table (it’s not really going to insert, that’s why it’s a virtual table)

[self.vLoadMessages insert:clientDescription completion:^(NSDictionary *item, NSError *error) {
    if (error) {
       // do nothing, just log it using your logging methodology
       NSLog(@"On no, bad things: %@", error);
    }
    else if (item && [item objectForKey:@"messages"]) {
        // TODO - now lets display the messages
    [self displayMessageAtIndex:0 array:[item objectForKey:@"messages"]];
    }
}];

We expect an array of messages in the response, from 0...n (you never know, 1 might not be enough!)

- (void) displayMessageAtIndex:(int) index array:(NSArray *)messages {
    NSDictionary *message = [messages objectAtIndex:index];
    // Util is a simple class I created to help with showing alerts and 
    // providing block support
    [Util displayDialogWithTitle:[message objectForKey:@"title"]
                         message:[message objectForKey:@"message"]
                      completion:^{
                          // when the user clicks OK...
                          NSString *link = [message objectForKey:@"link"];
		      // if there's a link, open it up
                          if (link) {
                              NSURL *url = [NSURL URLWithString:link];
                              [[UIApplication sharedApplication] openURL:url];
                          }
                          int newIndex = index + 1;
                          if (newIndex == messages.count) {
                              [self setupWelcomeView];
                          }
                          else {
                              [self displayMessageAtIndex:newIndex array:messages];
                          }
                      }];
}

By example, here's the experience when a user tries to use this with a version older than 1.2:

upgradedoto

Check out Part II of this series

 
Josh Post By Josh Twist
3:51 PM
07 Mar 2013

» Next Post: Building your app version defense strategy &ndash; Part II
« Previous Post: Video: Overview of the Mobile Services HTTP API

Comments are closed for this post.

Posted by Nate Jackson @ 07 Mar 2013 4:26 PM
Thanks for posting your approach Josh.

I believe the real story here is even more involved. You really have no control over when the new client version will be available in the App store. So, you cannot introduce the breaking server change until a version of the client is readily available. And since you don't know when the new version of the client will be available, you actually have to make your CLIENT be able to work with a old version of the SERVER. Once you know the new version of the client is available, you introduce a breaking SERVER change and process the messages similar to how your post indicates. Then you can go back to your client code and remove the parts that were dealing with the "old" server version.

It's a hot mess supporting these clients when you don't control the deployment process!

Thanks again for posting.

Posted by Chris @ 07 Mar 2013 7:02 PM
That's only somewhat true Nate. WIth the App Store, you can choose when to release an updated version of your app. So provided you have the change to your script staged and ready to go, you can choose to have your update not released after being approved until you give the go ahead. Once you do release, as soon as you're sure it's available in the store, you could flip your table's script to "force" the update. So the real trouble is if you're seeing the update available in the stores but others aren't. Hopefully Josh's post tomorrow will give more details on how to better handle the window there where you tell them to update and the new version isn't available yet.

Posted by Chris @ 07 Mar 2013 7:15 PM
One thing you may want to add is another flag to return to the client to lock them out of the app unless they've updated. This is definitely a last measure that you'd never want to take but if you discover a serious security issue or run into a situation where you absolutely need them to update their app before they continue to access the server, it can be useful.

Posted by Jon Nehring @ 17 Mar 2013 2:52 AM
Good article, Josh...and some equally good comments. Even apps with just a local data store may have to contend with breaking changes. I think I will have my apps using a cloud service pass along the version number with requests. Here's a scenario where one might want/need to accomodate users with an older version of an app for a while: They can't upgrade because the new version requires an OS or hardware upgrade. For instance, you write an app that can run on WP7.5+ but then want to take advantage of the speech recognition in WP8. Yet existing users with older phones have paid you for a year's subscription in advance. You can't cut them loose!

Depending on the unique needs of an app, what has to be done will vary. Perhaps new tables and URLs running side-by-side with the old and a server-side script that migrates the customer's data over to the new tables when they upgrade. Whatever it is, thanks for bringing up an important consideration.

© 2005 - 2014 Josh Twist - All Rights Reserved.