One of the many big surprises at WWDC this year was the news that iOS 8 and Mac OS X 10.10 would support app extensions, which give developers a variety of new ways in which data and services can be shared between apps.
One of these new types of extension, called a Share extension, essentially allows apps that generate user data provide content to apps that publish user data. As a developer whose main app, MarsEdit, is a blog-publishing tool for the Mac, you can imagine how this might be of interest to me.
The first thing I noticed upon digging into app extension development is that it’s fairly fiddly to get set up, and the default build, run, invoke iteration loop is fraught with administrative overhead.
First of all, when you add a Share extension target to an existing project, you’ll discover that the new target can’t be run directly. To test or debug it, you have to invoke the Share extension from some app that supports … sharing. There are many default apps on the Mac and iOS that support sharing data of various formats. For example on Mac OS X the system Preview app makes a canonical sharing source for photo data, while the Notes app is one of many suitable test apps for sharing text. If you don’t do anything special after adding a Share extension target to your project, the auto-generated Xcode scheme will be configured to ask every time you run:
This will get old fast, and 99% of the time what you’ll want to run is exactly the same dang app you just ran. Luckily, this is easily addressed in the scheme settings, where you can change the “Executable” selection in the “Info” section of the “Run” tab from “Ask on Launch” to a specific application of your choosing.
That’s all well and good, but you still have to go through the tedious task of selecting some suitable content in the target app, clicking the share button, and choosing your extension from the list, before you even have the luxury of examining your own code’s behavior.
Wouldn’t it be great if you could make changes to your app extension’s appearance or behavior, click Run, and moments later be looking at your Share extension’s UI without having to do anything else? Good news: you can, and I’m about to explain how to achieve it for both Mac and iOS targets.
Automatic Invocation
The problem with all the targets on your Mac or iOS that may or may not support sharing pertinent data to your extension, is you have to walk them through the process of invoking you. When you’re deep into the development of a particular aspect of the user interface or mechanical behavior of your extension, what you really want is to repeatedly invoke the extension with the same, predictable kinds of data. Luckily, it’s possible on both Mac and iOS platforms for a host app to invoke sharing extensions programmatically, so you just need to choose a Run target that will predictably invoke your extension upon launching.
The only way I know to guarantee this is to write your own code that invokes the extension, and put it in a custom debugging-purposed target, or else put it into your main app as conditional code that only runs when building Debug variants and when prompted to take effect. I can imagine a sophisticated version of this approach involving a multi-faceted extension-debugging target that would be capable of fast-tracking extension invocation for a variety of data formats. But for the simplest solution, I recommend adding the ability to invoke your extension to the very app that hosts it. Then your simple test mode behavior will always coexist in the same source base as your extension code.
The way I approached this was to add an environment variable, “RSDebugAppExtension” to the scheme configuration for my app extension, which is also configured now to target the main app as its Executable target. So when I build and run the app extension scheme, may main app runs with these environment variables set:
Now in the main app’s applicationDidFinishLaunching: method, I just have to add the appropriate code for invoking the app extension’s UI. Without the platform-specific voodoo, it looks something like this:
#if DEBUG // Check for flag to invoke app extension NSDictionary* env = [[NSProcessInfo processInfo] environment]; NSString* appExtensionDebug = [env objectForKey:@"RSDebugAppExtension"]; if (appExtensionDebug != nil) { // RUN MY EXTENSION } #endif
The #if DEBUG just ensures that I will never inadvertenly ship this code in a release version of my app. The environment variable ensures I won’t be plagued by my extension’s UI appearing whenever I’m working on other aspects of my app besides the extension.
Now we just have to dig into the specifics of how to “RUN MY EXTENSION.” I’ll show you how that works for both Mac and iOS based Share extensions.
Invoking a Mac-based Share Extension
If you’re working on a Mac-based Share extension, the key to invoking it at runtime is to enlist the help of the NSSharingService infrastructure. You simply ask it for a service with your identifier, and ask it to “perform.”
NSString* serviceName = @"com.red-sweater.yeehaw"; NSSharingService* myService = [NSSharingService sharingServiceNamed:serviceName]; [myService performWithItems:@[@"Hello world"]];
Now when I build and run with the aforementioned environment variable set up, it pops up my Share extension, pre-populated with the text “Hello world.”
Invoking an iOS-based Share Extension
Unfortunately, iOS doesn’t support the same NSSharingService infrastructure that the Mac has. This seems like an area where we might be lucky to see more unification over the years, but for the time being the iOS notion of a sharing service is tied up with the existing “Social.framework” and its SLComposeViewController class. This is the interface that is supposed to be used to invoke a sharing panel for Facebook, Twitter, Tencent Weibo, etc. And luckily, although I don’t believe it’s documented, you can also pass in the extension identifier for your custom Share extension to instantiate a suitable view controller for hosting your extension:
NSString* serviceName = @"com.red-sweater.yeehaw"; SLComposeViewController* myController = [SLComposeViewController composeViewControllerForServiceType:serviceName]; [myController setInitialText:@"Hello World"]; [[self navigationController] presentViewController:myController animated:YES completion:nil];
You can see the basic gist of it is similar to the Mac, but you have to configure the view controller directly as opposed to providing a list of arbitrary input values. Also bear in mind that because the iOS-based solution above relies upon [self navigationController] being non-nil, you’ll need to add the special debugging code at a point in your app’s launch process when this is already set up, or else find another way of ensuring you can meaningfully present view controllers to your app’s main window.
Going Further
Notice that I hinge all the invocation on simply whether the RSDebugAppExtension environment variable is non-nil. Basically while I’m debugging I have to go in and change the code in applicationDidFinishLaunching to invoke the extension in the way I want. This works OK for me at least at this stage, but I can imagine down the road wanting to be able to fine-tune the behavior of how the extension is invoked. For this purpose, why not encode into the value of the environment variable itself certain clues about how to invoke the extension? It could specify for example what text should be passed the extension, or other configuration as desired.
My intent in sharing this blog post is both to point out some potentially time-saving tricks that will help those of you developing Share app extensions, but also to encourage you to look for creative ways to cut out the tedious aspects of whatever you work on. When all is said and done, the few monotonous steps during each iteration of work on a Share extension probably don’t add up to all that much in sheer time, but they take a lot of the joy and immediacy out of development, which frankly puts me in a bad mood. Clicking Run and seeing my work, including my latest changes, pop up instantly on the screen makes me feel a lot happier about my job.