Custom URL Scheme
20 November 2014
iOS 8 ushered in a new era of inter-app sharing and collaboration with its app extensions. Prior to iOS 8, apps were pretty much limited to using custom URL schemes for inter-app communication. In the same way that Apple provides support for web pages, email, telephony and text messaging via the http, mailto, tel and sms URL schemes respectively, apps can expose an automation interface by declaring their own custom protocol, also known as a URL scheme.
The idea is pretty simple: when a URL that uses the app’s custom protocol is accessed, the app launches and performs the action encoded within the URL. What the action does is entirely down to the developer of the app. It’s even possible to test these custom URLs using nothing more than Mobile Safari.
For Daily Offers 1.3.1 released in August 2013, I added support for the following custom URL scheme:
- ukdailyoffers:// —launch the app, returning to the previous screen
- ukdailyoffers://asda —show Asda offers
- ukdailyoffers://sainsburys —show Sainsbury’s offers
- ukdailyoffers://tesco —show Tesco offers
- ukdailyoffers://drinks —show drink offers
- ukdailyoffers://search?term=<text>&order=<alpha | price> —search for <text>, ordered alphabetically or by price
The DailyOffers-Info.plist file includes the XML snippet below that registers this ukdailyoffers URL scheme:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.sideprojectsoftware.ukdailyoffers</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ukdailyoffers</string>
</array>
</dict>
</array>
When another app such as Launch Center Pro fires off a conforming URL (e.g. ukdailyoffersapp://search?term=half+price), the app delegate’s application:openURL:sourceApplication:annotation: method is called. Here’s the body of that method within the SPSAppDelegate class implementation:
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
SPSURLSchemeController *controller = [[SPSURLSchemeController alloc] initWithURL:url];
[controller pushSupermarketsViewController];
self.window.rootViewController = controller.rootViewController;
[self.window makeKeyAndVisible];
return [controller launch];
}
As you can see, the SPSURLSchemeController class does most of the work. The incoming URL is passed to its initWithURL: method. Here’s the header file code for SPSURLSchemeController:
//
// SPSURLSchemeController.h
// Daily Offers
//
// Created by John Topley on 03/08/2013.
// Copyright (c) 2013 John Topley. All rights reserved.
//
@interface SPSURLSchemeController : NSObject
@property (weak, nonatomic, readonly) UINavigationController *rootViewController;
- (instancetype)initWithURL:(NSURL *)url;
- (void)pushSupermarketsViewController;
- (BOOL)launch;
@end
As well as the initializer method, there are two other methods. From the user’s perspective, invoking a custom URL appears to jump to a screen in the “middle” of the app without first having to navigate through the screen hierarchy. This means that the view controller stack has to be set up accordingly behind the scenes, which is what the pushSupermarketsViewController method does (the supermarkets screen is the first screen in the app and hence the first view controller).
The launch method is where the real work happens, as it invokes one of two methods exposed by the SPSSupermarketsViewController class, depending on the action that’s been requested; but more about those later. The implementation of the initWithURL: and pushSupermarketsViewController methods is shown below:
//
// SPSURLSchemeController.m
// Daily Offers
//
// Created by John Topley on 03/08/2013.
// Copyright (c) 2013 John Topley. All rights reserved.
//
#import "SPSURLSchemeController.h"
#import "SPSSupermarket.h"
#import "SPSSupermarketIdentifiers.h"
#import "SPSSupermarketsViewController.h"
#import "SPSSearchQuery.h"
#import "SPSUserDefaults.h"
static NSString *const kMainStoryboard = @"MainStoryboard";
static NSString *const kSupermarketsViewController = @"SupermarketsVC";
@implementation SPSURLSchemeController {
NSURL *_url;
NSString *_path;
SPSSupermarketsViewController *_supermarketsViewController;
}
- (instancetype)initWithURL:(NSURL *)url
{
if ((self = [super init])) {
_url = url;
_path = [[url absoluteString] lastPathComponent];
}
return self;
}
- (void)pushSupermarketsViewController
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:kMainStoryboard bundle:nil];
_rootViewController = [storyboard instantiateInitialViewController];
_supermarketsViewController = [storyboard instantiateViewControllerWithIdentifier:kSupermarketsViewController];
[self.rootViewController pushViewController:_supermarketsViewController animated:NO];
}
...
@end
As previously noted, when writing the code for Daily Offers my style was to assign values to instance variables directly rather than using properties. The initWithURL: initializer method stores the URL and the path component of the URL (e.g. asda, tesco, search etc.) into a couple of instance variables for use later by the launch method.
The pushSupermarketsViewController method first obtains a reference to the main storyboard file and then instantiates the root view controller within the storyboard. Next, it instantiates the supermarkets view controller and pushes it onto the root view controller’s navigation stack. The implementation of the launch method is shown below:
//
// SPSURLSchemeController.m
// Daily Offers
//
// Created by John Topley on 03/08/2013.
// Copyright (c) 2013 John Topley. All rights reserved.
//
#import "SPSURLSchemeController.h"
#import "SPSSupermarket.h"
#import "SPSSupermarketIdentifiers.h"
#import "SPSSupermarketsViewController.h"
#import "SPSSearchQuery.h"
#import "SPSUserDefaults.h"
static NSString *const kMainStoryboard = @"MainStoryboard";
static NSString *const kSupermarketsViewController = @"SupermarketsVC";
// URL paths.
static NSString *const kDrinkPath = @"drink";
static NSString *const kSearchPath = @"search";
// Search constants.
static NSString *const kSearchQueryParameter = @"term=";
@implementation SPSURLSchemeController {
NSURL *_url;
NSString *_path;
SPSSupermarketsViewController *_supermarketsViewController;
}
...
- (BOOL)launch
{
if ([_path hasPrefix:kSearchPath]) {
SPSSearchQuery *query = [[SPSSearchQuery alloc] initWithURL:_url];
[_supermarketsViewController performSearchWithQuery:query];
// Use hasPrefix: so //drink or //drinks work.
} else if ([_path hasPrefix:kDrinkPath]) {
SPSSupermarket *supermarket = [SPSSupermarket initWithDrinkOffers];
[_supermarketsViewController loadOffersForSupermarket:supermarket];
} else if ([kAsdaKey isEqualToString:_path] || [kSainsburysKey isEqualToString:_path] || [kTescoKey isEqualToString:_path]) {
SPSSupermarket *supermarket = [[SPSUserDefaults sharedInstance] supermarketWithKey:_path];
[_supermarketsViewController loadOffersForSupermarket:supermarket];
}
return YES;
}
@end
Depending on whether the action is to perform a search, show the drinks offers, or show the offers for a named supermarket, the method routes to either the performSearchWithQuery: method or the loadOffersForSupermarket: method. There are a couple of points to note here:
- Drinks offers are treated as just another type of supermarket i.e. a collection of offers that are related in some way
- The SPSUserDefaults class (which acts as a façade to NSUserDefaults) can instantiate an SPSSupermarket model object when passed the name of a supermarket
Here are the declarations for those two methods in SPSSupermarketsViewController.h:
//
// SPSSupermarketsViewController.h
// Daily Offers
//
// Created by John Topley on 05/04/2012.
// Copyright (c) 2012 John Topley. All rights reserved.
//
#import "SPSSupermarket.h"
#import "SPSSearchQuery.h"
@interface SPSSupermarketsViewController : UITableViewController <UINavigationControllerDelegate>
#pragma mark - URL Scheme Support
- (void)loadOffersForSupermarket:(SPSSupermarket *)supermarket;
- (void)performSearchWithQuery:(SPSSearchQuery *)query;
@end
Taking the loadOffersForSupermarket: route first as it’s the simplest, the implementation simply performs a storyboard segue, passing the SPSSupermarket model object as the sender:
#pragma mark - URL Scheme Support
- (void)loadOffersForSupermarket:(SPSSupermarket *)supermarket
{
[self performSegueWithIdentifier:kShowOffers sender:supermarket];
}
The effect is the same as if the user had tapped on a row within the UITableView on the supermarkets screen to view the offers for that supermarket:
In order to understand the performSearchWithQuery: method, it’s first necessary to understand the SPSSearchQuery object that’s passed as an argument to that method. This is the header file for SPSSearchQuery:
//
// SPSSearchQuery.h
// Daily Offers
//
// Created by John Topley on 15/06/2014.
// Copyright (c) 2014 John Topley. All rights reserved.
//
#import "SPSSearchResultsOrderEnum.h"
@interface SPSSearchQuery : NSObject
@property (readonly, nonatomic, assign) SPSSearchResultsOrder searchResultsOrder;
@property (readonly, nonatomic) NSString *searchTerm;
@property (readonly, nonatomic, assign) BOOL URLSchemeSearch;
- (instancetype)initWithSearchTerm:(NSString *)searchTerm;
- (instancetype)initWithURL:(NSURL *)url;
@end
As you might have guessed from its name, the purpose of this class is to represent a user’s search for an offer or product. It uses an SPSSearchResultsOrderEnum to represent whether the search results are to be ordered alphabetically (the default) or in ascending price order:
//
// SPSSearchResultsOrderEnum.h
// Daily Offers
//
// Created by John Topley on 19/06/2014.
// Copyright (c) 2014 John Topley. All rights reserved.
//
typedef NS_ENUM(NSInteger, SPSSearchResultsOrder) {
SPSSearchResultsOrderAlphabetical,
SPSSearchResultsOrderPrice
};
SPSSearchQuery also has properties for the search term and a flag to record whether it came into life as a result of a URL scheme search or a user interface search. This flag is used by the search results view controller to determine whether to use the explicit search results ordering set on the SPSSearchQuery instance (as specified in the URL using the &order= query parameter), or the last ordering chosen by the user (which is stored as a user interface preference in NSUserDefaults).
The implementation of SPSSearchQuery is shown below:
//
// SPSSearchQuery.m
// Daily Offers
//
// Created by John Topley on 15/06/2014.
// Copyright (c) 2014 John Topley. All rights reserved.
//
#import <NSURL+QueryDictionary.h>
#import "SPSSearchQuery.h"
#import "NSString+DailyOffersAdditions.h"
// Valid query parameter keys and values.
static NSString *const kSearchOrderQueryParameter = @"order";
static NSString *const kSearchTermQueryParameter = @"term";
static NSString *const kSearchResultsOrderPrice = @"price";
@implementation SPSSearchQuery
- (instancetype)initWithSearchTerm:(NSString *)searchTerm
{
if ((self = [super init])) {
_searchTerm = [searchTerm copy];
// Default to alphabetical search results ordering.
_searchResultsOrder = SPSSearchResultsOrderAlphabetical;
}
return self;
}
- (instancetype)initWithURL:(NSURL *)url
{
if ((self = [super init])) {
NSDictionary *queryParams = [url uq_queryDictionary];
_searchTerm = [queryParams[kSearchTermQueryParameter] copy];
// Default to alphabetical search results ordering.
_searchResultsOrder = SPSSearchResultsOrderAlphabetical;
NSString *order = queryParams[kSearchOrderQueryParameter];
if ([kSearchResultsOrderPrice sps_isEqualToStringIgnoringCase:order]) {
_searchResultsOrder = SPSSearchResultsOrderPrice;
}
_URLSchemeSearch = YES;
}
return self;
}
@end
The initWithSearchTerm: initializer method is only used for interactive searches via the user interface, so I won’t discuss it further here. If you’ve a good memory, you may remember that the initWithURL: method is passed the full URL that was originally passed to the app delegate’s application:openURL:sourceApplication:annotation: method, before being passed to SPSURLSchemeController’s launch method, where our SPSSearchQuery instance was created.
An NSDictionary is created from the query string parameters to make working with them easier. This is done using the uq_queryDictionary method, which comes from Jonathan Crooke’s handy NSURL+QueryDictionary category.
The search term and search results ordering are obtained from the dictionary and assigned to the instance variables backing the properties. In this case I assign to the instance variables directly because the properties are declared as read-only, since they’re never going to change after being given initial values. Also, it’s good practice to favour immutability whenever possible.
Now that we understand the SPSSearchQuery class, we can return to SPSSupermarketsViewController’s performSearchWithQuery: method, which performs a storyboard segue, passing our SPSSearchQuery object as the sender:
- (void)performSearchWithQuery:(SPSSearchQuery *)query
{
[self performSegueWithIdentifier:kShowSearch sender:query];
}
The prepareForSegue:sender: method sets the passed SPSSearchQuery object as a property named searchQuery on the SPSSearchViewController class. This is the view controller underpinning the Search All Offers screen, where search text is entered or previously saved searches selected:
SPSSearchViewController’s viewDidLoad method checks to see if a value has been assigned to its searchQuery property. If it has, then it sets the search bar’s text to the search term within the SPSSearchQuery object and displays a user interface notification if the search term is fewer than the allowed number of minimum characters (currently three). Otherwise, it performs a storyboard segue to the search results screen, again passing the SPSSearchQuery object as the sender:
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
...
// The search query was specified directly using a URL scheme, so set the search bar text and show the search results.
if (self.searchQuery) {
self.searchBar.text = self.searchQuery.searchTerm;
if ([self.searchQuery.searchTerm length] < kMinimumSearchCharacters) {
[self showSearchTermMinimumCharactersNotification];
return;
}
[self performSegueWithIdentifier:kShowSearchResults sender:self.searchQuery];
}
}
The viewDidLoad method in SPSSearchResultsViewController.m is shown below. It checks the previously mentioned URLSchemeSearch flag on the SPSSearchQuery object. If it’s set then it assigns the view controller’s searchResultsOrder property to the value of the SPSSearchResultsOrder enum, which is also from the SPSSearchQuery instance. Otherwise, the last used search results ordering is read from the defaults system. Finally, the selected segment in the UISegmentedControl at the top of the screen is updated to reflect the correct ordering.
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
...
// The search query was specified directly using a URL scheme, so set the search results order directly, otherwise use NSUserDefaults.
if (self.searchQuery.URLSchemeSearch) {
self.searchResultsOrder = self.searchQuery.searchResultsOrder;
} else {
self.searchResultsOrder = [[SPSUserDefaults sharedInstance] searchResultsOrder];
}
self.searchResultsOrderControl.selectedSegmentIndex = self.searchResultsOrder;
...
}
Elsewhere in the SPSSearchResultsViewController class, the search term and the search results ordering are used by an NSFetchedResultsController that provides the data for the screen’s UITableView, but I shan’t cover that here as it’s not relevant to custom URL schemes. Here’s the result of it all:
We’ve covered a lot of ground in this post and indeed, it was quite a lot of effort to originally add URL scheme support to Daily Offers. That said, I think the facility is worth offering to your app’s users. The general pattern is to make sense of the incoming URL and then route through the view controller stack to the view controller appropriate for the requested action, passing along the objects needed to perform the action via view controller properties along the way.