At Curalate we build a mobile app that allows our clients to use our services on-the-go. It provides an ever-evolving subset of features from our web experience and we ship updates every month or so, but we don’t have any engineers that specialize in mobile development on our team! Ionic enables us to leverage the expertise of our full-stack engineers to ship a product on mobile using client technology that we’re already familiar with - AngularJS. We write well-factored JavaScript in Angular services once for the web, layer on unit tests, then leverage that same code to build an iOS app.

Fast forward to this summer - the Curalate dev team moved fast to launch Tilt to enable brands to create shoppable channels of vertical video. As a prerequisite, we wanted to make it easier for users to share images and videos with Curalate from any app by releasing an iOS share extension. We started looking for documentation about how to do this with Ionic but found that it was not supported as a first-class scenario. This is one area where Ionic doesn’t make things easier, but by sharing a guide to shipping an iOS share extension for an Ionic app we hope it’ll encourage more developers to try it.

Code Native

Since Ionic doesn’t have first class support for share extensions (iOS or otherwise), we have to roll up our sleeves and delve into native platform development to get the job done. Here’s how:

1. Generate a Share Extension with XCode

The easiest way to get started is to open the .xcodeproj generated by the “ionic platform add ios” command. From XCode, follow the instructions provided by Apple here to generate the boilerplate for a simple Share extension.

2. Choose Supported Content Types

Next, edit your extension project’s .plist file to declare the types of files you want to handle. For our app we enabled sharing images and videos using the following config:

<key>NSExtension</key>
<dict>
    <key>NSExtensionAttributes</key>
    <dict>
        <key>NSExtensionActivationRule</key>
        <dict>
            <key>NSExtensionActivationSupportsImageWithMaxCount</key>
            <integer>10</integer>
            <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
            <integer>10</integer>
        </dict>
    </dict>
</dict>

3. Share Authentication with your App

We require users to be authenticated with their Curalate account before they upload. We used iOS keychain sharing (described here) by wrapping what we learned from this StackOverflow post in a Cordova plugin. Whenever a user logs in or out of the Curalate mobile app we update the information securely stored in iOS keychain, then retrieve that information when our iOS share extension is launched.

4. Create an App Group

Images and videos can be quite large, so supporting background upload was a priority for us. Per documentation here, iOS has a restriction that background uploads must create a shared container - here’s how.

5. Upload All-The-Things

Now that you’ve got an App Group and you’ve retrieved auth information from Keychain Sharing, let’s upload the files that your user wants to share - here’s the relevant excerpt from the code we used to configure the background upload:

#define kAppGroup @"group.com.myapp"

- (void)uploadFileAtPath:(NSURL *)filePathURL fileName:(NSString *)fileName {
    // Retrieve the authentication headers from iOS Keychain
    NSDictionary *httpHeaders = [SimpleKeychain load:@"com.myapp.keychain"];
    [config setHTTPAdditionalHeaders:httpHeaders];

    // Form the HTTP request
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://app.myapp.com/upload"]];
    [request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
    [request setHTTPShouldHandleCookies:NO];
    [request setTimeoutInterval:120];
    [request setHTTPMethod:@"POST"];
    [request setValue:fileName forHTTPHeaderField:@"fileName"];
    [request setValue:@"1" forHTTPHeaderField:@"basefile"];
    [request addValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
    
    // Configure and execute a background upload
    NSString *appGroupId = kAppGroup;
    NSString *uuid = [[NSUUID UUID] UUIDString];
    NSURLSessionConfiguration *backgroundConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:uuid];
    [backgroundConfig setSharedContainerIdentifier:appGroupId];
    [backgroundConfig setHTTPAdditionalHeaders:httpHeaders];
    
    self.backgroundSession = [NSURLSession sessionWithConfiguration: backgroundConfig delegate:self delegateQueue: [NSOperationQueue mainQueue]];
    NSURLSessionUploadTask *uploadTask = [self.backgroundSession uploadTaskWithRequest:request fromFile:filePathURL];
    
    [uploadTask resume];
}

With that your upload is underway!

In our initial testing we noticed some flakiness in uploads completing, and eventually sourced the issue back to the fact that we needed to implement handleEventsForBackgroundSession within our app. You don’t have to do anything special, just add this to your AppDelegate.m.

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
    completionHandler();
}

Optimize Your Development Workflow

All of this is great, until you re-run “ionic platform add ios” and and it blows away all of the things you’ve just configured in XCode :( We haven’t found a good way to auto generate this stuff yet, so for the time being we’re checking in our iOS extension code and manually enabling the app group and keychain sharing capabilities whenever we need to re-add the iOS platform.

That said, we looked for a few solutions that didn’t ultimately pan out, these included:

  • Declaring the iOS app capabilities in Cordova’s config.xml or similar, namely:
    • Push Notifications
    • Keychain Sharing
    • App Groups
  • Configuring Ionic/Cordova to reference existing code for our iOS extension. Unfortunately the .xcodeproj structure contains a lot of generated interlinked keys that prevented us from going this route.
  • Using the Xcodeproj CocoaPod to generate our iOS extension from checked in source files. This looked promising, but ddoesn’t support Share extensions.

We’re continually looking for improvements to our development process, and will post updates here for any that we find!