Tutorial: Sharing Data Locally Between iOS Apps

AppDataSharing Header

Tutorial: Sharing Data Locally Between iOS Apps

In the sandboxed world of iOS development sharing data between applications can prove difficult. There are a number of reasons you may want your application to share data with other applications.

  • Releasing a paid app upgrade with a new SKU.
  • Moving user data to a universal binary.
  • Releasing a suite of complementary applications.
  • Partnerships with other developers.

Since iOS developers can’t share data directly through the file system, they need to find alternate solutions for their applications. Some common solutions include:

UIDocumentInteractionController

  • UIDocumentInteractionController — Allows the user to open a document in any other application that registers as being able to handle a particular document Uniform Type Identifier (UTI). The UIDocumentInteractionController has been used in the past as a means of opening a document in other applications on the device, for example, opening email attachments from the Mail app. Unfortunately, the UIDocumentInteractionController‘s UI displays only six applications. You cannot guarantee that your application will appear in the list. While the UIDocumentInteractionController has not been deprecated, the UIActivityViewController provides a more flexible replacement as of iOS 6.0.
    • Availability: iOS 3.2+
    • Pros
      • Allows sharing of common data types with a wide array of applications.
    • Cons
      • Allows control of the type of data sent to the UIDocumentInteractionController, but not the destinations.
      • Requires additional user interaction.
      • Limited number of data destinations may cause your application not to display in the list.

UIActivityViewController

  • UIActivityViewController — Allows the user to perform a number of actions with an array of data. For example they may print, email, copy, post to social media, or open in another application. You may create your own UIActivity subclasses to provide custom services to the user.

    • Availability: iOS 6.0+
    • Pros
      • Great for sharing common data types with a wide array of applications and social media.
      • Can supply an array of items for application to an activity. Objects should conform to UIActivityItemSource protocol.
      • Has the ability to set excluded activity types.
      • Paging UI allows for more data destinations than UIDocumentInteractionController.
    • Cons
      • You must define a custom activity type to restrict “Open In…” destinations of common data types.
      • Requires additional user interaction.
  • Shared Keychain Access — Allows you to securely store data to a shared keychain that other applications that are part of a suite of applications can access. All applications that share keychain access must use the same app ID prefix. For an example of shared keychain access in action. See Apple’s GenericKeychain sample code.

    • Availability: iOS 3.0+
    • Pros
      • Secure access to data.
    • Cons
      • You can only share data between applications that share a common app ID prefix.
      • The Keychain API on the iOS Simulator comes from OS X, which has different API than that of the iOS device.
  • Custom URL Scheme — Allows data to pass between applications using simple URLs.

    • Availability: iOS 3.0+
    • Pros
      • No network connection required.
      • Great for small amounts of data that you can easily encode into an escaped, legal URL.
    • Cons
      • You must encode data into an escaped legal URL. > Note: base64 encoding has seen common use turning serializable data into a string value. However, base64 strings may include characters that are invalid for use in URLs. You might consider using base64url. See Base 64 Encoding with URL and Filename Safe Alphabet for more information.
  • Web Service – Sync data through third party (e.g. Dropbox) or custom built web service.

    • Availability: iOS 2.0+
    • Pros
      • Useful for sharing and otherwise distributing large amounts of data.
    • Cons
      • Requires a network connection.
      • Web service implementation overhead.

One of the above solutions above may prove a great fit for your application, but they all leave room for another potential solution. What if you wanted tighter control over where the data goes and needed to do so while offline? You can accomplish these goals by combining custom URL schemes with a private UIPasteboard.

  • UIPasteboard + URL Scheme – Share data locally on the device between installed applications.
    • Availability: iOS 3.0+. Note: This article takes advantage of API from iOS 4.2+.
    • Pros
      • Network connection not required.
      • Limit sharing to a small number of applications.
      • Share data programmatically with minimal user interaction.
      • Specify the destination of the data.
      • Check availability of a specific destination application.
      • Launch destination application from the current app.
      • Flexibility in the size of data shared.
    • Cons
      • Not well suited to open sharing of data. Note: UIActivityViewController may be a better fit if general sharing is needed.
      • Small implementation overhead.

Pasteboard

A pasteboard allows an application to share data within the application or between applications. Most users have tacitly encountered a pasteboard while copying and pasting text from one place to another. You have three types of pasteboards available for use within your applications:

  1. General Pasteboard (UIPasteboardNameGeneral) — the general pasteboard is used for generic copy paste operations using all kinds of data.
  2. Find Pasteboard (UIPasteboardFind) — the find pasteboard is typically used for search operations. For example, holding the most recent search criteria from a UISearchBar.
  3. Custom Pasteboard — a custom pasteboard is intended for use within your own application or family of applications. You can give a custom pasteboard a unique identifier or let the system automatically generate one for you to use.

When sharing custom data objects between applications you should avoid using the general pasteboard for a couple reasons. First, when the application moves data to and from the pasteboard, the user is not explicitly initiating copy and paste operations as two distinct actions. Second, the user may have placed data on the general pasteboard for some other purpose. Perhaps they are copying and pasting a person’s name into one app and wants that data to remain on the pasteboard for later use. If the user goes to another application after you have placed data onto the pasteboard you could have inadvertently replaced or wiped out the data they were working with.

Looking at the description of the find pasteboard you can see that it just isn’t the right fit for this purpose since as it is reserved for text entered into a UISearchBar.

The custom pasteboard is best for the purpose of semi-privately sharing custom data between two applications. Custom pasteboards are identified using unique names. They can persist data beyond the application that creates them, allowing a pasteboard to hold onto data after the application is terminated or even after rebooting the device.

When writing or reading data to and from a pasteboard, you must specify a pasteboard type. Pasteboard types typically use a uniform type identifier (UTI) to identify the type of data going into and being retrieved from the pasteboard. See Apple’s UIPasteboard Class Reference documentation for additional information.

AppDataSharing Sample App

Person Editor Application UI __Person Viewer__ Application UI

To illustrate this method of using UIPasteboard to share application data, you will implement custom UIPasteboard and URL schemes in a sample Xcode project called AppDataSharing.

Download AppDataSharing Baseline Project

The sample project contains two target applications, each running in its own sandbox. The first application target, the Editor allows the user to edit information about a person, such as the person’s first name, last name, and date of birth. The second application target, the Person Viewer, displays a person’s name and date of birth in a pair of text labels. You can build and run each application by selecting the appropriate Scheme in the toolbar as shown below.

Scheme Selection

URL Types

URL types tell the system that an application can handle a particular kind of URL. For example, a terminal application might register as being able to handle a URL scheme that begins with ‘ssh’. Applications are free to define almost any arbitrary URL type. In fact, if you have the Facebook app installed on an iOS device you can see it in action by using the fb url scheme. Simply open a Safari browser and enter fb:// into the address bar. You’ll see that the Facebook app launches on the device. See Akosma.com’s list of custom URL schemes for more examples.

What happens if an application isn’t installed on the device? You can see what happens using Safari with another URL scheme. This time enter foobar:// in the address bar. If there are no applications on the device registered to handle foobar:// URLs, you will see an error message like the one pictured below.

Safari Error Message

You will use the URL handling API in this sample as a signal to indicate that one application can handle data from another as follows:

  1. You will define a custom URL type for your application.
  2. You will check to see if another application on the device can handle this custom URL type.

Adding the Custom URL Type

__Person Viewer__ Target Info

  1. Open the AppDataSharing Xcode project.
  2. With the project selected in the source list, navigate to the Person Viewer target.
  3. Select the Info tab.
  4. Click the disclosure triangle to expand the URL Types section.
  5. Click the + button to add a new URL scheme.
  6. Set the identifier attribute to match the target application identifier. In this example you set it to com.EnharmonicHQ.Viewer.
  7. Set the URL Schemes attribute to match the target application identifier. In this example you set it to com.EnharmonicHQ.Viewer.
    > Note: You are free to use almost any string for the URL scheme, but you are using the application identifier in attempt to avoid collisions with other applications that may want to use the same URL Scheme. Make sure you choose a fairly unique scheme to avoid launching the wrong application.
  8. Set the Role attribute to Viewer. This tells the system that the application can read and present items associated with the given url type, but cannot manipulate or save them. The URL Type should look like the picture below.

Configured URL Type

Build and run the Person Viewer in the simulator. You can now launch the application from Safari’s address bar by typing this URL: com.EnharmonicHQ.Viewer://.

Launching from the Person Editor

Before attempting to open a given URL, an application can make sure the system can handle the URL. This allows you to present enabled/disabled UI based on availability of another application that can handle the URL. After the Person Editor application has determined that the Person Viewer application is installed on the device and able to handle your custom URL scheme the Person Editor can launch the Person Viewer.

In the AppDataSharing Xcode project open ENHPersonEditorViewController.m and add the following line between the #import statements and the class extension to hold the custom URL scheme. We like to use the Bundle ID for this to guarantee uniqueness.

#import "ENHPersonEditorViewController.h"
#import "ENHPerson.h"

static NSString *kViewerURLScheme = @"com.EnharmonicHQ.Viewer"; 

@interface ENHPersonEditorViewController () <UITextFieldDelegate>
...

Now add a viewWillAppear: implementation as follows:

@implementation ENHPersonEditorViewController
...

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    UIBarButtonItem *actionButtonItem = [[UIBarButtonItem alloc] 
          initWithBarButtonSystemItem:UIBarButtonSystemItemAction
                               target:self
                               action:@selector(actionButtonItemTapped:)];
    NSString *urlString = [NSString stringWithFormat:@"%@://", 
                                    kViewerURLScheme];
    NSURL *url = [NSURL URLWithString:urlString];
    [actionButtonItem setEnabled:[[UIApplication sharedApplication] 
                                   canOpenURL:url]];
    [self.navigationItem setRightBarButtonItem:actionButtonItem];
}

...

Next the Person Editor application will need to launch the Viewer Application. add the following IBAction method.

-(IBAction)actionButtonItemTapped:(id)sender
{
    NSString *urlString = [NSString stringWithFormat:@"%@://", kViewerURLScheme];
    NSURL *url = [NSURL URLWithString:urlString];
    if ([[UIApplication sharedApplication] canOpenURL:url])
    {
        [[UIApplication sharedApplication] openURL:url];
    }
}

Build and run the Person Editor application. You should see an action button added to the right side of the top navigation bar. If the Person Editor application detects that another app can open com.EnharmonicHQ.Viewer:// URLs the button should enable. Tapping the action button should cause the Person Viewer application to launch.

Moving Data

Having the ability to detect the availability of another application and subsequently launch the app represents a great first step. Now its time to get to the meat and potatoes of moving data from the Person Editor to the Person Viewer.

The AppDataSharing project has a basic data model for a person in the EHNPerson object. The ENHPerson class has a dataRepresentation method you can use to quickly get an NSData object comprised of the values from the given ENHPerson object. At this point you could simply throw the data representation up onto a general pasteboard and pull the data off of the pasteboard from the Viewer application. This approach could work, but with a few potential pitfalls. What happens as the applications evolve and the data model changes? Is this the only data model object that will need sharing? Is there other binary data (e.g. image data) that needs sharing? Is there other information that you might want to add to help the destination application process the incoming data? You can address these issues and provide a bit of future-proofing by wrapping the data in a container object.

Packaging data into a container object

Packaging the data in a container object is relatively simple. First, identify what you want added to the package. Here are the items you will add to the package object:

  • Source Application Name
  • Source Application Identifier
  • Source Application Version
  • Source Application Build
  • Payload

In the AppDataSharing project create a new NSObject subclass called ENHAppDataPackage, and add it to both the Person Editor and Person Viewer targets as shown below.

Adding Data Package Class to Targets

Update the ENHAppDataPackage.h file as follows:

#import <Foundation/Foundation.h>
...

@interface ENHAppDataPackage : NSObject <NSCoding>

// Metadata
@property (copy, nonatomic, readonly) NSString *sourceApplicationName;
@property (copy, nonatomic, readonly) NSString *sourceApplicationIdentifier;
@property (copy, nonatomic, readonly) NSString *sourceApplicationVersion;
@property (copy, nonatomic, readonly) NSString *sourceApplicationBuild;

// Application Data
@property (strong, nonatomic, readonly) NSData *payload;

-(id)initWithSourceApplicationName:(NSString *)sourceApplicationName
       sourceApplicationIdentifier:(NSString *)sourceApplicationIdentifier
          sourceApplicationVersion:(NSString *)sourceApplicationVersion
            sourceApplicationBuild:(NSString *)sourceApplicationBuild
                           payload:(NSData *)payload;

+(ENHAppDataPackage *)dataPackageForCurrentApplicationWithPayload:(NSData *)payload;

@end

The above code declares several properties to hold the application data and associated metadata. All properties are marked as readonly to make sure that packages are initialized only with the designated initializer and that the package contents disallow modification from outside of the class. Next a beefy designated initializer populates all the attributes of the ENHAppDataPackage object. Finally, to make things a little easier we’ve provided a convenience method in the starter project that takes a payload data object and turns it into an NSData object.

Switch to the ENHAppDataPackage.m file, and add the following class extension just above the implementation:

@interface ENHAppDataPackage ()

// Metadata
@property (copy, nonatomic, readwrite) NSString *sourceApplicationName;
@property (copy, nonatomic, readwrite) NSString *sourceApplicationIdentifier;
@property (copy, nonatomic, readwrite) NSString *sourceApplicationVersion;
@property (copy, nonatomic, readwrite) NSString *sourceApplicationBuild;

// Application Data
@property (strong, nonatomic, readwrite) NSData *payload;
@end

@implementation ENHAppDataPackage

...

This code takes all of the properties that were publicly declared as readonly, and redeclares them as readwrite properties for internal use within the class.

Add the following method implementations to initialize the ENHAppDataObject:

-(id)initWithSourceApplicationName:(NSString *)sourceApplicationName
       sourceApplicationIdentifier:(NSString *)sourceApplicationIdentifier
          sourceApplicationVersion:(NSString *)sourceApplicationVersion
            sourceApplicationBuild:(NSString *)sourceApplicationBuild
                           payload:(NSData *)payload
{
    self = [super init];
    if (self)
    {
        [self setSourceApplicationName:sourceApplicationName];
        [self setSourceApplicationIdentifier:sourceApplicationIdentifier];
        [self setSourceApplicationVersion:sourceApplicationVersion];
        [self setSourceApplicationBuild:sourceApplicationBuild];
        [self setPayload:payload];
    }

    return self;
}

+(ENHAppDataPackage *)dataPackageForCurrentApplicationWithPayload:(NSData *)payload
{
    NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary];
    NSString *currentApplicationName = [infoPlist valueForKey:@"CFBundleDisplayName"];
    NSString *currentApplicationIdentifier = [infoPlist valueForKey:@"CFBundleIdentifier"];
    NSString *currentApplicationVersion = [infoPlist valueForKey:@"CFBundleShortVersionString"];
    NSString *currentApplicationBuild = [infoPlist valueForKey:@"CFBundleVersion"];

    ENHAppDataPackage *package = [[[self class] alloc] initWithSourceApplicationName:currentApplicationName
                                                         sourceApplicationIdentifier:currentApplicationIdentifier
                                                            sourceApplicationVersion:currentApplicationVersion
                                                              sourceApplicationBuild:currentApplicationBuild
                                                                             payload:payload];

    return package;
}

Synthesize the accessors as follows:

#pragma mark - Accessors
@synthesize sourceApplicationName = _sourceApplicationName;
@synthesize sourceApplicationIdentifier = _sourceApplicationIdentifier;
@synthesize sourceApplicationVersion = _sourceApplicationVersion;
@synthesize sourceApplicationBuild = _sourceApplicationBuild;
@synthesize payload = _payload;

To encode the entire package into a single file for placement on the pasteboard, the ENHAppDataPackage class will implement the two methods declared by the NSCoding protocol initWithCoder: and encodeWithCoder:. Add the following method implementations to ENHAppDataPackage.m:

#pragma mark - NSCoding

-(void)encodeWithCoder:(NSCoder *)encoder
{
    [encoder encodeObject:self.sourceApplicationName forKey:kENHSourceApplicationNameKey];
    [encoder encodeObject:self.sourceApplicationIdentifier forKey:kENHSourceApplicationIdentifierKey];
    [encoder encodeObject:self.sourceApplicationVersion forKey:kENHSourceApplicationVersionKey];
    [encoder encodeObject:self.sourceApplicationBuild forKey:kENHSourceApplicationBuildKey];
    [encoder encodeObject:self.payload forKey:kENHPayloadKey];
}

-(id)initWithCoder:(NSCoder *)decoder
{
    NSString *sourceApplicationName = [decoder decodeObjectForKey:kENHSourceApplicationNameKey];
    NSString *sourceApplicationIdentifier = [decoder decodeObjectForKey:kENHSourceApplicationIdentifierKey];
    NSString *sourceApplicationVersion = [decoder decodeObjectForKey:kENHSourceApplicationVersionKey];
    NSString *sourceApplicationBuild = [decoder decodeObjectForKey:kENHSourceApplicationBuildKey];
    NSData *payload = [decoder decodeObjectForKey:kENHPayloadKey];

    return [self initWithSourceApplicationName:sourceApplicationName
                   sourceApplicationIdentifier:sourceApplicationIdentifier
                      sourceApplicationVersion:sourceApplicationVersion
                        sourceApplicationBuild:sourceApplicationBuild
                                       payload:payload];
}

Next add the following method declarations to the ENHAppDataPackage.h file:

-(NSData *)dataRepresentation;
+(ENHAppDataPackage *)unarchivePackageData:(NSData *)data;

These two data helper methods will make it easy to access the archived version of the data package and then subsequently unarchive the package on the other side. Add their method implementations to ENHAppDataPackage.m as follows:

#pragma mark - Data Helpers

-(NSData *)dataRepresentation
{
    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    [archiver encodeObject:self forKey:kENHPackageDataKey];
    [archiver finishEncoding];

    return [NSData dataWithData:data];
}

+(ENHAppDataPackage *)unarchivePackageData:(NSData *)data
{
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    ENHAppDataPackage *package = [unarchiver decodeObjectForKey:kENHPackageDataKey];
    [unarchiver finishDecoding];

    return package;
}

Uniform Type Identifier

Uniform type identifiers uniquely identify a particular type of data format. For example, the UTI of a text document is public.text. The public.text UTI is just one of many UTIs that are declared system-wide in iOS. You can see a complete list of common system identifiers in the System-Declared Uniform Type Identifiers documentation.

Note: When using system-defined UTIs in your code, you should use the constants defined in UTCoreTypes.h in the MobileCoreServices framework when available, rather than the actual UTI strings.

The AppDataSharing project will package the ENHPerson object into an ENHAppDataPackage object. The ENHAppDataPackage doesn’t fit into any of the system defined UTIs, so the ENHAppDataPackage object will need a custom UTI.

Open the ENHAppDataPackage.h file and add the following between the #import and @interface:

#import <Foundation/Foundation.h>
...
extern NSString *kENHAppDataPackageUTI;
...
@interface ENHAppDataPackage : NSObject <NSCoding>

Open the ENHAppDataPackage.m file and define the UTI just above the static key definitions.

#import "ENHAppDataPackage.h"
...
NSString *kENHAppDataPackageUTI = @"com.EnharmonicHQ.AppDataSharing.DataPackage";
...
static NSString *kENHPackageDataKey = @"kENHPackageDataKey";

Apple recommends that custom UTIs use reverse-DNS notation to ensure name uniqueness between UTIs (for example, com.myCompany.myApp.myType). Since UTIs use reverse-DNS notation, only ASCII characters (A–Z, a–z), the digits 0 through 9, the dot (“.”), and the hyphen (“-”) are allowed. See the Uniform Type Identifiers Overview documentation for additional information.

Note: Any illegal character appearing in a UTI string will cause the system to reject it as invalid. Be aware that no errors are generated for invalid UTIs.

Data Sharing Controller

The AppDataSharing project needs a way to manage the process of moving the data from one app to the next. Here are the steps needed to complete the process.

  1. The source application packages the data object for sharing.
  2. The source application creates a custom UIPasteboard with a unique identifier.
  3. The source application writes package data to the custom UIPasteboard and specifies a UTI for the data.
  4. The source application launches the destination application.
  5. The destination application finds the custom UIPasteboard.
  6. The destination application reads the package data from the custom UIPasteboard.
  7. The destination application unpacks the shared data object.
  8. The destination application informs interested objects that the shared data object is available.
  9. The destination application handles the data.
  10. The destination application cleans up the pasteboard data.

Add a new NSObject subclass called ENHAppDataSharingController to the project. Add the new class to both the Person Editor and Person Viewer targets.

Open the ENHAppDataSharingController.h file and replace the boilerplate code with the following:

#import <Foundation/Foundation.h>

@class ENHAppDataPackage;

typedef void(^ENHAppDataSharingSendDataHandler)(BOOL *sent, NSError *error);
typedef void(^ENHAppDataSharingHandler)(ENHAppDataPackage *retrievedPackage, NSError *error);

extern NSString *kReadPasteboardDataQuery;

typedef enum
{
    ENHAppDataSharingErrorTypeNoApplicationAvailableForScheme = 100,
    ENHAppDataSharingErrorTypeNoPasteboardForName = 200,
    ENHAppDataSharingErrorTypeNoDataFound = 300,
} ENHAppDataSharingErrorType;

@interface ENHAppDataSharingController : NSObject

+(void)sendDataToApplicationWithScheme:(NSString *)scheme
                           dataPackage:(ENHAppDataPackage *)dataPackage
                     completionHandler:(ENHAppDataSharingSendDataHandler)completionHandler;

+(void)handleSendPasteboardDataURL:(NSURL *)sendPasteboardDataURL
                 completionHandler:(ENHAppDataSharingHandler)completionHandler;

@end

In the above code, a pair of block typedefs outline the completion handlers. The header also sets up a constant data query string and a set of error codes for passing into the completion handlers. The header also declares two class methods. One for sending outbound data packages from the source application and another to handle inbound data in the destination application.

Switch to the ENHAppDataSharingController.m file and and add the following above the @implementation.

#import "ENHAppDataPackage.h"

NSString *kReadPasteboardDataQuery = @"ReadPasteboardData";
NSString *const AppDataSharingErrorDomain = @"AppDataSharingErrorDomain"; 

When the source application creates a private pasteboard it will need to have a unique name. You could choose any unique string you wish. This project will allow the system to automatically generate and assign a unique name. There is one hitch in this idea; how does the destination application know which pasteboard name to use when it wants to retrieve the data? The solution is to pass the pasteboard name in the URL used to launch the destination application.

Sending Data

Add the following method implementation to ENHAppDataSharingController.m:

#pragma mark - URLs

+(NSURL *)sendPasteboardDataURLForScheme:(NSString *)scheme pasteboardName:(NSString *)pasteboardName
{
    NSString *urlString = [NSString stringWithFormat:@"%@://?%@#%@", scheme, kReadPasteboardDataQuery, pasteboardName];

    return [NSURL URLWithString:urlString];
}

This method generates a URL using a scheme, the predefined kReadPasteboardDataQuery string, and the pasteboard name. You can see that the url follows the following format: scheme://?query#fragment. In this string the ‘?’ denotes the start of a query and the ‘#’ marks the beginning of a fragment. This makes it easy to parse the URL in the destination application. You can place the ReadPasteboardData string in the query portion of an NSURL and the pasteboard name into the fragment portion.

Add the following method implementation to ENHAppDataSharingController.m:

+(void)sendDataToApplicationWithScheme:(NSString *)scheme
                           dataPackage:(ENHAppDataPackage *)dataPackage
                     completionHandler:(ENHAppDataSharingSendDataHandler)completionHandler;
{
    NSError *error = nil;

    // Setup the Pasteboard
    UIPasteboard *pasteboard = [UIPasteboard pasteboardWithUniqueName];
    [pasteboard setPersistent:YES]; // Makes sure the pasteboard lives beyond app termination.
    NSString *pasteboardName = [pasteboard name];

    // Write The Data
    NSData *data = [dataPackage dataRepresentation];
    NSString *pasteboardType = kENHAppDataPackageUTI;
    [pasteboard setData:data forPasteboardType:pasteboardType];

    // Launch the destination app
    NSURL *sendingURL = [[self class] sendPasteboardDataURLForScheme:scheme pasteboardName:pasteboardName];
    if ([[UIApplication sharedApplication] canOpenURL:sendingURL])
    {
        completionHandler(YES, nil);
        [[UIApplication sharedApplication] openURL:sendingURL];
    }
    else
    {
        [pasteboard setData:nil forPasteboardType:pasteboardType];
        [pasteboard setPersistent:NO];

        NSDictionary *errorInfoDictionary = @{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ %@", 
            NSLocalizedString(@"No application was found to handle the url:", nil), sendingURL]};
        error = [NSError errorWithDomain:AppDataSharingErrorDomain
                                    code:ENHAppDataSharingErrorTypeNoApplicationAvailableForScheme
                                userInfo:errorInfoDictionary];
    }

    completionHandler(NO, error);
}

This method performs the bulk of the work required to send data to the destination application.

  • First you create a unique pasteboard that will persist beyond the lifetime of the source application.
  • Then, you write the data to the new pasteboard and the system attempts to launch the destination application using your special query URL. If none of the applications installed on the device are capable of opening the URL the method clears out the pasteboard and removes the pasteboard persistence.
  • Finally, the completion handler is called back.

To verify that our application was launched with the appropriate URL, add the following method implementation to ENHAppDelegate.m:

- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication
         annotation:(id)annotation
{
    NSLog(@"Application launched with URL: %@", url);
}

Build and run the Person Editor application to load it into the simulator. Next, build and run the Person Viewer application. While the Person Viewer application is running, return to the home screen (Shift-⌘-H) and launch the Person Editor application.

Populate the Person Editor UI with a first name, last name, and date of birth. Now, tap the action button in the navigation bar to launch the Person Viewer application. In the Xcode console you will see a log message similar to the one below.

Console output of the __Person Viewer__ Application when launched via URL

Receiving Data

The application receiving the data package will next need to know how to handle your special URL in order to pull it from the correct pasteboard. Add the following method implementation to ENHAppDataSharingController.m:

+(void)handleSendPasteboardDataURL:(NSURL *)sendPasteboardDataURL
                 completionHandler:(ENHAppDataSharingHandler)completionHandler;
{
    NSString *query = [sendPasteboardDataURL query];
    NSString *pasteboardName = [sendPasteboardDataURL fragment];
    NSAssert2(([query isEqualToString:kReadPasteboardDataQuery] && pasteboardName), 
        @"Malformed or incorrect url sent to %@. URL: %@", 
        NSStringFromSelector(_cmd), sendPasteboardDataURL);

    ENHAppDataPackage *dataPackage = nil;
    NSError *error = nil;

    NSString *pasteboardType = kENHAppDataPackageUTI;
    UIPasteboard *pasteboard = [UIPasteboard pasteboardWithName:pasteboardName create:NO];
    if (pasteboard)
    {
        NSData *data = [pasteboard dataForPasteboardType:pasteboardType];
        if (data)
        {
            dataPackage = [ENHAppDataPackage unarchivePackageData:data];
        }
        else
        {
            NSDictionary *errorInfoDictionary = @{NSLocalizedDescriptionKey: [NSString stringWithFormat:
                @"%@ %@", NSLocalizedString(@"No data found on pasteboard with name:", nil), 
                pasteboardName]};
            error = [NSError errorWithDomain:AppDataSharingErrorDomain
                                        code:ENHAppDataSharingErrorTypeNoDataFound
                                    userInfo:errorInfoDictionary];
        }
        [pasteboard setData:nil forPasteboardType:pasteboardType];
        [pasteboard setPersistent:NO];
    }
    else
    {
        NSDictionary *errorInfoDictionary = @{NSLocalizedDescriptionKey: 
            [NSString stringWithFormat:@"%@ %@", 
            NSLocalizedString(@"No pasteboard found for name:", nil), pasteboardName]};
        error = [NSError errorWithDomain:AppDataSharingErrorDomain
                                    code:ENHAppDataSharingErrorTypeNoPasteboardForName
                                userInfo:errorInfoDictionary];
    }
    completionHandler(dataPackage, error);
}

This method starts out by asserting that an appropriate URL was provided. It then attempts to find the named private pasteboard and retrieve the package of data. If the system cannot find the pasteboard or if the pasteboard holds no data, an error is generated. Finally the data package (or error) is passed to the completion handler. Also note that when finished with the pasteboard this method cleans up by setting the pasteboard contents to nil.

Putting It All Together

Add the following imports to ENHPersonEditorViewController.m:

// Data Sharing
#import "ENHAppDataPackage.h"
#import "ENHAppDataSharingController.h"

Replace the actionButtonItemTapped: implementation in the ENHPersonEditorViewController.m file with the following:

-(IBAction)actionButtonItemTapped:(id)sender
{
    NSData *personData = [self.person dataRepresentation];
    ENHAppDataPackage *package = [ENHAppDataPackage dataPackageForCurrentApplicationWithPayload:personData];
    [ENHAppDataSharingController sendDataToApplicationWithScheme:kViewerURLScheme
                                                     dataPackage:package
                                               completionHandler:^(BOOL *sent, NSError *error) {
        if (sent)
        {
            NSLog(@"Data Package Sent");
        }
        else if (error)
        {
            NSLog(@"Error sending data package: %@", [error localizedDescription]);
        }
    }];
}

This action method now packages up the ENHPerson object and sends it off to the Person Viewer application. The completion handler then logs whether the package was sent successfully and will log any errors.

The project just needs to implement some handling code to receive the package data. Replace the application:openURL:sourceApplication:annotation: method implementation of ENHAppDelegate.m with the following:

- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication
         annotation:(id)annotation
{
    NSLog(@"Application launched with URL: %@", url);
    [[NSNotificationCenter defaultCenter] postNotificationName:@"ENHHandleOpenURLNotification" object:url];
    return YES;
}

The app delegate doesn’t handle the data directly, instead it posts a notification to interested objects that the application has a new URL to open. Update the ENHPersonDetailViewController.m file with the following imports and code and replace the existing viewDidLoad: implementation:

#import "ENHAppDataSharingController.h"
#import "ENHAppDataPackage.h"
...

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self setTitle:NSLocalizedString(@"Viewer", nil)];
    [self.nameLabel setText:@""];
    [self.dateOfBirthLabel setText:@""];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleOpenURL:)
                                                 name:@"ENHHandleOpenURLNotification"
                                               object:nil];
}

-(void)handleOpenURL:(NSNotification *)notification
{
    NSURL *url = [notification object];
    if ([[url query] isEqualToString:kReadPasteboardDataQuery])
    {
        [ENHAppDataSharingController handleSendPasteboardDataURL:url
                                               completionHandler:^(ENHAppDataPackage *retrievedPackage, NSError *error) {
            if (retrievedPackage)
            {
                NSData *packageData = [retrievedPackage payload];
                ENHPerson *person = [ENHPerson personWithData:packageData];
                [self setPerson:person];
            }
            else
            {
                NSLog(@"Error handling pasteboard data url: %@", [error localizedDescription]);
            }
        }];
    }
}

Build and run the Person Viewer to install the revised application on the simulator/device. You won’t see any visible changes in the Person Viewer application, at least not yet…

Build and run the Person Editor application. Set the First Name, Last Name, and Date of Birth attributes. Click the action button in the top right. The Person Viewer application should now launch and populate the user interface with data created in the Person Editor.

Security

To add a small amount of security to the project, replace the application:openURL:sourceApplication:annotation: method implementation in ENHAppDelegate.m with the following:

- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication
         annotation:(id)annotation
{
    NSLog(@"Application launched with URL: %@", url);
    if ([sourceApplication hasPrefix:@"com.EnharmonicHQ"])
    {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"ENHHandleOpenURLNotification" object:url];
        return YES;
    }

    return NO;
}

This small change checks that the URL came from an application with a matching prefix to the application identifier.

While this is not totally secure, it can prevent unintended behavior in the application. To learn more about securing data read Properly encrypting with AES with CommonCrypto by Rob Napier.

Download AppDataSharing Solution Project

Going Further

Moving data from the Person Editor application to the Person Viewer works well, but what if you launch the Person Viewer without any data to view? Use the same URL handling strategies to allow the Person Viewer to request data from the Person Editor application. Below is an outline of the key steps you will need to address.

  • Add a new URL type and scheme in the Person Editor application target.
  • Create a new query URL that uses the scheme you created for the Person Editor target.
  • In the Person Viewer application check to see if the Person Editor application is installed and is capable of handling the new scheme.
  • Enable UI based on handling capability.
  • Setup URL handler.

The project provided a small amount of future-proofing by sending some metadata along with each data package. This metadata could be used to ensure the Person Viewer is capable of handling the version of the inbound data. Extend the ENHPerson class with your custom attributes, and add appropriate handling code to the Person Viewer. You might also consider adding a payload object classname attribute to the ENHAppDataPackage for additional future-proofing where you might have multiple payload types.

UX Considerations

Having the device swap back and forth between applications can provide a jarring user experience. Therefore, we strongly recommend that you keep application switching to a minimum. If you find that your own projects need to frequently switch between applications to get the job done, please consider using an alternative solution (e.g. web service, shared keychain access, etc.).

References

  1. Apple’s Generic Keychain Sample Code
  2. Base 64 Encoding with URL and Filename Safe Alphabet
  3. UIPasteboard Class Reference
  4. AppDataSharing Baseline Project
  5. A list of some Custom URL Schemes
  6. System-Declared Uniform Type Identifiers
  7. Uniform Type Identifiers Overview
  8. Properly encrypting with AES with CommonCrypto
  9. AppDataSharing Solution Project
  10. Creative Commons BY-NC-ND License
  11. AppDataSharing Project MIT License

Thank You

Special thanks to Jim Turner for suggesting UIPasteboard as a way to move data and inspiring this article. I’ve lost count of how many beers I owe you by now.

Licenses

Article

This “Tutorial: Sharing Data Locally Between iOS Apps” article is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.

AppDataSharing Source Code

The code in the AppDataSharing sample project is licensed under MIT License.

About the Author

Dillan LaughlinGrowing up Dillan was always that kid that took things apart to see how they worked and experiment with making them better. Today he continues to tinker with electronics and is a staunch supporter of the Maker movement. Dillan has always been fascinated with things he can remotely interact with or collect sensor data from. As an avid homebrewer he has even started building an automated home brewing system that he can monitor and control from his iPhone.   After grad school in Human-Computer Interaction with a focus on Mobile UX, Dillan joined Enharmonic as an iOS and Mac developer. Dillan also likes flexing his UX experience when working on client and internally developed projects. Dillan has worked on a number of projects including gogoDocs for Google Drive, and Random House's Living Language. Dillan blogs occasionally at enharmonichq.com and can be followed on twitter or app.net.View all posts by Dillan Laughlin →

Leave a Reply

You must be logged in to post a comment.