CloudXR Client for visionOS Using Foveated Streaming#
This tutorial walks you through building a simple visionOS application that uses Apple’s Foveated Streaming framework to stream XR content from a streaming application running CloudXR Runtime.
System Requirements#
Xcode 26.0 or later
visionOS 26.4 SDK or later
An Apple Vision Pro device (Foveated Streaming requires device hardware for eye tracking)
Option 1: Use the Xcode Foveated Streaming Template (Recommended)#
Xcode 26 includes a template that creates a Foveated Streaming project with all the required boilerplate code already in place. This is the fastest way to get started.
Open Xcode and select New Project (or go to File > New > Project).
Select the Multiplatform tab, then choose the Foveated Streaming template and click Next.
Enter your product name, team, and organization identifier, then click Next and choose a save location.
Xcode creates a project that already includes:
The
FoveatedStreamingimportA
FoveatedStreamingSessioninstanceAn
ImmersiveSpace(foveatedStreaming:content:)sceneThe
com.apple.developer.foveated-streaming-sessionentitlementA basic connection UI
Build and run to verify the project compiles. You can then customize the connection UI, add your own content, and connect to a local streaming endpoint running CloudXR Runtime.
If you used the template, you can skip ahead to Connecting to a Streaming Application to connect to a local streaming endpoint.
Option 2: Create the Project Manually#
If you prefer to set up the project from scratch or need to add Foveated Streaming to an existing app, follow the manual steps below.
Make Your Project#
Open Xcode and select New Project. If Xcode is already open, go to File > New > Project.
From the template menu bar, select visionOS, then select App, and click Next.
Set up and configure the following options:
Product Name: My First Foveated Streaming Client
Team: Your team (selected from the dropdown)
Organization Identifier: An identifier (provided by your organization and team)
Initial scene: Window
For now, assume that you want your users to start in a connection window.
Immersive space renderer: None
You will set this up later with Foveated Streaming.
Click Next. Xcode prompts you to choose where to save your project. Once you have selected a destination, click Create.
Note
Make sure you know how to run apps on both the visionOS simulator and a Vision Pro device. If you are unsure, refer to Running your app in Simulator or on a device.
Build your project and make sure everything works. To build, click the “Play” icon at the top left of the Xcode window.
Add the Foveated Streaming Entitlement#
Foveated Streaming requires a specific entitlement to function. You must add it to your visionOS target before using the framework.
In the Project Navigator, select your project, then select your visionOS target.
Go to the Signing & Capabilities tab, or edit your entitlements file directly (e.g.,
Entitlements.plist).Add the following entitlement:
<key>com.apple.developer.foveated-streaming-session</key> <true/>
Your entitlements file should look like this:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.developer.foveated-streaming-session</key> <true/> </dict> </plist>
Set the Deployment Target#
Ensure that your visionOS deployment target is set to 26.4 or later:
Select your project in the Project Navigator.
Select your visionOS target.
Under General > Minimum Deployments, set visionOS to 26.4.
Import Foveated Streaming#
Unlike CloudXR Framework, Foveated Streaming is a system framework built into visionOS. There is no Swift package to add—you simply import it.
From the Project Navigator, go to your main application source file,
My_First_FoveatedStreaming_ClientApp.swift, and add the following import:import FoveatedStreaming
Now your file should look like this:
import SwiftUI import FoveatedStreaming @main struct My_First_FoveatedStreaming_ClientApp: App { var body: some Scene { WindowGroup { ContentView() } } }
Build your application and run it on the simulator to verify there are no build errors.
Create the App Model#
Foveated Streaming requires @MainActor isolation for its session. Create an AppModel class to manage app-wide state.
Create a new Swift file named
AppModel.swiftand add the following code:import SwiftUI @MainActor @Observable class AppModel { enum ImmersiveSpaceState { case closed case inTransition case open } var immersiveSpaceState = ImmersiveSpaceState.closed }
Note
The
@MainActorannotation is required becauseFoveatedStreamingSessionis main actor-isolated. Any class that creates or accesses the session must also be on the main actor.
Set Up the Foveated Streaming Session#
Open your main app file (
My_First_FoveatedStreaming_ClientApp.swift). You need to create aFoveatedStreamingSessionand anAppModel, then declare anImmersiveSpacewith thefoveatedStreaminginitializer for immersive rendering.Update the file to look like this:
import SwiftUI import FoveatedStreaming @main struct My_First_FoveatedStreaming_ClientApp: App { @State private var appModel = AppModel() @State private var session = FoveatedStreamingSession() var body: some Scene { WindowGroup { ContentView(session: session) .environment(appModel) } .defaultSize(width: 480, height: 640) ImmersiveSpace(foveatedStreaming: session) { ImmersiveView() .environment(appModel) } .immersionStyle(selection: .constant(.progressive), in: .progressive) } }
Key differences from CloudXR Framework:
FoveatedStreamingSessionreplacesCloudXRSession. It is created with a simple initializer—no configuration struct is needed at initialization.ImmersiveSpace(foveatedStreaming:content:)replaces the standardImmersiveSpacewith aCloudXRSessionComponent. It automatically handles session rendering, so you do not need manual entity setup.The session is passed directly to
ContentViewand theImmersiveSpaceinitializer rather than injected via.environment()as withCloudXRSession.The immersive space is managed automatically by the session. Before connecting, set
session.immersivePresentationBehaviors = .automatic(openImmersiveSpace, dismissImmersiveSpace)to let the session open and dismiss the immersive space based on the streaming lifecycle. You do not callopenImmersiveSpace(id:)manually.
Build to check for errors.
Enter Your Connection Details#
Next, build the UI for entering the streaming endpoint IP address and port.
Open
ContentView.swift. Replace the default content with the following:import SwiftUI import Network import FoveatedStreaming struct ContentView: View { let session: FoveatedStreamingSession @AppStorage("ipAddress") private var ipAddress: String = "" @AppStorage("port") private var port: String = "55000" @Environment(\.openImmersiveSpace) var openImmersiveSpace @Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace var body: some View { VStack { Form { HStack { Text("IP Address") Spacer() TextField("0.0.0.0", text: $ipAddress) .autocorrectionDisabled(true) .keyboardType(.numbersAndPunctuation) .textInputAutocapitalization(.never) .onSubmit { ipAddress = ipAddress.trimmingCharacters( in: .whitespacesAndNewlines ) } } HStack { Text("Port") Spacer() TextField("55000", text: $port) .autocorrectionDisabled(true) .keyboardType(.numberPad) .textInputAutocapitalization(.never) .onSubmit { port = port.trimmingCharacters( in: .whitespacesAndNewlines ) } } } Text("Status: \(session.status.description)") .padding() Button("Connect") { Task { @MainActor in let endpoint: FoveatedStreamingSession.Endpoint if ipAddress.isEmpty || ipAddress == "0.0.0.0" { endpoint = .systemDiscovered } else if let ip = IPv4Address(ipAddress), let portNum = NWEndpoint.Port(port) { endpoint = .local(ipAddress: ip, port: portNum) } else { return } // Tell the session to manage the immersive space automatically session.immersivePresentationBehaviors = .automatic( openImmersiveSpace, dismissImmersiveSpace ) do { try await session.connect(endpoint: endpoint) } catch { print("Failed to connect: \(error)") } } } .padding() .disabled(!session.isDisconnected) if !session.isDisconnected { Button("Disconnect") { Task { await session.disconnect() } } .padding() } } .padding() } } private extension FoveatedStreamingSession { var isDisconnected: Bool { switch status { case .disconnected, .initialized, .connecting: return true default: return false } } }
Key points:
Endpoint types: Foveated Streaming uses
FoveatedStreamingSession.Endpointto specify the connection target. Use.local(ipAddress:port:)for a direct IP connection (usingNetwork.frameworktypesIPv4AddressandNWEndpoint.Port),.systemDiscoveredfor built-in QR-code pairing, or.remote(serverName:signalingHeaders:)for cloud connections.Automatic immersive space: Before connecting, set
session.immersivePresentationBehaviors = .automatic(openImmersiveSpace, dismissImmersiveSpace)so the session automatically opens the immersive space when streaming begins and dismisses it when streaming ends. You do not callopenImmersiveSpace(id:)manually.Session status: Foveated Streaming uses
session.status(notsession.stateas in CloudXRKit) to track the session lifecycle. The status enum includes:.initialized,.connecting,.connected,.paused,.pausing,.resuming,.disconnecting, and.disconnected.Async disconnect: Unlike CloudXRKit, where
disconnect()is synchronous, Foveated Streaming’sdisconnect()is asynchronous, and must be awaited.
Build and run. You should see an IP address and port entry form with a Connect button.
Create the Immersive View#
Foveated Streaming handles rendering automatically through the ImmersiveSpace(foveatedStreaming:content:) initializer. Your immersive view only needs to provide any overlay content you want on top of the stream.
Create a new Swift file named
ImmersiveView.swiftand add the following:import SwiftUI import RealityKit struct ImmersiveView: View { @Environment(AppModel.self) var appModel var body: some View { RealityView { content in // The ImmersiveSpace(foveatedStreaming:) initializer handles // session rendering automatically. // Add any overlay content here if needed. } .onAppear { appModel.immersiveSpaceState = .open } .onDisappear { appModel.immersiveSpaceState = .closed } } }
Key difference from CloudXR Framework:
You do not need to create a
CloudXRSessionComponentor add it to an entity. TheImmersiveSpace(foveatedStreaming:content:)initializer handles all session rendering internally.The
ImmersiveViewis only used for overlay content (such as gesture targets or UI elements on top of the stream).Optionally, attach
.foveatedStreamingPauseSheet(session:)to a view to display or suppress the system pause sheet.
Connecting to a Streaming Application#
Now you are ready to connect to a streaming application running CloudXR Runtime.
Make sure your streaming application is running and reachable on the network.
Build the app and run it on your Apple Vision Pro device.
Enter the streaming endpoint IP address and port (default:
55000), then tap Connect.Alternatively, leave the IP address field as
0.0.0.0or empty to use.systemDiscovered, which triggers the built-in QR-code pairing flow.
Foveated Streaming connects to the streaming application. Because
immersivePresentationBehaviorswas set to.automatic, the session automatically opens the immersive space when streaming begins. You should see streamed XR content with dynamic foveated streaming—higher quality where you look, reduced quality in the periphery.
Note
When using .systemDiscovered, the system presents a QR-code pairing interface. The streaming application displays a QR code that the Apple Vision Pro scans to establish the connection. This replaces the need for manually entering IP addresses.