gears

iOS App Extensions are a great way to let your users take advantage of your app’s functionality while not in your app. For example, you could add a Share Extension to allow users to quickly share a link from Safari via your social media app. Or, you could add an Action Extension to allow users of your note-taking app to annotate an article she was reading in another app.

There are many different types of extensions you could write (see this Apple Programming Guide for more examples), but we’ll limit ourselves to looking at the more general Action Extension.

Even if you’re not sure what these extensions are, you’ve probably seen them before. For example, clicking the share icon at the bottom of a page in Safari brings up a menu with a couple extensions:

Example extension types
The first row of actions are Share Extensions and the second row of actions are Action Extensions.

At the end of this post, we’ll have our React Native app listed in the second row among other Action Extensions that, when selected, will open up a view in our React Native app.

We’ll be using Ignite to get our app started, and we’ve created an example app available on Github. If you’re not using Ignite (you should be!), we also have an example app without Ignite available.

Here’s an overview of what we’re going to accomplish:

  1. Add a bare Action Extension to our app.
  2. Configure the Action Extension to be able to run the JavaScript runtime.
  3. Change the Action Extension to render our React Native view.
  4. Show a custom React Native view for our Action Extension.
  5. Hook up a “Done” button so users can return the original app.

Create an Action Extension with Ignite

Prior to this point, I’ll assume you’re working with an existing application. If not, install Ignite and generate a new app with ignite new ActionExtensionExample.

Now, we’ll switch over to XCode to add an Action Extension to our app. Begin by adding a new target to the XCode project:

Add new target

We’ll choose “Action Extension” to create our extension:

Add new Action Extension

When configuring the extension, be sure to choose “Objective-C” for “Language”, and “Presents User Interface” for the “Action Type”:

Configure Action Extension

After clicking “Finish”, you’ll have a basic Action Extension as part of your app. You can check out the example ActionViewController.m for an example of what the Action Extension can do. Don’t get too attached to that code, though, since we’ll be re-writing most of it shortly.

Configure the Action Extension for React Native

The goal of this part of the process is to make our Action Extension configuration look like the configuration for our React Native target’s configuration.

We’re using React Native version 0.38.0, so if your app’s configuration is different than what you’re seeing in this post, favor what your app’s configuration is.

To begin, link the necessary libraries with the Action Extension target. In the “Build Settings” tab of the Target configuration, add the following libraries:

Linking libraries
Note that some extra libraries are required for an Ignite application.

Next, we need to add some linker flags to allow the extension to compile correctly. Add -ObjC and -lc++ to the “Other Linker Flags” setting in the “Build Settings” tab of the target configuration:

Other linker flags

In order to allow the JavaScript bundle to be loaded, we need to add an exception in “App Transport Security Settings” to allow localhost to be loaded. In the Info.plist of the Action Extension add the following exception:

Allow localhost exception

Now’s a good time to pause and make sure that everything’s set up properly. Re-build and install the app in the simulator with react-native run-ios. The extension will be built and installed along with the app and you shouldn’t have any errors.

To make sure that the extension is installed, open Safari in the iPhone Simulator and press the “Share” button at the bottom of the screen. You won’t see your extension yet, since it first needs to be enabled. Scroll over all the way to the right in the action row until you see the “More” link:

More action extensions

You should see your new action extension at the bottom of the list:

Enable our action extension

Toggle the enabled switch and drag it up to the beginning for easier testing:

Enabled

Now when you open the share link in Safari you should see your action extension!

We did it!

Clicking on the action extension will open the default, example view controller, so let’s make that our own instead.

Show React Native View in Action Extension

As mentioned previously, the generated Action Extension included a default view controller in ActionViewController.m. That view controller implements viewDidLoad to implement the example functionality. We’re going to replace that method with a loadView method that creates an RCTRootView with our app’s JavaScript bundle (see also this commit in our example app):

1
2
3
4
5
6
7
8
9
10
11
12
- (void)loadView {
    NSURL *jsCodeLocation;

    jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];

    RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                        moduleName:@"RNActionExtension"
                                                 initialProperties:nil
                                                     launchOptions:nil];
    rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
    self.view = rootView;
}

Note that most of this code was gently lifted straight from the AppDelegate.m file in the React Native app (with nil replacing launchOptions, since the view does not accept any options).

Show Custom React Native View from Action Extension

If you were to try out your Action Extension now (after rebuilding first), you’d see the whole React Native app loaded after clicking on the action. Since the goal of an Action Extension is to provide some single piece of functionality, we need to update our app to allow showing a special view in our Action Extension.

We’ll accomplish this by passing in an initial property to designate when our app is started inside an Action Extension and render a different component based on that property. (See also this commit in our example app.)

First, update the loadView method in ActionViewController.m to pass in a dictionary with isActionExtension set to true for the initialProperties argument of initWithBundleURL:

1
2
3
4
5
NSDictionary *initialProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool: TRUE] forKey:@"isActionExtension"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                    moduleName:@"RNActionExtension"
                                              initialProperties:initialProps
                                                  launchOptions:nil];

Next, in our JavaScript code we can add a new screen to be displayed, ActionExtensionScreen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// @flow

import React from 'react'
import { Text, View } from 'react-native'
import { Metrics } from '../Themes'

// Styles
import styles from './Styles/ActionExtensionScreenStyle'

export default class ActionExtensionScreen extends React.Component {

  render () {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>Hello from our Action Extension!</Text>
      </View>
    )
  }

}

We can update the root component of our application to render this new screen when the desired prop is true. In an Ignite application, this component is in App/Containers/App.js. Update the component to check the isActionExtension prop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class App extends Component {
  static propTypes = {
    isActionExtension: React.PropTypes.bool
  }

  static defaultProps = {
    isActionExtension: false
  }

  render () {
    const component = this.props.isActionExtension ? <ActionExtensionScreen /> : <RootContainer />
    return (
      <Provider store={store}>
        { component }
      </Provider>
    )
  }
}

Now, when we rebuild the app and open our Action Extension, we should see our new view!

We did it again!

Unfortunately, though, there’s no way to exit the Action Extension without quitting Safari. Let’s fix that.

Add a “Done” button to dismiss the extension

An Action Extension can only be closed by calling completeRequestReturningItems on success or cancelRequestWithError on error on the extensionContext. Since we can’t do that directly from our JavaScript code, we need to add a new Native Module that will handle this for us.

If you’ve not written a Native Module before, check out our previous blog post for a quick crash course on writing a Native Module with React Native.

The Native Module is slightly complicated by the fact that Action Extensions do not have access to the application’s context so we have to keep track of the Action Extension’s view controller ourselves. We’ll update the ActionViewController.h to export a pointer to the view controller and set that pointer when our view is loaded. (See this commit in our example repo.)

Our ActionViewContoller.h should now look like this:

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>

@interface ActionViewController : UIViewController

- (void) done;

extern ActionViewController * actionViewController;

@end

Our ActionViewContoller.m should also include the new actionViewController pointer and set it at the end of loadView (note that some parts of the file are not included in the sample code here):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ActionViewController * actionViewController = nil;

@implementation ActionViewController

- (void)loadView {
    NSURL *jsCodeLocation;

    jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];

    NSDictionary *initialProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool: TRUE] forKey:@"isActionExtension"];
    RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                        moduleName:@"RNActionExtension"
                                                 initialProperties:initialProps
                                                     launchOptions:nil];
    rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
    self.view = rootView;
    actionViewController = self;
}

@end

We now add a new Native Module that exports a method to call the done method on the ActionViewController. Create a new file ActionExtension.h:

1
2
3
4
#import "RCTBridgeModule.h"

@interface ActionExtension : NSObject <RCTBridgeModule>
@end

And a new file ActionExtension.m:

1
2
3
4
5
6
7
8
9
10
11
12
#import "ActionExtension.h"
#import "ActionViewController.h"

@implementation ActionExtension

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(done) {
  [actionViewController done];
}

@end

The implementation of our new done method is pretty straightforward: import the ActionViewController.h so we have access to actionViewController and call it’s done method.

Make sure that the new module (ActionExtension.m) is included in the “Compile Sources” of the “Build Phases” tab in both the app and action extension target configurations:

Compile sources

Finally, we can add a button to our React Native component utilizing our new Native Module. (See this commit in our example repo.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react'
import { NativeModules, Text, View } from 'react-native'
import RoundedButton from '../Components/RoundedButton'
import { Metrics } from '../Themes'

// Styles
import styles from './Styles/ActionExtensionScreenStyle'

export default class ActionExtensionScreen extends React.Component {

  render () {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>Hello from our Action Extension!</Text>
        <View style={styles.button}>
          <RoundedButton
            text='Done'
            onPress={() => { NativeModules.ActionExtension.done() }} />
        </View>
      </View>
    )
  }
}

As it is, our Action Extension doesn’t really do anything interesting, but you can update the loadView method to pass in additional props, using self.extensionContext.inputItems.

It's so beautiful!

Now all our hard work is rewarded with an Action Extension utilizing React Native views!