Messages App/App Extension (iOS10) with React Native

For a developer with a strong React Native background, coding in Objective-C or Swift is more time consuming. Fortunately, it’s easy to integrate a React Native view inside an iOS project so you can develop apps using a technology you already know.

This article will focus on the integration specifically for an iOS 10 Messages App/App Extension. App Extensions don't behave exactly the same way as full apps and therefore have some differences in the setup.

The first step is to install Xcode 8. The beta is available here. All the examples will be in Objective-C given that the React Native code base is using Objective-C.

If you want to run on your device directly you will also need to install iOS10 on your phone (usage of the force might be required):

installing iOS10 Beta

Setup the Project (Obj-C)

  1. Create a new iMessage Application project in Xcode, and make sure you select Objective-C as the language.
iMessage Application Project Creation in Xcode 8

You should get a template project with a MessagesViewController.

2. The next step is to use npm to install React and React Native dependencies. To create your initial package.json just run npm init.

Then make sure you add the following script and dependencies.

"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"dependencies": {
"react": "~15.2.0",
"react-native": "^0.30.0"
}

3. Now run npm install.

4. Now that we have the basic dependencies installed we're going to need Cocoa Pods. If you don't want to use Pods you can follow this procedure and jump to step 7 instead. If you don't have Cocoa Pods installed just run sudo gem install cocoapods

5. Now that you have Cocoa Pod installed, we need to create the pod file. To do that, in the same folder as your .xcodeproject, just run pod init.

6. The only pod you need to integrate React Native in your project is the React pod. Since we have that dependency installed from npm, we can just point pods to it. Each part of react-native that you use needs to be specified in the subspecs section. For more information about the subspecs see the React Native documentation. Here is an example with basic support for text images and dev tools:

pod 'React', :path => './node_modules/react-native', :subspecs => [
'Core',
'RCTText',
'RCTNetwork',
'RCTWebSocket',
'RCTImage',
]

Make sure you add that to the MessagesExtension target and not to your main app target.

7. Once this is done, install the pods using pod install. Once the installation complete, close your current Xcode project and open the .xcworkspace that pod just created!

8. Add the React Native minimal App transport security to your project MessagesExtension info.plist. Without this, the development server will not work!

<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>

Now go in the default MainInterface.storyboard and remove the "Hello Word" that is there by default.

Rendering your first React Native view!

In MessagesViewController.m create a function to render the rootView. Below is an example that we use.

First, there is a function that lazy initializes the bridge. This will run only once when the app starts. The render function will reuse the same bridge every time for performance reasons.

The ReactNative view gets built when the conversation becomes active or when there are transitions to a new presentation style (compact vs expanded). This allows you to pass different props to change what is rendered or to render a different root component (Root components are registered using AppRegistry.registerComponent).

-(void) willBecomeActiveWithConversation:(MSConversation*)conversation {
[super willBecomeActiveWithConversation:conversation];
    [self presentReactNativeView:self.presentationStyle];
}
-(void) willTransitionToPresentationStyle:(MSMessagesAppPresentationStyle)presentationStyle {
[self presentReactNativeView:presentationStyle];
}
-(void) presentReactNativeView:(MSMessagesAppPresentationStyle)presentationStyle {
// If you need you can pass the presentation style to your view
RCTRootView *rootView = [
[RCTRootView alloc] initWithBridge:[self getBridge]
moduleName: @"AmazingMessageExtension"
initialProperties: @{
@"presentationStyle": @(presentationStyle),
}
];
UIViewController *vc = [UIViewController new];
vc.view = rootView;
    for (UIViewController* cc in self.childViewControllers) {
[cc willMoveToParentViewController:nil];
[cc.view removeFromSuperview];
[cc removeFromParentViewController];
}
    [self addChildViewController:vc];
vc.view.frame = self.view.bounds;
[self.view addSubview:vc.view];
    [vc didMoveToParentViewController:self];
}
-(RCTBridge*) getBridge {
static dispatch_once_t once;
static id bridge;
dispatch_once(&once, ^{
NSURL *jsCodeLocation;
#if DEBUG
jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
#else
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
bridge = [
[RCTBridge alloc] initWithBundleURL:jsCodeLocation
moduleProvider:nil
launchOptions:nil
];
});
return bridge;
}

Initial Component

Create index.ios.js

import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
ScrollView,
NativeModules,
} from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
class AmazingMessageExtension extends Component {
render() {
return (
<ScrollView style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit index.ios.js
</Text>
<Text style={styles.instructions} onPress={() => NativeModules.DevMenu.reload()}>
Press Me to Reload!
</Text>
</ScrollView>
);
}
}
AppRegistry.registerComponent('AmazingMessageExtension', () => AmazingMessageExtension);

Running the beast!

Now run npm start and run the project from Xcode! It should build and show you the Hello World!

Hello Word!

Nice to Know

  • Not all components are available. DevMenu won't open in an app extension. While they can actually open from Messages App Extension, the support is very partial and React Native doesn't support it out of the box.
  • If you want to reload the code, you can use NativeModules.DevMenu.reload() (the easiest way is probably to make a small button that overlays in the corner in dev to reload the app). For hot reload, live reload and remote debugging you currently need to use a fork of react-native (or apply the patch yourself on the code).See https://github.com/facebook/react-native/pull/9242
  • LinkingIOS module to webview won't work out of the box and you have to write a function yourself that uses the following code:
[self extensionContext openURL:URL completionHandler:nil];

The extensionContext is accessible from the MessagesViewController.

Edit: Linking works in Beta4… but doesn't work anymore in Beta5 — it seems to be a bug in the new release.


Building for Production

To do a production build, you need to add a build step. Go to your project → Build Phases. Press the plus at the top and select new Run Script Phase

New Run Script Phase

Then add the following script

export NODE_BINARY=node
../node_modules/react-native/packager/react-native-xcode.sh

This script will run when you archive the project and add main.jsbundle to the application.


We built our first iMessage Application using React Native! It's going to be available as soon as the store opens. Check it out here: http://www.vooplan.com/.