Deep Linking for iOS

Deep linking is a technique that allows an app to be opened to a specific UI or resource, in response to some external event. By implementing a deep linking strategy, your app becomes more responsive and capable of navigation to arbitrary content in response to push notifications, much in the same way that URLs can link you to specific pages on the Web.

With the Actions Framework, the Urban Airship SDK now supports opening deep links from push notifications, and through Rich HTML content interactions. Because there is no one standard means of performing deep linking, this support only provides a consistent interface for triggering deep links. As an application developer, you still need to pick a deep linking strategy and perform some manual integration to be able to take advantage of this feature.

Developers reading this will likely be coming from one of two distinct backgrounds. If you are coming to deep linking for the first time, we have suggestions on implementation best practices, which includes maximizing compatibility with the deep link support in the Actions Framework. If you are developing or maintaining an app with a different deep link strategy, feel free to skip to the bottom where we will discuss ways to customize this support so that it fits with your pre-existing system.

Getting Started

The most common strategy for deep linking on iOS is to register a unique URL scheme with the operating system, so that your app can respond to requests to open URLs that map to distinct locations within your UI.

The first step here is to select a scheme, ideally unique. We recommend a vendor-specific scheme using reverse-DNS notation, such as vnd.mycompany.myapp. Next, select a unique identifier for the scheme. This will typically be the bundle identifier.

Custom schemes are registered in your application’s Info.plist file, under the URL Types section. If this section doesn’t already exist, click the + button at the top level of the plist editor to add it. You should see URL Types, as an array with one item. Item 0 should be a dictionary, containing a “URL Identifier” key. Copy your custom scheme identifier into this slot. Next, click the + button associated with the dictionary to add a “URL Schemes” section, which is another array. You can add as many custom schemes as you like to this array.

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {

    NSMutableArray *schemes = [NSMutableArray array];

    // Look at our plist
    NSArray *bundleURLTypes = [[NSBundle mainBundle].infoDictionary objectForKey:@"CFBundleURLTypes"];
    [bundleURLTypes enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [schemes addObjectsFromArray:[bundleURLTypes[idx] objectForKey:@"CFBundleURLSchemes"]];
    }];

    if (![schemes containsObject:url.scheme]) {
        return NO;
    }

    [self deepLink:url.pathComponents];

    return YES;
}

Here we’re programmatically enumerating our plist file and making sure that the URL in question conforms to our registered scheme. While not strictly necessary, checking the scheme ensures that the method will still behave as intended if it is called directly. This could be done more simply with a hard-coded string, but the above method is safer in case the scheme is changed after the fact. If we get a match, we call a deepLink method, with the path components of the URL. We’ll touch on what you might do with these components in the next section.

The Navigation Process

In any URL-based app navigation system on iOS, the path components of the URL ultimately map to the path the app must take from its eventual destination. This is most straightforward to accomplish with Storyboard-based UIs, where each path component represents a storyboard ID. For instance, a hypothetical app might navigate to its preferences screen with a URL such as vnd.mycompany.myapp://deeplink/preferences, where “preferences” is a storyboard ID representing a preferences view controller.

This part of the design process is highly contingent on the particular layout of your app. For instance, if our hypothetical app is based on a UINavigationController set as the root view controller, we could manipulate the navigation stack:

- (void)deepLink:(NSArray *)path {

    // Store valid storyboard IDs in an array to avoid exceptions at
    // runtime
    NSArray *linkableStoryboardIDs = @[@"preferences"];

    UINavigationController *navController = (UINavigationController *)self.window.rootViewController;

    [navController popToRootViewControllerAnimated:NO];

    UIStoryboard *storyboard = self.window.rootViewController.storyboard;

    NSMutableArray *viewControllers = [navController.viewControllers mutableCopy];
    for (NSString *storyboardID in path) {

        if ([linkableStoryboardIDs containsObject:storyboardID]) {
            [viewControllers addObject:[storyboard instantiateViewControllerWithIdentifier:storyboardID]];
        }
    }

    navController.viewControllers = viewControllers;
}

For legacy apps, or other apps without a Storyboard-based UI, your best bet is to use Key Value Coding to programmatically instantiate view controllers and follow a chain of presentation from the root view controller. Again, this is highly dependent on your app’s layout, but most complex UI transitions should be achievable from the root view controller by some combination of navigation stack manipulation, modal presentation, tab selection, and so on. KVC is a natural fit here, as a particular view controller can expose a property returning a child or otherwise dependent view controller, which can then be accessed at runtime using objectForKey: or objectForKeyPath:.

Customizing The Deep Link Action

If you already have a pre-existing deep linking system in place, it may already play nicely with the deep link action that comes with the Urban Airship SDK. If your application handles deep links as URLs that are registered with the OS with a custom scheme, everything should just work.

Scenarios that might necessitate customization of the deep link action would include deep linking systems that are not URL-based, or that do not use the standard app delegate callback for handling URLs. In this case, you can replace the stock deep link action by re-registering it with a custom implementation. This is easiest with an ad hoc block-based approach, as shown below:

UAAction *myDeepLinkAction = [UAAction actionWithBlock:^(UAActionArguments *args, UAActionCompletionHandler *handler){
    // Perform the deep link here
    [MyDeepLinkHandler handleDeepLink:args.value];

    // Call the completion handler with the desired result.
    // This example assumes we will be passing an empty (nil) result value,
    // but if a non-nil result value
    // is desired, make sure it's a JSON-serializable type (string,
    // dictionary, number, etc) so that it can
    // be properly bridged in JavaScript callbacks.
    handler([UAActionResult emptyResult]);
} acceptingArguments:(BOOL)^(UAActionArguments *args){
    // It's not a good idea to open deep links in the background
    if (arguments.situation == UASituationBackgroundPush) {
        return NO;
    }
    // Do any necessary argument value validation here, tailored to your
    // needs.
    // This example assumes we'd be taking either strings or URLs as
    // argument values.
    return (BOOL)([args.value isKindOfClass:[NSString class]] || [args.value isKindOfClass:[NSURL class]]);
}];


// Update the the deep link action in the registry with myDeepLinkAction
[[UAirship shared].actionRegistry updateAction:myDeepLinkAction forEntryWithName:kUADeepLinkActionDefaultRegistryName];

In the above example, we’re defining a new action that accepts either strings or URLs as their argument value, and that will not run in the background. In the main block, we’re passing the argument value to a hypothetical deep link handler class, and then calling the completion handler with an empty result.

Updating the deep link action in the registry with this ad hoc definition allows us swap out the action implementation at runtime, preserving existing predicates and triggering logic.

Note: The default predicate for the deep link action further restricts its execution to the UASituationLaunchedFromPush and UASituationWebViewInvocation only. While it is technically possible to deep link from a foreground push, in most cases doing so would be bad practice from a UX perspective. Updating an action in the registry as shown above preserves existing predicates, so ordinarily there should be no cause for concern unless you are also customizing the predicate, in which case you should take special care to ensure that the new predicate restricts the action to a sensible subset of execution contexts.

Deep Linking to App Settings

As of IOS8, it is possible to create a deep link directly to your app’s settings page within the iOS system settings. This can be accomplished by creating a deep link with the following URL template:

app-settings: