2024-02-13
Extending React Native visionOS with RealityKit
Immersive Space with Snow Particles
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 YourAppNext, go to YourApp/visionos folder and run following commands to install Pods:
bundle install && bundle exec pod installOpen the project in Xcode and click Run
xed visionos/YourApp.xcworkspaceJavaScript 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.