This Repository contains a step-by-step guide on how to integrate React Native into an iOS application.
The repository starts with a simple app, with two tabs:
- The home tab is a native view where the team is going to work using native technologies
- The settings tab will be a React Native view.
Every commit represents a step toward integrating the React Native into the app.
You can checkout every commit to see if the current state of the project.
This commit is the initial state. We have the iOS app in the MixedApp folder and currently both tabs are native views.
The goal is to replace the Settings view with a React Native view.
First step to integrate React Native is to move your ios project into a dedicated ios folder.
- create a new
ios
folder - move the
MixedApp
folder inside theios
folder
We now need to add the React and React Native dependencies to our project.
-
Create a
package.json
file inside theMixedApp
folder. It must be a sibling of the previous ios folder -
Paste the following content inside the
package.json
{
"name": "MixedApp",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "yarn react-native start"
}
}
- Navigate to the
MixedApp
folder and run the following command:
yarn add react-native@latest
- One of the warning line would be something like:
warning " > react-native@0.75.3" has unmet peer dependency "react@^18.2.0".
so make sure to run also
yarn add react@^18.2.0
- Add a
.gitignore
file to ignore thenode_modules
folder
The index.js
file is the entry point for the JS module we want to load in our app.
At the same level of the package.json
- Create and
index.js
file - Add the following content to the
index.js
file
import React from 'react';
import {AppRegistry, StyleSheet, Text, View} from 'react-native';
const Settings = ({data}) => {
return (
<View style={styles.container}>
<Text>Settings</Text>
<Text>{data}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#EEEEEE',
},
});
// Module name
AppRegistry.registerComponent('Settings', () => Settings);
Now, it's time to link the React Native dependencies to the iOS project.
The tool we use to do that is Cocoapods and Ruby Gems.
Follow the install instructions in the Cocoapods website
We use Ruby Bundler to manage the ruby dependencies.
- Create a
Gemfile
at the same level of thepackage.json
file with this content
source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
- Inside the iOS folder, create a
Podfile
file, with this content
# Resolve react_native_pods.rb with node to allow for hoisting
require Pod::Executable.execute_command('node', ['-p',
'require.resolve(
"react-native/scripts/react_native_pods.rb",
{paths: [process.argv[1]]},
)', __dir__]).strip
platform :ios, min_ios_version_supported
prepare_react_native_project!
linkage = ENV['USE_FRAMEWORKS']
if linkage != nil
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
use_frameworks! :linkage => linkage.to_sym
end
target 'MixedApp' do
config = use_native_modules!
use_react_native!(
:path => config[:reactNativePath],
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
post_install do |installer|
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false,
# :ccache_enabled => true
)
end
end
- Navigate inside the
ios
folder - run the
bundle install
command - run the
bundle exec pod install
command - update the
.gitignore
file to ignore the**/ios/Pods
, the**/ios/generated
folder, and the.xcode.env.local
file
Now it's time to modify the iOS code to connect with React Native.
React Native is kickstarted by a class called RCTAppDelegate
. As the name implies, React Native works with the AppDelegate
hierarchy chain, and not with the SceneDelegate
hierarchy.
- Open the project with the command
open ios/MixedApp.xcworkspace
- Delete the
SceneDelegate.swift
file - In Xcode, select your app icon and the Info tab.
- Delete the
Application Scene Manifest
line - Open the
AppDelegate.swift
file and apply the following changes
import UIKit
+ import React
+ import React_RCTAppDelegate
@main
-class AppDelegate: UIResponder, UIApplicationDelegate {
+class AppDelegate: RCTAppDelegate {
- func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
- // Override point for customization after application launch.
- return true
- }
+ override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ super.moduleName = "Settings"
+ self.initialProps = [:];
+ super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ let rootViewController = TabViewController()
+ self.window.rootViewController = rootViewController
+ self.window.makeKeyAndVisible()
+ return true
+}
+ override func sourceURL(for bridge: RCTBridge) -> URL? {
+ return bundleURL()
+ }
+ override func bundleURL() -> URL?
+ {
+ #if DEBUG
+ return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
+ #else
+ return NSBundle.main .url(forResource: "main", withExtension: "jsbundle")
+ #endif
+ }
- // MARK: UISceneSession Lifecycle
- func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
- // Called when a new scene session is being created.
- // Use this method to select a configuration to create the new scene with.
- return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
- }
-
- func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
- // Called when the user discards a scene session.
- // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
- // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
- }
}
- Open the
SettingsViewController.swift
file and modify it as it follows
import Foundation
+import React
+import React_RCTAppDelegate
class SettingsViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
- self.view.backgroundColor = .red
+ self.view = (RCTSharedApplication()?.delegate as? RCTAppDelegate)?.rootViewFactory .view(withModuleName: "Settings", initialProperties: [:])
}
}
- Select your app in Xcode.
- Select
Build Settings
- in the top right corner, filter for
User Script Sandboxing
- Set the value to
NO
.
Now you can build and run your app, but you'll see an error like this one.
The problem is that Metro is not running. The next step will fix this.
Metro is the JS bundler that provides JS files to your app while you develop it.
To configure it properly, follow these steps:
- From the same folder where the
package.json
is, runyarn add --dev @react-native/metro-config
- Create a new file
metro.config.js
with this content
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
/**
* Metro configuration
* https://reactnative.dev/docs/metro
*
* @type {import('metro-config').MetroConfig}
*/
const config = {};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
- create a new file
.watchmanconfig
with this content:
{}
- Run
yarn start
to start metro. - In another terminal, navigate to the
ios
folder - Run
bundle exec pod install
. This is needed because theyarn add @react-native/metro-config
changed thenode_modules
folder and native dependencies are out of sync. - From Xcode, build and run your app.
If you now navigate to the Settings tab, you should see your JS being loaded.
If you now modify the index.js
file, you can update how it looks in your app, in real time!
To pass data between the native side and the JS side, you can use the initialProperties
dictionary of the SettingsViewController
.
- Modify the
SettingsViewController.swift
as it follows:
override func viewDidLoad() {
super.viewDidLoad()
- self.view = (RCTSharedApplication()?.delegate as? RCTAppDelegate)?.rootViewFactory .view(withModuleName: "Settings", initialProperties: [:])
+ self.view = (RCTSharedApplication()?.delegate as? RCTAppDelegate)?.rootViewFactory .view(withModuleName: "Settings", initialProperties: ["data":"Hello from React Native"])
}
- Rebuild and rerun the app