This post gives an overview of the NSExtension private API and describes how to create a background process in your app that you can communicate with on-demand.

side-regular

Why would you want to do this?

Using a separate process within your app increases code isolation. If your background process crashes, your main app does not.

Does Apple use this internally?

Yes. Swift Playgrounds uses multiple background processes to compile and parse code. Get yourself a copy of the IPA for the app and take a look for yourself.

Note: In order to do this, you must use a private API in the iOS SDK. This API may change in the future. It is not suggested that you ship an app in the App Store that uses a private API.

What is NSExtension?

With the release of iOS 8, Apple introduced the ability for app developers to add extensions to their app. These extensions allow apps to present UI in areas like the system-wide share sheet or a notification center widget. Over time, they have added many other extension types that a developer can ship alongside their app. As of iOS 10, there are 19 extension types: full

When you ship an extension alongside your iOS app, it is placed in the AppName.app/Plugins directory with the name ExtensionName.appex. Each extension has an Info.plist similar to your App's, containing a unique bundle identifier and other attributes. That Info.plist has a key called NSExtension with a Dictionary value. Here are some of the possible keys in that dictionary and what they mean:

  • NSExtensionAttributes - Various information about the app extension, including an NSExtensionActivationRule, which tells the system in which circumstances it should launch your extension
  • NSExtensionPointIdentifier - The differentiating type of the extension. This one could be something like com.apple.Safari.content-blocker, com.apple.keyboard-service,
  • NSExtensionPrincipalClass - The main class of the extension. Must be something that conforms to the NSExtensionRequestHandling protocol.

You can find the full list of possible keys and values in the NSExtension dictionary here.

Extensions run in their own process & sandbox.

This is important to remember. Because of this no extension can access any files in the main app's sandbox. They are completely isolated from each other. You can create a shared container for reading/writing files by putting your app and its extensions in an App Group.

If the extensions and apps are isolated and separate processes, how do they communicate?

NSExtension is built on top of NSXPCConnection. On iOS, NSXPCConnection is also a private API, but it is available for developers on macOS. NSExtensions don't have a main method, instead, they have an _NSExtensionMain method, which takes care of the communication with the host app, instantiation of the NSExtensionContext and NSExtensionPrincipalClass defined in the Info.plist, and general life cycle of the extension.

How do you pass objects between a host app and an extension?

An NSExtensionContext contains an array of NSExtensionItem objects under the property inputItems. Each NSExtensionItem has a property called attachments, which is an NSArray. Below are a list of valid item types you can put into the attachments array of an NSExtensionItem.

  • NSArray
  • NSString
  • NSValue
  • NSNumber
  • NSData
  • NSDate
  • NSNull
  • NSURL
  • NSUUID
  • NSError
  • NSExtensionItem
  • UIImage

If you need to pass another object between the app and its extension, serialize the object by implementing the NSCoding protocol and using a NSKeyedArchiver to archive it into NSData, which is a valid type to pass through.

Creating a custom extension and communicating with it yourself

Apple has not yet opened up a public API to do this, so doing this is completely unsupported. However, Apple does use this for at least one of their own apps (Swift Playgrounds), so it is production-ready.

Interface

Below is the interface of the NSExtension class. I have only listed the methods needed to get a simple communication stream going between an extension and the host app, and there are many more properties and methods.

@interface NSExtension : NSObject

+ (instancetype)extensionWithIdentifier:(NSString *)identifier error:(NSError **)error;

- (void)beginExtensionRequestWithInputItems:(NSArray *)inputItems completion:(void (^)(NSUUID *requestIdentifier))completion;

- (int)pidForRequestIdentifier:(NSUUID *)requestIdentifier;
- (void)cancelExtensionRequestWithIdentifier:(NSUUID *)requestIdentifier;

- (void)setRequestCancellationBlock:(void (^)(NSUUID *uuid, NSError *error))cancellationBlock;
- (void)setRequestCompletionBlock:(void (^)(NSUUID *uuid, NSArray *extensionItems))completionBlock;
- (void)setRequestInterruptionBlock:(void (^)(NSUUID *uuid))interruptionBlock;

@end

Implementation

Your first order of business is adding a new target to your project. Pick "Action Extension" and say the type is "No User Interface". This will create a new target, Info.plist file, and an NSExtensionRequestHandling class. Delete the contents of that class and the JavaScript file that is created.

Info.plist setup

First up, let's make some changes to the Info.plist of the extension.

full

  • NSExtensionPrincipalClass - The name of the class that conforms to the NSExtensionRequestHandling protocol.
  • NSExtensionPointIdentifier - One of the following:
    • com.apple.app.non-ui-extension - One process will handle every request that is sent.
    • com.apple.app.non-ui-extension.multiple-instances - One process will be spawned for each request that is sent.
  • NSExtensionActivationRule - FALSEPREDICATE, meaning it is only activated when we tell it to activate manually.

Creating your NSExtension programmatically

The following creates an instance of the extension, given the extension's bundle ID. Then, it adds blocks to get called when certain things happen to the extension.

NSError *error;
NSExtension *extension = [NSExtension extensionWithIdentifier:@"com.example.App.AppExtension" error:&error];

	// This block will be called if the extension calls [context cancelRequestWithError:]
[extension setRequestCancellationBlock:^(NSUUID *uuid, NSError *error) {
    NSLog(@"Request %@ cancelled. %@", uuid, error);
}];

// This block will be called if the extension process crashes or there was an XPC communication issue.
[extension setRequestInterruptionBlock:^(NSUUID *uuid) {
    NSLog(@"Request %@ interrupted.", uuid);
}];

// This block will be called if the extension calls [context completeRequestReturningItems:completionHandler:]
[extension setRequestCompletionBlock:^(NSUUID *uuid, NSArray *extensionItems) {
        NSLog(@"Request %@ completed. Extension items: %@", uuid, extensionItems);
}];

Sending a request to the NSExtension

Once you have created the NSExtension, then you can send requests to it. Each request contains an array of NSInputItem objects, which you can create to pass data to the extension.

NSExtensionItem *item = [[NSExtensionItem alloc] init];
	[item setAttachments:@[@"input string"]];

	[extension beginExtensionRequestWithInputItems:@[item] completion:^void (NSUUID *requestIdentifier) {
	    int pid = [self.extension pidForRequestIdentifier:requestIdentifier];
    NSLog(@"Started extension request: %@. Extension PID is: %i", requestIdentifier, pid);
	}];

Each request is given a UUID when it is created. This is given to you as a parameter to the completion block of beginExtensionRequestWithInputITems:completion:. The UUID is passed into the requestInterruptionBlock, requestCancellationBlock and requestCompletionBlock when there is an error with the request or the extension completes it. Hold on to that UUID if you need to identify a response to a request.

Sample Project

side-mini

I've uploaded a working sample project, which you can download and play around with. It has the NSExtension capitalize a string and return it back. It also shows how to serialize a custom object and pass it between the host app the NSExtension.

Download that sample project here.