back to index

embedding unity in react native

Problem: Building a construction app that needs both a polished mobile experience and complex AR visualization. React Native excels at navigation, authentication, and project management. Unity excels at BIM rendering, AR anchoring, and 3D interaction. The standard approach is two separate apps with deep links between them. Poor UX. Fragmented experience.

Solution: Embed Unity as a framework inside the React Native app. Single binary. Single Xcode project. Unity loads on demand only when users enter AR mode, keeping launch time fast. Clean bidirectional communication through a native bridge layer.

Architecture Overview

The integration follows a host-guest pattern. React Native is the primary app shell, handling all UI, navigation, authentication, and data management. This is what users interact with 95% of the time. Unity is the AR engine, loaded as an embedded framework that activates only when users enter AR viewing mode. The native bridge is an Objective-C layer enabling bidirectional communication: React Native calls into Unity to start AR sessions, Unity calls back to request data and report events.

Build Process

Unity compiles to an iOS framework rather than a standalone application. This requires specific build configuration and post-processing.

First, configure Unity to export as framework targeting iOS with ARM64 and bitcode disabled. Then Unity's PostProcessBuild hooks modify the generated Xcode project automatically: disabling bitcode (Apple deprecated it), adding required frameworks, configuring Info.plist for App Store compliance, and enforcing ARM64 architecture.

The generated UnityFramework.framework drops into the React Native Xcode workspace as an embedded framework. Unity loads at runtime only when needed, keeping app launch fast. Users browsing projects never pay the cost of loading the AR engine.

Native Call Proxy

The bridge layer uses Objective-C protocols. Unity defines what it needs from the host application. React Native implements the protocol.

@protocol NativeCallsProtocol
@required
- (void) requestPreferences;
- (void) requestModel;
- (void) requestMarker;
- (void) logMessageReceived:(char*)stackTrace;
@end

extern "C" {
    void requestPreferences() { return [api requestPreferences]; }
    void requestModel() { return [api requestModel]; }
    void requestMarker() { return [api requestMarker]; }
    void logMessageReceived(char* stackTrace) { 
        return [api logMessageReceived:stackTrace]; 
    }
}

The extern "C" block exposes functions Unity can call via P/Invoke. Clean interface with no direct coupling between runtimes. Unity doesn't know React Native exists. It just calls functions. The bridge implementation decides what happens.

Unity-Side Bridge

On the Unity side, a NativeAPI class wraps the external calls using P/Invoke:

#if HOSTMANAGER
public class NativeAPI
{
    [DllImport("__Internal")]
    public static extern void requestPreferences();

    [DllImport("__Internal")]
    public static extern void requestModel();

    [DllImport("__Internal")]
    public static extern void logMessageReceived(string stackTrace);
}
#endif

The HOSTMANAGER compiler directive controls this code path. Development builds run standalone with direct API access for fast iteration. Production builds communicate through the bridge. Same codebase, different data sources.

Host Manager Pattern

The HostManager singleton coordinates all host application interaction. On startup, it immediately requests preferences from React Native to get authentication tokens and project configuration. Then it waits for model data before beginning the import.

public class HostManager : Singleton<HostManager>
{
    public string ModelResponseString { get; private set; }

    public void RequestModelData()
    {
        ModelResponseString = null;
        NativeAPI.requestModel();
    }

    // Called by React Native via UnitySendMessage when model data is ready
    public void ResponseRequestModel(string data)
    {
        ModelResponseString = data;
    }
}

The flow: Unity framework loads, HostManager requests preferences, React Native responds with settings via UnitySendMessage, Unity shows a loading screen and requests model data, React Native fetches BIM data from its API and sends JSON back, HostManager receives data and triggers import. React Native owns authentication and project selection. Unity receives tokens ready to use. No duplicate auth logic.

Communication Protocol

Data flows as JSON strings. Both sides serialize and deserialize independently. The protocol is simple: string in, string out.

The settings structure includes domain, access token, account/project IDs, storey configuration, view type (AR or 3D), and offline mode flag. React Native owns the complexity of authentication, project selection, and API communication. Unity receives a ready-to-use configuration object.

Conditional Compilation

The codebase supports multiple build targets through compiler directives. With HOSTMANAGER defined, data comes from the React Native bridge. Without it, data loads directly from the BIM API. Same codebase, different data sources. Development uses direct API for faster iteration with hot reload. Production uses the bridge for the integrated experience.

UI Adaptation

Some Unity UI elements shouldn't appear in embedded mode. React Native provides those screens. A simple HideInReactBuild component destroys itself on Awake when HOSTMANAGER is defined. Attach it to login screens, project selection, settings menus: anything React Native handles in production.

Model Import Integration

The importer module waits for React Native to provide model data using Unity's coroutine system:

private IEnumerator WaitForReactCoroutine(Transform parent, 
    Action<ModelController> onModelImported)
{
    HostManager.Instance.RequestModelData();

    yield return new WaitUntil(() => 
        HostManager.Instance.ModelResponseString != null);

    onModelImported(BimFactory.ImportBimJsonModel(
        parent, HostManager.Instance.ModelResponseString));
}

Request model data, yield until response arrives, parse and import when ready. This keeps Unity responsive during the wait. Loading animations continue. UI remains interactive. The user sees progress rather than a frozen screen.

Viewer Type Selection

React Native determines the viewing mode through the settings JSON. Two viewer types are available: MarkerAR for full AR mode with QR marker anchoring, camera feed, and real-world overlay; Mobile3D for a standard 3D viewer with pan, zoom, and rotate without AR. React Native controls which mode launches based on user selection. Unity loads the appropriate scene and configuration.

Error Handling

Unity crashes shouldn't crash the entire app. The bridge catches unhandled exceptions and Unity log messages of type Exception, forwarding them to React Native. React Native receives crash logs, can display user-friendly error messages, and can report to crash analytics. Unity's problems don't leave users stranded in a broken AR view.

Why This Architecture

Users don't switch between apps. They seamlessly transition from project list to AR view within a single experience. Unity framework loads only when needed, keeping app launch fast and memory used only during AR sessions.

The separation is clean: React Native handles UI/UX, Unity handles rendering. Each runtime does what it does best. Login once in React Native, token flows to Unity. Update the React Native UI without touching Unity. Update Unity rendering without touching React Native. Independent development cycles.

Not a web view. Not a cross-compiled shim. Real native code on both sides.

Build Configuration

Two build variants maintain development velocity. Development builds run Unity standalone with direct API access for fast iteration and hot reload. Production builds embed Unity in React Native with full bridge communication for integration testing.

Same codebase compiles both ways. No code duplication. No drift between environments.

Result: A construction app that feels native everywhere. Smooth navigation in React Native. Powerful AR in Unity. One binary. One experience. The architecture invisible to users, which is exactly the point.