2024-02-13

Extending React Native visionOS with RealityKit

Immersive Space with Snow Particles

React Native
visionOS

Contents

Introduction

React Native possesses a unique capability that enables the blending of Native code with JavaScript as needed. In today's blog post, we will explore how to enhance your spatial application with immersive features using Swift and the RealityKit framework.

If you are not familiar with React Native for visionOS, I recommend starting with this blog post: Announcing React Native for Apple Vision Pro.

Our goal is to create a Mixed Reality Immersive Space that generates snow particles:

Disclaimer: The main focus of this blog post is to describe how to integrate custom RealityKit code into React Native visionOS, not how to use RealityKit. If you want to dive deeper into this topic, checkout this WWDC talk.

Getting started

First let's initialize a new project:

npx @callstack/react-native-visionos@latest init YourApp

Next, go to YourApp/visionos folder and run following commands to install Pods:

bundle install && bundle exec pod install

Open the project in Xcode and click Run

xed visionos/YourApp.xcworkspace

JavaScript part

Let's start with implementing JavaScript part. Navigate to App.tsx and add basic markup with two buttons and text.

import React from 'react';
import {Alert, Button, StyleSheet, Text, View} from 'react-native';
 
function App(): React.JSX.Element {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>React native visionOS 👋</Text>
      <Button
        title="Open ImmersiveSpace"
        onPress={openImmersiveSpace}
        color="white"
      />
      <Button
        title="Close ImmersiveSpace"
        onPress={closeImmersiveSpace}
        color="white"
      />
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  title: {
    fontSize: 30,
    color: 'white',
    fontWeight: 'bold',
    marginBottom: 15,
  },
});
 
export default App;
 

Next, let's implement methods to open and close immersive spaces:

import {XR} from '@callstack/react-native-visionos';
import React from 'react';
import {Alert, Button, StyleSheet, Text, View} from 'react-native';
 
function App(): React.JSX.Element {
  const [isOpen, setIsOpen] = useState(false) // Store if session is open
  const openImmersiveSpace = async () => {
    try {
      await XR.requestSession('SnowEmitter'); // Pass uniquie ID
      setIsOpen(true)
    } catch (error) {
      if (error instanceof Error) {
        Alert.alert('Error', error.message); // Handle errors
      }
    }
  };
 
  const closeImmersiveSpace = async () => {
    await XR.endSession();
    setIsOpen(false)
  };
 
  return (
    //..
  );
}

This code calls XR.requestSession() passing a unique identifier of ImmersiveSpace that we will implement in the next section.

Warning: XR API doesn't store information whether session is open, you need to track it yourself.

Swift Part

Inside of App.swift add new ImmersiveSpace. Use the same identifier as in JavaScript part.

@main
struct SnowEmitterAppApp: App {
  @UIApplicationDelegateAdaptor var delegate: AppDelegate
  @State private var immersionStyle: ImmersionStyle = .mixed
  
  var body: some Scene {
    RCTMainWindow(moduleName: "SnowEmitterApp")
      .defaultSize(CGSize(width: 500, height: 800))
    ImmersiveSpace(id: "SnowEmitter") { // Match ID from JS
      SnowEmitterView()
    }
    .immersionStyle(selection: $immersionStyle, in: .mixed, .full)
  }
}

You can learn more about ImmersiveSpaces here: https://developer.apple.com/documentation/swiftui/immersive-spaces

Next, let's implement SnowEmitterView. Create a new Swift file and paste below snippet:

import SwiftUI
import RealityKit
 
struct SnowEmitterView: View {
  var body: some View {
    RealityView { content in
      let particleEntity = Entity() // Create new Entity
 
      var particles = ParticleEmitterComponent.Presets.snow // Pick snow preset
	  // Set properties
      particles.emitterShape = .plane
      particles.birthLocation = .surface
      particles.mainEmitter.size = 0.003
 
	  // Add Particle emitter component
      particleEntity.components[ParticleEmitterComponent.self] = particles
	  
	  // Set position and scale
      particleEntity.position = SIMD3(x: 0, y: 2, z: -2)
      particleEntity.scale *= 3
      particleEntity.transform.scale *= 3
 
	  // Add particle entity
      content.add(particleEntity)
    }
  }
}

This view uses RealityView and adds a new Entity with a ParticleEmitterComponent.

You can read more about Particle Emitters, here.

That's all

And that's it! Now you have App UI written in React Native and spatial extension in RealityKit. I hope you found this article useful. If you have any questions or feedback feel free to reach out to me on Twitter.

For those interested in the sample code, you can find it in the GitHub Repository.