Creating an Apple App Clip with React Native
If you’re excited about Apple’s new App Clips just released in iOS 14 and you use React Native, then you might be asking “Can React Native be used to create an App Clip?” The short answer is yes! The long answer is still yes, but there are some pitfalls to avoid. Read on to find out how to create an App Clip for a React Native app, how to deploy and run it from Test Flight, and learn about some of the problems you’re likely to encounter.
Getting started
I’m assuming you’re already set up for React Native development, so all you should need to do is install a version of Xcode that supports App Clip development. At the time of writing I’m using Xcode 12.2 beta. You’ll also need to target an iPhone or simulator with iOS 14 when testing.
If you don’t have an app that you want to create an App Clip for then you can easily create a new one with:
npx react-native init ReactNativeTest
Tip: It will be easier to get your first App Clip up and running if you start with a new basic app than it will be to add it to a large existing app.
Create the App Clip target
1) Open the project ios/ReactNativeTest.xcworkspace file in Xcode.
2) File -> New -> Target then select App Clip (This can be found most easily by typing in the search box)
3) Setup the details for your App Clip on the following screen.
The last value is important because you can only embed your App Clip in one target. A project I was working on had several targets for alpha, beta, and production releases. Don’t worry though, it’s easy enough to create additional App Clip targets later.
4) This will create a new ios/ReactNativeTestClip folder populated with fairly typical project files. At this point you should be able to run your App Clip from Xcode, but it will just show a rather unexciting blank screen and won’t be using React Native yet.
Add pods
1) Open the ios/Podfile and add the required pods for your App Clip.
2) Run pod install in your ios folder again to make sure the App Clip target has the pods it will need.
Add the magic of React Native
There are a few ways you could add the RCTRootView used by React Native to the App Clip project. You might notice the app does it in ios/ReactNativeTest/AppDelegate.m but today we’re going to modify ios/ReactNativeTestClip/ViewController.m as this only needs a few changes to get things running.
// // ViewController.m // ReactNativeTestClip // #import "ViewController.h" #import <React/RCTBundleURLProvider.h> #import <React/RCTRootView.h> @interface ViewController () @end @implementation ViewController - (void)loadView { #if DEBUG // When debugging we load the js bundle from the Metro Bundler // running on your development machine. "index" is the name of the // js file used as an entry point (don't include the extension). NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; #else // For release builds we add the js bundle to the build. // By default the js bundler will create a file called // "main.jsbundle" for us. NSURL *jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif // moduleName corresponds to the appName used for the // app entry point in "index.js" RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"ReactNativeTest" initialProperties:nil launchOptions:nil]; // Default to a white background. rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; self.view = rootView; } - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. } @end
If you try to run the App Clip now you’ll get an error in UIApplicationMain. [AppDelegate window]: unrecognized selector sent to instance. That’s because we also need to set some extra keys in the ios/ReactNativeTestClip/Info.plist file.
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> <key>NSExceptionDomains</key> <dict> <key>localhost</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> </dict> </dict> </dict> <key>UIViewControllerBasedStatusBarAppearance</key> <false/>
If you open the ios/ReactNativeTest/Info.plist file you’ll notice the above permissions are already set for the main app.
Add a build phase to start the packager
When building a debug version of the App Clip we need to ensure the packager is running to serve the App Clip the JavaScript content. We add a new build phase to do this.
1) Go to the Build Phases tab for the ReactNativeTestClip target and click the + button on the top left.
2) Select New Run Script Phase.
3) Rename the new build phase as Start Packager and drag it higher up the list; it should come just after the [CP] Check Pods Manifest.lock phase.
4) Copy the code from the corresponding Start Packager build phase in the ReactNativeTest target which is already set up for the main app.
Add a build phase to bundle the JavaScript
When building a release version of the App Clip we want to access the JavaScript content that’s bundled with the App Clip itself. We add a new Build Phase to do this.
1) Go to the Build Phases tab for the ReactNativeTestClip target and click the + button on the top left.
2) Select New Run Script Phase.
3) Rename the new phase as Bundle React Native code and images and drag it higher up the list; it should come just after the Copy Bundle Resources phase.
4) Copy the code from the corresponding Bundle React Native code and images build phase in the ReactNativeTest target which is already set up for the main app.
Crack open the champagne (or have a cup of tea)
Your App Clip is now ready to run with React Native. Give it a try. You should see the standard React Native welcome screen you’d see if running the app itself.
Hey, isn’t my App Clip the same as my app?
As an astute reader you might be wondering why we’ve created an App Clip that’s exactly the same as our app. We did this because it was a quick way of getting a React Native App Clip up and running. In reality you’ll probably want a different entry point into your App Clip. We can set this up pretty easily.
1) Open up ios/ReactNativeTestClip/ViewDelegate.m. Change the entry point for the App Clip when running a debug build by changing the value of jsBundleURLForBundleRoot from “index” to “indexclip”. We’ll create this new JavaScript file shortly.
2) Go to the Bundle React Native code and images section in the Build Phases tab of the project (we set this up earlier). Change the code to:
export NODE_BINARY=node ../node_modules/react-native/scripts/react-native-xcode.sh indexclip.js
When making a release build the bundler will now create a bundle from indexclip.js rather than the default index.js.
3) Create a new indexclip.js file in the main project directory. Try out the following code to get you started.
import { AppRegistry, View, Text } from ‘react-native’; import { name as appName } from ‘./app.json’; import React from ‘react’; /** * You’ll actually want to import the AppClip in a way similar to * what index.js does, maybe from a new AppClip.js file. */ const AppClip = () => ( <View style={{ flex: 1, justifyContent: “center”, alignItems: “center”, backgroundColor: “#BFEFFF” }}> <Text style={{ fontSize: 26, color: “#204080” }}>Hello React Native App Clip</Text> </View> ); AppRegistry.registerComponent(appName, () => AppClip);
If you run your App Clip from Xcode you should now see the following:
If you switch your target to ReactNativeTest and run that, you should still see the original React Native welcome screen. Cool, eh?
Let’s get this sucker onto TestFlight
Don’t be content sitting back and enjoying your champagne for too long as there are more challenges to conquer when you progress your App Clip to TestFlight.
To be fair this process is actually pretty straight forward for the simple App Clip we’ve made in this article, but it might not be so easy if you’re creating an App Clip for an existing app or you wait until your App Clip is completely ‘ready’ before you upload it to TestFlight.
I think uploading your app / App Clip to TestFlight is worth doing before you put too much work into your App Clip because:
1) Running the App Clip from Xcode makes it feel like you’re just running an app.
2) If you’ve broken some of the restrictions placed on an App Clip you won’t actually be warned about it until you try uploading to TestFlight.
3) The way the App Clip launches ‘in the real world’ isn’t quite the same as the way it launches from Xcode.
4) Assuming you’re going to release your App Clip to the public you’ll need to do this at some point anyway.
Signing and capabilities
To build a release version you’ll need to set up the Signing and Capabilities as you normally would for an app, but there’s a slight difference when you add an App Clip because you need to set up Signing and Capabilities for it too:
1) For the App Clip bundle identifier use the parent app’s bundle identifier with a suffix. E.g.,
Parent bundle identifier: au.com.adapptor.reactnativetest
App Clip bundle identifier: au.com.adapptor.reactnativetest.clip
You’ll need to set up this additional identifier on the Apple Developer website too.
2) Open the ios/ReactNativeTestClip.entitlements file and make sure you have the value of the com.apple.developer.parent-application-identifiers key set to the bundle identifier of the parent app.
Version, build, and deployment info
To build a release version, the App Clip must have the same Version and Build number as the app, or building the app will fail. The App Clip must also have the same devices selected in its Deployment Info as the app, or building the app will fail.
Open the General tab for the app and check that the Version, Build, and selected devices match those for the App Clip target. By default the React Native app will only have iPhone selected, but the App Clip will have iPhone and iPad selected.
App Clips need icons too
Just like your app needs icons, so too does your App Clip. You’ll need to add images for the following icons at minimum:
(120x120px) 2x iPhone App iOS 7–14 60pt
(1024x1024px) 1x App Store iOS 1024pt
If you’re missing any icons this will be picked up if you run Validate App on the archive.
Fly my pretty
If you’ve done all of the above steps then you’re ready to upload to TestFlight. For this step you actually archive then upload the app rather than the App Clip. When you created the App Clip, Xcode changed the app so that it will embed the App Clip target inside it. You can see this in the Frameworks, Libraries, and Embedded Content section of the General tab of the project.
Note that trying to upload the App Clip from an archive of the App Clip target will result in disappointment. So now that you know how to upload your app with an App Clip, give it a go!
Launching from TestFlight
There are a number of ways you can test launching your App Clip and Apple are currently working to add support for more, but arguably the easiest way is through TestFlight.
1) Once you’ve successfully uploaded your app with an App Clip (remember it will take a while in the processing step on the Activity tab) go to the Test Flight tab and select the newly uploaded build.
2) You’ll notice there’s a new App Clip Invocations section. Fill this out for one or more test invocations by entering a TITLE and URL. The title is what will be shown in the TestFlight app and the URL is what will be passed to the App Clip when it is opened (read the App Clip documentation if you’re not familiar with this).
3) Now you (or a tester) can — drum roll — launch the App Clip from TestFlight on a physical device. Just select the app and you will see a new button (or buttons) to open the App Clip.
If after all your hard work getting to this point you find that your App Clip crashes immediately when opened, don’t panic (well, not right away at least). It’s most likely because the JavaScript bundle isn’t loading correctly. Check you followed the step Add a Build Phase to Bundle the JavaScript outlined earlier.
But wait, there’s more
Get ready for rejection
I’m suspecting that like most people you don’t particularly enjoy rejection. If you’re adding an App Clip to an existing app it’s worth preparing yourself for it. Oddly (at least at the time of writing) many of the strict requirements imposed on App Clips aren’t checked until you try uploading the App Clip to TestFlight.
On the plus side, once you’ve ironed out the kinks in your App Clip and got into the swing of things, you’re unlikely to see these errors come back to haunt you.
App Clip size over 10 MB
An App Clip of 100 MB will run just fine from Xcode. It’s only after you archive it, try Validate App,upload it to TestFlight, and wait a while (perhaps a soothing cup of tea is in order here) that you will discover it has disappeared from the Activity tab you’ve been patiently watching, and an email has arrived in your inbox telling you the App Clip was too big (without so much as a hint as to how much over the size limit it was). Refer to the section 10 MB App Clip Size Limit below for how to deal with this issue.
Missing an App Icon
Missing Info.plist value — CFBundleIconNameRefer to the section App Clips Need Icons too for how to deal with this problem.
Using a prohibited permission
E.g., Invalid SDK usage — ‘requestAlwaysAuthorization’App Clips don’t allow some of the features of a standard app and if you have them set your App Clip will be rejected. Check that you’re not directly using any forbidden features, and also be wary of third-party modules that might require them. For example react-native-maps can trigger the ‘requestAlwaysAuthorization’ error if using Google as the provider. Before becoming too invested in development it would be time well spent to look up what restrictions Apple has placed on App Clips and make sure that your project doesn’t require anything that isn’t permitted.
10 MB App Clip size limit
One of the restrictions imposed on App Clips is a 10 MB size limit. This is to ensure they load quick smart, ready for the user to engage with right away. Alas 10 MB isn’t a whole lot in the current era of app development and React Native isn’t renowned for creating tiny apps.
You might be thinking of exporting an IPA to see how big your App Clip is … don’t. The 10 MB limit applies to the uncompressed app size and an IPA is compressed. The good news is you can determine the uncompressed size of the App Clip with the following steps:
1) Archive the app:
Please note that you want to archive the app target and NOT the App Clip target. The app will embed the App Clip in it and you’ll get the option to choose which of the two you’d like to export later.
Select Archive from the Product tab. Make sure you have the Release build configuration selected (from Product -> Scheme -> Edit Scheme) and the destination set to Any iOS Device (armv7, arm64) (from Product -> Destination).
2) Export:
a) Once the app has archived select Distribute App.
b) Select Development as the method of distribution.
c) Next you will be given the choice to export the App or App Clip, pick the latter.
d) Select All compatible device variants for the App thinning option on the Development distribution options screen.
e) Finish the export process.
3) Open the App Thinning Size Report.txt file in the exported folder. Look for the line that is something like this:
App size: 1.1 MB compressed, 3.2 MB uncompressed
Please note that it’s the uncompressed size that needs to be under 10 MB.
The sizes above were obtained by exporting the App Clip we created in this article (if you were wondering).
Oh my, it’s so big
Bigger is better for many things but the size of your App Clip isn’t one of them. If your App Clip is too big there are a few ways you can investigate where those precious MB are being used. One approach is to look at the built app file. By default Xcode saves your built App Clips in the hidden folder ~/Library/Developer/Xcode/DerivedData/<project name><assorted random looking characters>/Build/Products/Release-iphoneos/<target name>.app. Right click it and choose Show Package Contents. The sizes here aren’t particularly representative of the final App Clip size but they might give an indication of where your MB are being spent.
Unix executable
There will be a unix executable in the folder named after your App Clip target. If it’s too big you are probably importing too many modules in your packages.json file or pods in the ios/Podfile.
main.jsbundle
This is the file containing all your JavaScript code. If it’s too big you might have references in your App Clip code to app code that you’re not actually using in the App Clip. Modules in packages.json can also use up space here. If your bundle is too large you might be able to dig deeper with a tool such as react-native-bundle-visualizer.
Assets
You’ll probably have an assets folder. If it’s too big then check what’s in there. An unwanted file (e.g., big.png) can be removed from your project by removing require(big.png) from your JavaScript code.
Fonts
Fonts can also chew up a fair bit of your 10 MB allowance. Consider if you really need 20 different fonts in your App Clip, and remove any that you don’t from the project.
Oh launch screen, where art thou?
One potentially confusing aspect of App Clips is that there is (and isn’t) a launch screen. To clarify: by default one is created for you and if you run the App Clip from Xcode you will see it while your app loads, but when launching the App Clip in the real world (e.g., from TestFlight) then the launch screen won’t be shown. I suspect the launch process is a bit different from your regular app, so be wary of making any clever changes to optimise the App Clip launch without testing them via TestFlight, or you could be in for an unhappy surprise later.
Final thoughts
That’s all the time I have to describe my React Native App Clip discoveries for now. There’s still so much more I’d love to explore, refine, and clarify in this exciting new area of development, but I hope this story gets you started. Please comment if you have anything you’d like to share about the topic.