Custom App URLs

Have you ever thought “I wish there was a way to click a link in an email and launch my app…” Well there is, through the magic of custom app URLs. The basic idea is this: register a custom URL protocol with the iPhone, and if someone clicks a link with that protocol on that device, it will launch your app. But wait, there’s more: because it’s a URL, you can pass parameters into the app on launch. If you’re a C programmer, this is like being able to pass command line parameters into the application as an argv list.

I first started looking into this in detail while working on LandFormer. In LandFormer, I wanted a nice and simple way for users to share levels they had created without having to use a 3rd party social gaming system or Facebook. I started looking into custom URLs and discovered it could do everything I wanted. Using these URLs, I can encode a level’s data as a URL, embed it in an email, and let the user send an email to their friend. When the friend opens the email on their iPhone and clicks the link, LandFormer is launched, it parses the URL, and loads the level. It’s simple, elegant, and it’s easy to set up once you know where to look. So let’s do that now.

Define a Protocol

The first thing you need to do is decide on a custom URL protocol for your app. Choose something that other apps aren’t going to use. Don’t pick http, because that’s already taken. Pick something related to your app name, or website URL (e.g. “myAwesomeApp://”).

Once you’ve picked your custom protocol, you need to tell your app to register it with the phone. You do this through the app’s Info.plist file. Open the plist file in Xcode. Add a new row of type “URL types”. When created it will contain one item by default, “Item 0”. Expand Item 0 and you’ll see one element inside called “URL identifier”. This is an identifier that must be unique. Use the reverse domain naming system that Apple recommends (e.g. “com.mycompany.myawesomeapp”). Now add a new item to the array under the URL identifier row. Set the new item to “URL Schemes”. This creates an array with one item in it, “Item 0”. Set Item 0’s value to be your custom URL protocol (e.g. “myAwesomeApp”). Save the plist.

Setting up the custom URL in the Info plist

Great! Now you’ve told the iPhone that when a URL starts with “myAwesomeApp://” that it should launch your app. If that’s all you want to do, you’re done! Go have a soda. However, this method allows us a lot more power over how we launch the app, so let’s see what we can do with it.

Handling a Launch URL

When your app is launched with a custom URL, you have access to that string when the app launches. This means that you can take different launch actions based on what’s in that string. Let’s take a look at how to do that.

Right now in your app you probably have a function in your app delegate that looks like this:

- (void)applicationDidFinishLaunching:(UIApplication *) application
{
    // Do some launch stuff
}

That’s the application where you’d set up your connections between your window and your root viewcontroller, etc. However, in order to handle that custom URL, we need to handle things a bit differently. There are two options.

1) Leave your code in applicationDidFinishLaunching: and handle the URL in a later call to application:handleOpenURL:

2) Replace your applicationDidFinishLaunching: method with application:didFinishLaunchingWithOptions:

[EDIT (2010-07-30): If you want your app to handle URLs when your app is in the background in iOS 4, you must use handleOpenURL. When an app regains focus because the user clicked a custom URL, you will not get a call to application:didFinishLaunchingWithOptions, but you will get a call to application:handleOpenURL.]

I’m going to talk about the 2nd method, since I’m assuming we’re in a situation where we want to set up a different initialization sequence based on the URL passed in. So lets replace our applicationDidFinishLaunching: method with this:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Do our launch stuff here now
}

Notice that we now get an extra parameter passed to us in the form of an NSDictionary of launch options. One of these options is our launch URL, so let’s fish that out:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Get our launch URL
    if (launchOptions != nil)
    {
        // Launch dictionary has data
        NSURL* launchURL = [launchOptions objectForKey: UIApplicationLaunchOptionsURLKey];
 
        // Get the part of the URL after the ://
        NSString* queryString = [launchURL query];
 
        // Escape the string
        NSString* escapedQuery = [queryString stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
 
        // Parse the URL string
        ...
    }
 
    // Do our launch stuff here now
}

So all I’ve done there is grab the URL out of the dictionary. The call to [launchURL query] grabs the part of the URL after the ://. So if the URL was:

myAwesomeApp://?myString=test&myInt=0

then calling -query on the URL returns:

myString=test&myInt=0

The call to -stringByReplacingPercentEscapesUsingEncoding will convert all the URL-safe characters (like %20 for a ‘space’ character) back into the proper ascii char (or in this case, UTF8 char).

Parsing the data can then be done with your favourite parsing method (in LandFormer I used NSScanner to parse each element). You could also parse the data and store the vars into an NSMutableDictionary so that you could then do key-based lookups on the data.

After that, it’s up to you how you want to use that data. You can make decisions about how you want the game to load.

The LandFormer Example

To put this into perspective, the LandFormer URL looks like this for a created level that someone is mailing to a friend:

landformer://?title=Over%20The%20Hills%20And%20Mountain&moves=13
    &level=3343333333141222311002000
    &check=13F24CFE848198A8BDB8196494904B16

So you can see that the URL is made up of 4 variables: title, moves, level, and check. The check is a checksum done on the data to make sure that the URL hasn’t been messed around with (so as to stop players from sending their friends impossible levels). The rest of the data is self-explanatory.

When the game loads, if a URL is found, it parses the data to get all the information it needs to load that level, then lets the user start playing the level. So if the user has loaded the game directly from clicking on a link, they aren’t taken to the Main Menu, they’re taken straight into the game so they can play their friend’s level.

Cool Side-Effects

One of the really cool side-effects of this method for sharing levels is that these links work from inside a web browser, email, or even a twitter client. So if someone creates a level, sure, they can send it to a friend via email inside the game. But, they can also collect level URLs they’ve created and create their own level packs by posting the links to their website. Then anyone can click those links to play their levels. They can also share the links on twitter, and anyone browsing using an iPhone twitter client (who also has the game), can just click the link to download the level. The possibilities are endless!

Another cool potential use for this would be to allow you to pass data between two of your own apps. Say you make a new game. You could register a custom URL protocol for it, then add a special link inside your old game that allows users who have that game to click it to unlock something in the new game.

A Word on Error Handling

So far everything I’ve mentioned is all of the “look how awesome this is” variety. However, it’s important to mention code security. Any time you allow the user access to data that affects your game, you need to be careful. When parsing the URL coming into your app, make sure you’re handling failure cases well. You don’t want your app to crash if someone starts messing around with the URL manually. You don’t want to give them access to your app’s memory. Be careful with how you parse that data, make your error checking and handling robust, and you should be fine.

Now go have fun!

Owen