How I reached the limits of React-Native by implementing an iOS Share Extension

Maxime Blanchard
Kraaft.co
Published in
8 min readAug 2, 2021

Let me drive you through my journey creating a wonderful share extension in React-Native. In this post I’ll describe the iOS part as it’s the part that lead to problems. Android was fairly straightforward.

TL;DR

Share Extensions in React Native are not easy to implement because you are limited in the amount of memory you can use (it’s quite low compared to an application). You have two solutions, optimizing your code using engines like Hermes or redirect straight away into your app to launch your sharing flow (Android-like).

Context

First, what is a Share Extension? From Apple’s developer website, we have this:

Share extensions give users a convenient way to share content with other entities, such as social sharing websites or upload services. For example, in an app that includes a Share button, users can choose a Share extension that represents a social sharing website and then use it to post a comment or other content.

Basically, it’s an extension of your app that allows users to share content from an external source to your application.

Kraaft Share Extension
Kraaft Share Extension

When you want to add a Share Extension in React-Native, 2–3 libraries are available. I started with react-native-share-menu (it’s not what I used at the end, you’ll understand why later).

What you need to know is that a Share Extension is different depending on the platform. On iOS you have a modal where you can enter some text by default and a “post” button to send everything.

What’s great with the library I chose is that it allows you to have a custom view on iOS when you share (like Whatsapp or Messenger). In this custom view you’ll be able to add your share flow. In our case we had to select the conversation to send the picture in and then a screen to annotate the said picture (see GIF above).

When creating the Share Extension, it’s kind of an app on its own. We have to configure what library it uses (in the Podfile) and entry point for it.

Plan

Here is the plan I had in mind:

  1. Setup Podfile
  2. Link Firebase
  3. Fetch and display user conversations
  4. Annotate the picture
  5. Send the picture
  6. [Bonus] Store it in AsyncStorage (in case we are offline and we want to retrieve it when going back to the main app)

Setup Podfile

The Share Extension being a Target on its own, we need to have an entry for it in the PodFile.

target "Kraaft" do
config = use_native_modules!
...
end
target "KraaftShare" do
pod 'RNFBApp', :path => 'path/to/node_modules/@react-native-firebase/app'
...
end

Link Firebase and use Firestore

We want to use Firebase on the extension and so if the user is logged in the app it will also be logged in the extension. Fortunately for us, we use react-native-firebase and as the native firebase SDK, we have access to a function that allows us to store the firebase state in a specific place, under a specific identifier useUserAccessGroup :

await auth().useUserAccessGroup('com.example.app');

After creating a ‘Keychain Access Group’ I managed to make it work.

Once we are connected to Firebase, it’s super simple to call Firestore and display the conversations so this part was really a matter of minutes.

Enable Picture Annotation

Next step is to annotate the picture. The current flow we have in the app is a modal that opens up when the user wants to annotate his picture. As we want the Share Extension to have the same flow, I used the same library, react-native-photo-editor.

But… It was not opening the modal and obviously I had no logs nor errors in my console.

Here comes Xcode debugger to the rescue. It’s a bit tricky to run, but it’s possible to run the Share Extension and add breakpoints from Xcode. It’s not the point of this post, but you should give it a try, it can be reaaaaaaally useful sometimes.

The problem was that the library was requesting the main app view to attach its modal but, as we are in a Share Extension and not in an App, there is no such thing.

I had to fork the library and fix this small, yet annoying, problem. It was the first fork of this journey and not the last, unfortunately…

→ link to the commit

Okay, so we are now connected to Firebase, we can select a conversation and then annotate the picture if we want. The last thing to do is now to send it.

Upload the picture

When you want to send a file, usually you upload it to Firebase Storage. Believe it or not, the actual code of the iOS Firebase SDK is not configured to allow sending files to Storage from such place (extensions mainly).

Thus, I had to fork another repository…

→ link to the commit

Async Storage

Finally, when I was able to send pictures to the back-end, I needed to store the content of the message I wanted to send in some kind of queue somewhere on the device. We do that because if we are offline we don’t want the user to lose its message. Once it’s in there, and that we retrieve the connection, we dequeue it and send it. As a Share Extension is meant to be simple and fast, we are doing the ‘dequeue’ logic inside the main application; we don’t want to hold there until we retrieve connection.

Before, I used to use react-native-async-storage, but as we want to share the same “container” with both the app and the extension, I had to change to this one: react-native-shared-group-preference. This new library handles the App Groups, a feature designed to share data between applications.

Grand Finale

Okay, everything is working, in the simulator at least → Time to deploy a ‘Release’ version. Everything was working until I decided to share a Screenshot I just took.

My Share Extension opens fine but when I try to send the picture, it crashes (it closes itself).

It’s Debug time again but this time on a real device; Indeed, it’s a crash:

'Thread 1: EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=120 MB, unused=0x0)'

Apparently, there is a memory limit that I didn’t know about. Few things to note about this limit:

  • When it is exceeded, iOS just kills the process.
  • On Simulator, the memory limit is disabled.

After a couple of weeks of work, here was my reaction:

The Problem

After some digging, I found this table containing the limit and priority for every process.

Memory Limits on iOS

In fact, when debugging, before even sending the picture, the Memory Usage is at almost 100 MB. And thus, as we do some image manipulation when sending an image, the memory was increasing beyond the limit.

From there, I had 2 options:

  • We stick with this flow, and we try to optimise the memory consumption
  • We move the flow into the app (so that the share extension will just redirect us to the main application)

Optimization

For reference, here is the memory consumption of each part in our Share Extension:

Memory Consumption in Share Extension

What worked was to optimize the FlatList using this article : https://reactnative.dev/docs/optimizing-flatlist-configuration. But even after some refactoring of our components, it was still peaking at 85 MB, which is unfortunately still too much for our extension to work properly.

New day, new ideas, we tried to update our React native version to 0.64 to enable Hermes Engine.

From React Native’s website:

For many apps, enabling Hermes will result in improved start-up time, decreased memory usage, and smaller app size.

Quick note, Hermes is really effective but you need to build in Release mode to see its benefits. Actually when building the Share Extension in Debug with Hermes, we reached the 120 MB when loading the conversations, it was unusable...

After building in Release mode, I managed to send the screenshot that was leading to a crash before.

For reference, when running with Hermes, our Share Extension has a size of 50 MB instead of 92 MB before.

It’s good news but we still have issues when loading more than 1500 conversations (which is usually not common for our clients). All in all, this solution works fine, but I still manage to make it crash. Basically, it’s working for our use case (we can still limit the number of conversations as no one will want to go through their 1500 conversations to send pictures) but I was still not convinced.

In-App Flow

The In-App Flow is common on Android but it is actually quite uncommon on iOS. Even when dealing with native, Apple will always open a ‘kind of’-modal when you try to share something. The trick is to navigate to the main application right after the Share Extension starts. If you pay attention, you will still be able to see the translucent modal but it’s automatically dismissed in 0.2 second.

Technically, I did not have to change the library for sharing, but the one that I used was not allowing to share multiple pictures. The new library that I used is react-native-receive-sharing-intent: you have to fine-tune the native iOS part, but once it’s working, you are all set!

The only downside its that once you are redirected, even if you cancel the flow, you are still on the application. With the previous flow, you go back to where you were (Photos for example).

Kraaft Share Extension
BeforeAfter comparison

Conclusion

IOS Share Extension in React Native is complex as you are limited in the Memory Usage and React-Native will take up an incompressible amount of space. Hermes is a viable solution depending on the logic you want to implement in your extension.

Otherwise, you need to move the sharing flow inside your main app and mimic the Android behaviour.

--

--