Let's look at how to interact with multiple scenes and present Particle Systems during Vision Pro development with NativeScript. We will use @nativescript/swift-ui to provide a concise API to open and dismiss scenes on demand as well as even update them dynamically with contextual data.
We can explore these features with a Vision Pro project; I've created one you can refer to here:
👉 Demo Github Repo which can be run with both the Vision Pro simulator on a Mac or a physical device.
If you have a physical Vision Pro, you can also experience this fully by searching for and installing the "NativeScript Preview" app on the Vision Pro App Store, which will allow you to use this StackBlitz:
Or you can also create one anytime with:
npm i -g nativescript@vision
ns create myapp --vision
If you'd like to use any preferrable JavaScript flavor, refer to the visionOS docs here.
In order to support multiple scenes you always want to check your App_Resources/visionOS/Info.plist
to ensure it contains at least this setting:
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
We interact with scenes by their unique identifier and the @nativescript/swift-ui APIs:
openScene(options: OpenSceneOptions)
: Toggle opens/dismiss on a sceneupdateScene(options: OpenSceneOptions)
: Update contextual data to any opened sceneThe OpenSceneOptions
interface is as follows:
export interface OpenSceneOptions {
/**
* The id of the scene to open
*/
id: string;
/**
* Whether the scene is immersive or not.
* Only set this when you know the WindowGroup is defined with immersive style.
*/
isImmersive?: boolean;
/**
* Any data bindings to pass to the scene.
*/
data?: any;
}
You can extend these options to strongly type your data
per your needs. Let's explore an example.
You first want to define any number of scenes you'd like your app to provide via App_Resources/visionOS/src/NativeScriptApp.swift
:
import SwiftUI
@main
struct NativeScriptApp: App {
var body: some Scene {
NativeScriptMainWindow()
// NEW SCENE: A distinct window of a playable video
WindowGroup(id: "Video") {
VideoSceneView()
}
.windowStyle(.plain)
}
}
Here we define a WindowGroup with id
of 'Video' with the plain windowStyle.
The VideoSceneView
, provided in the example repo, is a good example of how you can implement any number of things in your multi-scene app.
We can open that scene from our NativeScript app as follows:
import { openScene } from '@nativescript/swift-ui';
openScene({
id: 'Video'
});
The video player opens in a new window which can be placed anywhere in the 3d space. Calling openScene
again with the same id
will always dismiss an already open scene with a matching id
.
You can also provide data for any number of SwiftUI @State properties as desired when opening or updating a scene.
import { openScene } from '@nativescript/swift-ui';
openScene({
id: 'Video',
data: {
url: '<any link to a playable video resource, mp4, etc.>'
}
});
This allows SwiftUI to consume the contextual data for anything needed as follows:
struct VideoSceneView: View {
@State var context: NativeScriptSceneContext?
func createPlayer(url: String) -> AVPlayer {
let player = AVPlayer(url: URL(string: url)!)
player.play()
return player
}
var body: some View {
ZStack {
if context != nil {
VideoPlayer(player: player)
.scaledToFill()
} else {
EmptyView()
}
}.onAppear {
setupContext()
}
}
func setupContext() {
context = NativeScriptSceneRegistry.shared.getContextForId(id: "Video")
let url = context!.data["url"] as! String
player = createPlayer(url: url)
}
}
We can update that scene dynamically from our NativeScript app as follows:
import { updateScene } from '@nativescript/swift-ui';
updateScene({
id: 'Video',
data: {
url: '<new url to play>'
}
});
We can support dynamic updates to any scene views by adding an onReceive modifier:
var body: some View {
ZStack {
// video player
}.onReceive(NotificationCenter.default
.publisher(for: NSNotification.Name("NativeScriptUpdateScene")), perform: { obj in
// allow our scene data to be updated if already opened!
setupContext()
}).onAppear {
// upon first open
setupContext()
}
}
The debut of Particle Emitters with visionOS brings an exciting API to the table. Let's look at how to interact with particles through Immersize Spaces.
An Immersize Space is another type of Scene and we can use RealityKit to display particle systems within it.
Let's look at using a ParticleEmitterComponent in addition to exploring Vortex by @twostraws which is a Swift Package that brings some nice additions.
We can modify our nativescript.config.ts
to include the SPM (Swift Package Manager) for visionos:
visionos: {
SPMPackages: [
{
name: 'Vortex',
libs: ['Vortex'],
repositoryURL: 'https://github.com/twostraws/Vortex.git',
version: '1.0.0'
}
]
}
We can then try out a sample view setup from Vortex:
struct FirefliesView: View {
var body: some View {
VortexViewReader { proxy in
ZStack(alignment: .bottom) {
VortexView(.fireflies.makeUniqueCopy()) {
Circle()
.fill(.white)
.frame(width: 32)
.blur(radius: 3)
.blendMode(.plusLighter)
.tag("circle")
}
}
}
}
}
Let's add a new Scene to open:
@main
struct NativeScriptApp: App {
var body: some Scene {
NativeScriptMainWindow()
WindowGroup(id: "Fireflies") {
FirefliesView()
}
.windowStyle(.plain)
We can now open the dynamic particle effect via openScene
in our NativeScript app:
import { openScene } from '@nativescript/swift-ui';
openScene({
id: 'Fireflies'
});
Let's now try adding an Immersize Space with Particles:
@main
struct NativeScriptApp: App {
@State private var immersionStyle: ImmersionStyle = .mixed
var body: some Scene {
NativeScriptMainWindow()
ImmersiveSpace(id: "ParticleEmitter") {
ParticleEmitterView()
}
.immersionStyle(selection: $immersionStyle, in: .mixed, .full)
We can setup our ParticleEmitterView
to also support dynamic data from our NativeScript app:
struct ParticleEmitterView: View {
@State var context: NativeScriptSceneContext?
@State var preset: String = "magic"
var body: some View {
ZStack {
if context != nil {
RealityView { content in
var particles = ParticleEmitterComponent.Presets.magic
switch preset {
case "fireworks":
particles = ParticleEmitterComponent.Presets.fireworks
default:
particles = ParticleEmitterComponent.Presets.magic
}
let particleEntity = Entity()
particleEntity.components[ParticleEmitterComponent.self] = particles
content.add(particleEntity)
}
} else {
EmptyView()
}
}.onAppear {
setupContext()
}
}
func setupContext() {
context = NativeScriptSceneRegistry.shared.getContextForId(id: "ParticleEmitter")
preset = context!.data["preset"] as! String
}
}
We can use openScene
in our NativeScript app again but this time we'll use the isImmersive
option to designate this is specifically to open an immersive space:
import { openScene } from '@nativescript/swift-ui';
openScene({
id: 'ParticleEmitter',
isImmersive: true,
data: {
preset: 'fireworks'
}
});
Neat stuff!
nStudio is recognized for establishing a healthy open source governance model to serve the global communities interest around NativeScript. If you are in need of professional assistance on your project, nStudio offers services spanning multiple disciplines and can be reached at [email protected].