React Native iOS apps

Bundle your JavaScript into a self-contained simulator build of your React Native iOS app, then zip it with ditto and upload it to Pie. With the JavaScript inside the build, it runs without Metro.

⚠️
These steps are for React Native CLI projects. Expo projects build differently.

Before you start

  • React Native CLI: Version 0.60 or higher
  • macOS computer: You can’t build for iOS on Windows or Linux
  • Xcode: A current version installed with command line tools
  • Node.js: With npm or yarn installed
  • CocoaPods: If your project uses native dependencies

Point the build at a test or staging environment. See Prepare mobile builds for the requirements that apply to every platform.

Choose your build method

There are two ways to build:

  1. Manual bundling: You control the bundling. Best for your first build.
  2. Automated bundling: Faster, if your project is already set up for it.
ℹ️
Not sure which? Check your project’s Build Phases in Xcode. If it has “Bundle React Native code and images”, use automated bundling. Otherwise, bundle manually.

Manual bundling method

Install dependencies

Open a terminal in your project root:

# Install JavaScript dependencies
npm install
# or
yarn install

# Install CocoaPods dependencies (if applicable)
cd ios && pod install && cd ..

Stop Metro bundler

Make sure Metro isn’t running, so it can’t conflict with the build:

# Kill any running Metro instances
killall -9 node

Or close any terminal windows running Metro.

Bundle your JavaScript code

Run the bundle command from your project root:

npx react-native bundle \
  --entry-file index.js \
  --platform ios \
  --dev false \
  --bundle-output ios/main.jsbundle \
  --assets-dest ios

Your project might use a different entry file: index.js (standard JavaScript), index.tsx (TypeScript), index.ios.js (platform-specific), or a custom entry defined in package.json or app.json.

This command creates:

  • Production JavaScript bundle at ios/main.jsbundle
  • Asset files (images, fonts) in the ios folder
  • Hermes bytecode files (.hbc) if using the Hermes engine
  • An assets folder, only if your project has images or fonts

Add the bundle to Xcode

  1. Open your iOS project:

    open ios/YourProjectName.xcworkspace

    Use .xcodeproj if you don’t use CocoaPods.

  2. In Project Navigator (left panel), right-click your project name

  3. Select Add Files to “[YourProjectName]”

  4. Navigate to the ios directory

  5. Select main.jsbundle and the assets folder, if it exists

  6. Check Copy items if needed, and select your app target under Add to targets

  7. Click Add

To check, select main.jsbundle in Project Navigator and confirm your target is ticked under Target Membership in the File Inspector (right panel).

Configure JavaScript loading

Update your AppDelegate to load the bundled JavaScript instead of Metro.

For Objective-C (AppDelegate.m)

Find the jsCodeLocation configuration in the application:didFinishLaunchingWithOptions: method, and replace it with:

jsCodeLocation = [[NSBundle mainBundle] 
                  URLForResource:@"main" 
                  withExtension:@"jsbundle"];

If you see conditional DEBUG code like this, the Release build already uses the bundle:

#if DEBUG
  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] 
                    jsBundleURLForBundleRoot:@"index"];
#else
  jsCodeLocation = [[NSBundle mainBundle] 
                    URLForResource:@"main" 
                    withExtension:@"jsbundle"];
#endif

For Swift (AppDelegate.swift)

Replace the jsCodeLocation setup with:

let jsCodeLocation = Bundle.main.url(forResource: "main", 
                                     withExtension: "jsbundle")

If conditional DEBUG code is present, the Release build already uses the bundle:

#if DEBUG
  let jsCodeLocation = RCTBundleURLProvider.sharedSettings()
    .jsBundleURL(forBundleRoot: "index")
#else
  let jsCodeLocation = Bundle.main.url(forResource: "main", 
                                       withExtension: "jsbundle")
#endif

Build the app

  1. Select your target simulator in Xcode, ideally with a recent iOS version.
  2. Choose your build scheme:
    • Debug: Larger file size, includes diagnostics
    • Release: Optimized build, recommended for testing
  3. Navigate to Product → Clean Build Folder (⇧⌘K)
  4. Navigate to Product → Build (⌘B)
  5. Wait for the build to finish

Test your build offline

This check confirms the JavaScript is inside the build:

  1. Disable network connectivity by turning off Wi-Fi
  2. Close all terminal windows so Metro is not running
  3. Open iOS Simulator
  4. Navigate to Product → Show Build Folder in Finder
  5. Go to Build/Products/Debug-iphonesimulator/ for Debug or Build/Products/Release-iphonesimulator/ for Release
  6. Drag the .app file into the simulator to install it
  7. Launch the app and verify it starts without errors
  8. Test core functionality to confirm everything works
  9. Re-enable the network
If the app works offline, the bundle is set up correctly and the build is ready to upload.

Package and upload

  1. Zip the .app with ditto, so the .app sits at the top level of the zip:

    ditto -c -k --sequesterRsrc --keepParent YourApp.app YourApp.zip

    Selecting Compress on the .app in Finder makes the same zip. Don’t upload an .ipa: Pie refuses it.

  2. In Pie, open Builds and select Upload build

  3. Choose the zipped .app, enter a Build ID, and select Submit for testing

Restore development settings

Optional: if you changed AppDelegate.m or AppDelegate.swift, put the original code back to keep developing locally with Metro.

Automated bundling method

If your project has the “Bundle React Native code and images” build phase:

  1. Open your project:

    open ios/YourProjectName.xcworkspace
  2. Select your app target → Build Settings

  3. Search for Build Configuration and set it to Release

  4. Navigate to Product → Clean Build Folder (⇧⌘K)

  5. Navigate to Product → Build (⌘B)

  6. The build creates the JavaScript bundle for you

  7. Locate your .app file in Product → Show Build Folder in Finder

  8. Test offline, zip the .app with ditto, and upload it

You don’t need to bundle by hand or change any code. The Release configuration takes care of it.

What you’ll see

Build uploaded means the file reached Pie, and the build appears in Builds. It doesn’t mean Pie accepted the build. If Pie refuses it, such as a device build, the portal shows nothing more, and the build can still be picked under Select Build. Pick your simulator build there when you start a run. Then tell Pie how to sign in and what to test with the post-upload requirements.

Hermes engine

If your project uses Hermes, the default in React Native 0.70 and later:

  • Bundling works the same way.
  • Hermes produces smaller, faster bundles.
  • Check ios/Podfile for hermes_enabled = true.
  • Make sure all dependencies work with Hermes.

Build scheme comparison

FeatureReleaseDebug
Bundle sizeSmallerLarger
PerformanceOptimizedUnoptimized
Debug symbolsNoYes
Recommended forPie testingIssue diagnosis

Use Release builds for Pie, because they behave like production. For more, see the React Native publishing guide.

Troubleshooting

Blank or white screen

Possible causes:

  • Bundle not found in the app bundle
  • Incorrect entry file specified
  • Hermes compatibility issues

Solutions:

  • Verify main.jsbundle has Target Membership checked in Xcode
  • Confirm the entry file name, and check package.json for a custom entry
  • If you use Hermes, make sure all dependencies work with it

App tries to connect to Metro (localhost:8081)

Possible causes:

  • AppDelegate still in development mode
  • Debug scheme selected instead of Release
  • Cached build artifacts

Solutions:

  • Verify AppDelegate changes were saved
  • Switch to the Release scheme or update the AppDelegate code
  • Clean the build folder and rebuild: Product → Clean Build Folder
  • Search your codebase for hardcoded localhost:8081 references

Assets not loading (missing images or fonts)

Possible causes:

  • Assets folder not added to Xcode
  • Assets not included in the target
  • Incorrect asset paths in code

Solutions:

  • Verify the assets folder was added with Copy items if needed
  • Check Target Membership in the File Inspector
  • Confirm your code uses correct relative paths for assets

Native modules failing

Possible causes:

  • CocoaPods not installed
  • Static linking issues
  • New architecture compatibility (React Native 0.68 and later)

Solutions:

  • Run cd ios && pod install && cd ..
  • Check module documentation for static build requirements
  • Check that each module works with the React Native architecture

Build errors in Xcode

Possible causes:

  • Hermes configuration issues
  • Missing bundle file
  • Duplicate symbols

Solutions:

  • Verify hermes_enabled in ios/Podfile
  • Confirm ios/main.jsbundle exists
  • Delete DerivedData: rm -rf ~/Library/Developer/Xcode/DerivedData

Still stuck? Contact support@pie.inc with your React Native version, build errors or console logs, screenshots of relevant issues, and what you have tried.

Next steps

Last updated on