React Native: Managing iOS Dependencies

We at Sandstorm really embraced React in lots of projects, ranging from internal tools, our product exply, to customer projects and the Neos CMS React UI. That's why it was very natural for us to use React Native - a nice toolkit to build cross-platform apps. However, there are some problems to solve in order to nicely manage native iOS dependencies. Read on for a description of the issues, and our solutions to them!

Our Problem: The build system for iOS is ... suboptimal (IMHO).

As soon as you have to leave the JavaScript world to integrate a native binding, things become kind of difficult - especially on iOS: This is because there is no official package manager for Objective C / Swift, and (in my opinion) no really good build system - but you rather need to "drag and drop" XCode projects into your main project as explained in the React Native docs. Most other well-known programming platforms have a central and standardized package repository and build system, e.g. the JVM has Gradle/Maven; PHP has Composer, the JavaScript world has NPM.

Furthermore, e.g. IntelliJ for Java has the possibility to auto-configure itself based on the maven pom.xml or build.gradle file – thus there is a human-readable, minimal and expressive dependency listing and build description. For XCode, on the other hand, you have to work with extremely verbose xcodeproject files, which have to be checked into Git, and are generally very fragile to handle in my experience in React Native projects. Just having a single setting "wrong" of the 100s of settings in XCode will then lead to very strange build issues.

To me, this is extremely fragile, and when skimming GitHub, you see the same issues appearing again and again on many React Native libraries.

Note: I am sure the XCode build system also has its benefits, but to me personally, it definitely lacks the conciseness and expressiveness of e.g. a gradle-based build; and a standardized way to manage dependencies. So be aware, I am no "native" ObjectiveC/Swift developer, so some details might be inaccurate.

Our Goals

  • We'd like a standardized way to install native dependencies.
  • We'd like to adjust the XCode project as minimally as possible.
  • We'd like reproducible builds with little moving parts.

CocoaPods to the rescue!

We've checked the two community dependency managers for ObjectiveC: CocoaPods and Carthage. We settled on CocoaPods after trying out both. Some people see the holistic approach of CocoaPods as a little dangerous; but for us, it worked quite well now with almost no problems.

The basic principle of CocoaPods is depicted in the following diagram: It creates a "Pods" project, which exposes a static library libPods.YourReactNativeProject.a, which contains the native dependencies. Furthermore, your own project (which by default has e.g. references to React Native core), is modified to contain this single dependency. Everything which is managed by Cocoapods is shown in orange in the diagram below.

The problem now is that the native libraries (e.g. react-native-svg) naturally depend on React Native core again - thus we need to ensure that they reference the same React Native library which you link to from the outer project.

Note: For the build and deployment to work properly, you need to keep a direct dependency from your project to React Native; and you need to prevent dependencies from your libraries to React Native. Otherwise, developing and testing will still work, but things will fail when building for the device.

Step by Step Tutorial to managing React Native Dependencies with CocoaPods

The following steps should help you to get started with cocoapods for React Native quickly, and serve as a reference for our learnings.

1. Installing cocoapods, create a basic Podfile

First, install cocoapods using sudo gem install cocoapods.

Then, in the ios folder, create a Podfile with the following contents:

# File contents of "ios/Podfile" platform :ios, '9.0' target 'ReactNativeCocoapodExample' do pod 'React', :path => '../node_modules/react-native', :subspecs => [ 'Core', 'CxxBridge', 'DevSupport', # the following ones are the ones taken from "Libraries" in Xcode: 'RCTAnimation', 'RCTActionSheet', 'RCTBlob', 'RCTGeolocation', 'RCTImage', 'RCTLinkingIOS', 'RCTNetwork', 'RCTSettings', 'RCTText', 'RCTVibration', 'RCTWebSocket' ] # the following dependencies are dependencies of React native itself. pod 'yoga', :path => '../node_modules/react-native/ReactCommon/yoga/Yoga.podspec' pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec' pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/GLog.podspec' # your other libraries will follow here! end # The following is needed to ensure the "archive" step works in XCode. # It removes React from the Pods project, as it is already included in the main project. post_install do |installer| installer.pods_project.targets.each do |target| if target.name == "React" target.remove_from_project end end end

The above Podfile does not contain any foreign dependencies yet; however it sets up the dependency to React which we'll need as soon as we add the first library.

Note: This podfile is basically taken from the official docs; though they do not mention that this can be a good way to manage foreign dependencies. Furthermore, it has been adjusted by our real-world learnings.

2. Execute pod install

By running pod install, an ios/Pods directory will be created, containing an XCode project with all foreign dependencies. Furthermore, the already-existing xcodeproj will be modified to contain the dependency to the pod-library. This will be the only modification to the main xcodeproj - no need to manually modify it anymore!

After this, commit your results and ensure that react-native run-ios as well as the Run action through XCode still work.

Note: We are following the recommendation of cocoapods, and are checking in the full ios/Pods folder into Git.

Example: react-native-svg

To install react-native-svg, first install it using yarn add react-native-svg.

Note: We always use yarn as dependency manager, as it ensures that the dependencies get checked out in a reproducible way.

Now, it's time to include it in the Podfile. To do that, we have to inspect the package in node_modules/react-native-svg - and if we are lucky (like in the case of this package), we are finding a *.podspec file; in this case it is named RNSVG.podspec. Remember the name, it will be used in the next step.

Now, add the following line to the ios/Podfile:

# your other libraries will follow here! pod 'RNSVG', :path => '../node_modules/react-native-svg'

You see we are referencing the podfile in the NPM package (which is installed through yarn).

Now, you should run pod install again (in the ios folder). You should not see changes to the main XCode project, only the Pods xcode project will change. You should verify that the dependency you just added actually ends up as target in xcode - as shown in the following image:

If your just-added library does not appear as target of the Pods project, the Podfile of the library is wrong – we usually patch the Podfile then (as we will show in the next example).

Now, again, test that you can use the newly included library in your JavaScript code, and that the system both compiles through react-native run-ios and via starting it in XCode.

Example: react-native-app-auth

In order to add react-native-app-auth, you need to add the following two lines to the Podfile:

pod 'RNAppAuth', :path => '../node_modules/react-native-app-auth/ios' pod 'AppAuth', '>= 0.91'

When running pod install, it will fail because the homepage is not set in that podfile. When you fix this, you'll see that the dependency does not appear as Target in the Pods project of XCode (see screenshot above to remember where to look). After investigation, it turned out that the path to the sources in the Podfile was not correct - after we fixed this and ran pod install again, the problem disappeared.

To sum up, we are patching the package (which is a pragmatic way to solve the problem; but if you know a better way let us know). For that, we are using the patch-package npm package to remember the patches in our project. In a nutshell, you need the following patch:

patch-package new file mode 100644 Binary files /dev/null and b/node_modules/react-native-app-auth/.DS_Store differ --- a/node_modules/react-native-app-auth/ios/RNAppAuth.m +++ b/node_modules/react-native-app-auth/ios/RNAppAuth.m @@ -3,7 +3,7 @@ #import <AppAuth/AppAuth.h> #import <React/RCTLog.h> #import <React/RCTConvert.h> -#import "AppDelegate.h" +#import "../../../ios/ReactNativeCocoapodExample/AppDelegate.h" @implementation RNAppAuth --- a/node_modules/react-native-app-auth/ios/RNAppAuth.podspec +++ b/node_modules/react-native-app-auth/ios/RNAppAuth.podspec @@ -6,13 +6,13 @@ Pod::Spec.new do |s| s.description = <<-DESC RNAppAuth DESC - s.homepage = "" + s.homepage = "homepage" s.license = "MIT" s.license = { :type => "MIT", :file => "../LICENSE" } s.author = { "author" => "kadi.kraman@formidable.com" } s.platform = :ios, "7.0" s.source = { :git => "https://github.com/FormidableLabs/react-native-app-auth.git", :tag => "master" } - s.source_files = "RNAppAuth/**/*.{h,m}" + s.source_files = "*.{h,m}" s.requires_arc = true s.dependency "React"

The patch changes the following things:

  • Manually fix the reference to AppDelegate.h. If anybody knows a better way to do this, let us know!
  • Add the homepage.
  • Fix the source files path - the source files are directly next to RNAppAuth.podspec, without a nested directory. I am unsure how this Podfile could ever work :-)

Now, you again need to run pod install.

Example: react-native-vector-icons

Installing react-native-vector-icons works just as usual: Install it using yarn add react-native-vector-icons and then add the following line to the Podfile:

pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'

Then, remember to run pod install. You then still need to edit the Info.plist file of your project to include the needed fonts in the application bundle, as explained here in the official docs.

Example: react-native-i18n

Installing react-native-i18n works by adding it using yarn add react-native-i18n, and then add the following line to the Podfile:

pod 'RNI18n', :path => '../node_modules/react-native-i18n'

Then, remember to run pod install.

Example: react-native-navigation

Installing react-native-navigation works by adding it using yarn add react-native-navigation ^2.0.0, and then add the following line to the Podfile:

pod 'ReactNativeNavigation', :path => '../node_modules/react-native-navigation'

Then, remember to run pod install. You now need to adjust your AppDelegate.m as described in step 3 of the official docs.

Example: React ART

React ART is a little special, as it is already packaged with the React Native source code; it is just not linked explicitely. To link it (and to have it work in all cases), I needed to manually link it as described in step 1 and 2 of the React Native docs. Furthermore, I added it as subspec in the Podfile:

target 'ReactNativeCocoapodExample' do pod 'React', :path => '../node_modules/react-native', :subspecs => [ # all other subspecs here (see first code listing) 'ART' ] # further dependencies here end

Now, remember to run pod install.

This is so far the only library where a manual adjustment of the main XCode project is needed.

Closing Notes

  • This has been tested with react 16.3.1, react-native 0.55.4, cocoapods 1.5.3, XCode 9.4, and Mac OS 10.13.4 (High Sierra).
  • As soon as you use react-native link, react-native run-ios will break with build errors! If that happens, the following has helped for us:

    • Go to [YourReactApp] -> select the main target and copy it; name it "..._XCode"

    • In the original target, go to "Build Phases" and "Link Binary with Libraries", and remove everything except libPods-...... This should fix react-native run-ios.

    • When you want to run/debug through XCode, use the copied XCode target.

    • Summary: Never use react-native link for modifying iOS targets!

Summary

  • We are not manually modifying the xcodeproj which was generated by react-native init. We never use react-native link.
  • We're using cocoapods to add native dependencies to the project. We version the full Pods folder in Git.
  • If we need to patch the sources, we do so by using patch-package. It would be nicer to get rid of this, but we won't be dogmatic about this.