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 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#

  1. Open Xcode and select New Project. If Xcode is already open, go to File > New > Project.

  2. From the template menu bar, select visionOS, then select App, and click Next.

  3. 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.

  4. 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.

  5. 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.

  1. In the Project Navigator, select your project, then select your visionOS target.

  2. Go to the Signing & Capabilities tab, or edit your entitlements file directly (e.g., Entitlements.plist).

  3. 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:

  1. Select your project in the Project Navigator.

  2. Select your visionOS target.

  3. 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 addyou simply import it.

  1. 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()
            }
        }
    }
    
  2. 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.

  1. Create a new Swift file named AppModel.swift and add the following code:

    import SwiftUI
    
    @MainActor
    @Observable
    class AppModel {
        enum ImmersiveSpaceState {
            case closed
            case inTransition
            case open
        }
        var immersiveSpaceState = ImmersiveSpaceState.closed
    }
    

    Note

    The @MainActor annotation is required because FoveatedStreamingSession is main actor-isolated. Any class that creates or accesses the session must also be on the main actor.

Set Up the Foveated Streaming Session#

  1. Open your main app file (My_First_FoveatedStreaming_ClientApp.swift). You need to create a FoveatedStreamingSession and an AppModel, then declare an ImmersiveSpace with the foveatedStreaming initializer 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:

    • FoveatedStreamingSession replaces CloudXRSession. It is created with a simple initializerno configuration struct is needed at initialization.

    • ImmersiveSpace(foveatedStreaming:content:) replaces the standard ImmersiveSpace with a CloudXRSessionComponent. It automatically handles session rendering, so you do not need manual entity setup.

    • The session is passed directly to ContentView and the ImmersiveSpace initializer rather than injected via .environment() as with CloudXRSession.

    • 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 call openImmersiveSpace(id:) manually.

  2. Build to check for errors.

Enter Your Connection Details#

Next, build the UI for entering the streaming endpoint IP address and port.

  1. 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.Endpoint to specify the connection target. Use .local(ipAddress:port:) for a direct IP connection (using Network.framework types IPv4Address and NWEndpoint.Port), .systemDiscovered for 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 call openImmersiveSpace(id:) manually.

    • Session status: Foveated Streaming uses session.status (not session.state as 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’s disconnect() is asynchronous, and must be awaited.

  2. 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.

  1. Create a new Swift file named ImmersiveView.swift and 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 CloudXRSessionComponent or add it to an entity. The ImmersiveSpace(foveatedStreaming:content:) initializer handles all session rendering internally.

    • The ImmersiveView is 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.

  1. Make sure your streaming application is running and reachable on the network.

  2. Build the app and run it on your Apple Vision Pro device.

  3. Enter the streaming endpoint IP address and port (default: 55000), then tap Connect.

    • Alternatively, leave the IP address field as 0.0.0.0 or empty to use .systemDiscovered, which triggers the built-in QR-code pairing flow.

  4. Foveated Streaming connects to the streaming application. Because immersivePresentationBehaviors was set to .automatic, the session automatically opens the immersive space when streaming begins. You should see streamed XR content with dynamic foveated streaminghigher 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.