Migration from CloudXR Framework#

This page provides step-by-step instructions for migrating an existing visionOS Xcode project from the CloudXR Framework (CloudXRKit) to Apple’s Foveated Streaming framework.

Overview#

Foveated Streaming is Apple’s native framework for XR streaming on visionOS, providing similar functionality to CloudXRKit but with a different API surface. Both frameworks are built on the same CloudXR technology, making migration a straightforward API translation.

This migration involves:

  • Adding the Foveated Streaming entitlement

  • Removing the CloudXRKit package dependency

  • Updating import statements

  • Replacing session management code

  • Updating immersive space handling

  • Migrating data channel (message channel) code

Framework Comparison#

High-Level API Comparison#

The table below provides a high-level comparison of the API semantics between CloudXRKit and Foveated Streaming:

Aspect

CloudXRKit

Foveated Streaming

Similarity

Receiving Messages

receivedMessageStream async sequence

receivedMessageStream async sequence

Identical

Session Lifecycle

connect(), disconnect(), pause(), resume()

connect(endpoint:), disconnect(), pause(), resume()

Very similar

Session State

session.state returns SessionState enum

session.status returns Status enum

Very similar

Data Channels

MessageChannel with ChannelInfo identifier

MessageChannel with MessageChannel.ID identifier

Very similar

Session Management

Session protocol / CloudXRSession class

FoveatedStreamingSession class

Similar concept

Immersive Space

Standard ImmersiveSpace

ImmersiveSpace(foveatedStreaming:content:)

Similar concept

Connection Configuration

CloudXRKit.Config struct with multiple options

Endpoint enum (.local, .systemDiscovered, or .remote)

Simplified

Sending Messages

sendServerMessage(_:) -> Bool

sendMessage(_:) throws

Different name and error handling

Concurrency Model

Mix of sync and async methods

Fully async (async/await)

More consistent

Actor Isolation

Not strictly enforced

@MainActor required for session

Different requirement

Immersive Presentation

Manual via openImmersiveSpace(id:)

Automatic via session.immersivePresentationBehaviors

Different approach

Immersive Rendering

Manual via CloudXRSessionComponent on Entity

Automatic via ImmersiveSpace(foveatedStreaming:)

Different approach

Channel API (get)

session.getMessageChannel(channelInfo)

session.messageChannel(for: channelID) (@MainActor)

Renamed, actor-isolated

Channel API (available)

session.availableMessageChannels (nonisolated)

session.availableMessageChannels (@MainActor)

Same name, different isolation

Architecture Comparison#

Component

CloudXRKit

Foveated Streaming

Framework Type

Third-party SDK (NVIDIA)

First-party Apple framework

Package Distribution

Swift Package (GitHub)

System framework (built into visionOS)

Minimum visionOS

visionOS 2.4+

visionOS 26.4+

Entitlement

None required by framework

com.apple.developer.foveated-streaming-session (required)

Project Configuration#

Step 1: Add Required Entitlement#

Foveated Streaming requires a specific entitlement to function. Add the following to your visionOS target’s entitlements file (e.g., YourApp.entitlements):

<key>com.apple.developer.foveated-streaming-session</key>
<true/>

Note

CloudXRKit does not require a specific entitlement from the framework itself. The above entitlement is mandatory for Foveated Streaming.

Step 2: Remove CloudXRKit Package Dependency#

  1. Open your Xcode project.

  2. Select your project in the navigator.

  3. Select the visionOS target.

  4. Go to General > Frameworks, Libraries, and Embedded Content.

  5. Remove CloudXRKit from the list.

Step 3: Update Deployment Target#

Ensure your visionOS deployment target is set to 26.4 or later:

XROS_DEPLOYMENT_TARGET = 26.4

API Migration#

Import Statements#

Before:

import CloudXRKit

After:

import FoveatedStreaming

Session Type#

Before:

var session: Session?
// or
var session: CloudXRSession?

After:

var session: FoveatedStreamingSession = FoveatedStreamingSession()

Note

FoveatedStreamingSession initializer is @MainActor isolated. Ensure your containing class is marked with @MainActor.

Session State#

Before:

if session.state == .connected {
    // ...
}

After:

if session.status == .connected {
    // ...
}

Immersive Space#

Before:

@Environment(\.openImmersiveSpace) var openImmersiveSpace

ImmersiveSpace(id: "immersive") {
    RealityView { content in
        let entity = Entity()
        entity.components[CloudXRSessionComponent.self] = .init(session: session)
        content.add(entity)
    }
}

// Opening
await openImmersiveSpace(id: "immersive")

After:

@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace

// In the App struct's body:
ImmersiveSpace(foveatedStreaming: session) {
    RealityView { content in
        // Add any overlay content here
        // Session rendering is handled automatically
    }
}
.immersionStyle(selection: .constant(.progressive), in: .progressive)

// Before connecting, configure automatic immersive space management:
session.immersivePresentationBehaviors = .automatic(openImmersiveSpace, dismissImmersiveSpace)
try await session.connect(endpoint: endpoint)

Important

ImmersiveSpace(foveatedStreaming:content:) automatically handles session rendering. You do not need to add a CloudXRSessionComponent or equivalent.

The immersive space is not opened manually with openImmersiveSpace(id:). Instead, set session.immersivePresentationBehaviors = .automatic(openImmersiveSpace, dismissImmersiveSpace) before calling connect(endpoint:). The session will automatically open the immersive space when streaming begins and dismiss it when streaming ends.

Connection#

Before:

var config = CloudXRKit.Config()
config.connectionType = .local(ip: "192.168.1.100")
config.resolutionPreset = .standardPreset

let session = CloudXRSession(config: config)
try await session.connect()

After:

import Network

let session = FoveatedStreamingSession()

// For local connection with IP and port (using Network.framework types)
let endpoint = FoveatedStreamingSession.Endpoint.local(
    ipAddress: IPv4Address("192.168.1.100")!, port: NWEndpoint.Port(integerLiteral: 55000)
)

// For system-discovered connection (uses built-in QR-code pairing)
let endpoint = FoveatedStreamingSession.Endpoint.systemDiscovered

// For remote/cloud connection with signaling headers
let endpoint = FoveatedStreamingSession.Endpoint.remote(
    serverName: "proxy.example.com",
    signalingHeaders: ["Authorization": "Bearer \(token)"]
)

try await session.connect(endpoint: endpoint)

Note

  • When using .systemDiscovered, Foveated Streaming provides a built-in QR-code pairing flow that the system handles automatically. This replaces the need for custom authentication mechanisms like Starfleet or Guest modes in CloudXRKit.

  • The .local endpoint now uses Network.framework types (IPv4Address / IPv6Address and NWEndpoint.Port).

  • The .remote endpoint supports signaling headers for cloud deployments, similar to CloudXR Framework’s remoteSecure connection type.

Pause Sheet#

Foveated Streaming provides an optional system pause sheet that can be displayed when the session is paused. Use the .foveatedStreamingPauseSheet(session:) view modifier to control it:

ImmersiveSpace(foveatedStreaming: session) {
    ImmersiveView()
        .foveatedStreamingPauseSheet(session: session)
}

Disconnection#

Before:

session.disconnect()  // Synchronous

After:

await session.disconnect()  // Asynchronous

Pause and Resume#

Before:

session.pause()
try session.resume()

After:

try await session.pause()
try await session.resume()

Code Changes#

App Model#

Before:

@Observable
public class AppModel {
    public var session: Session?
}

After:

@MainActor
@Observable
public class AppModel {
    public var session: FoveatedStreamingSession = FoveatedStreamingSession()
}

Note

The @MainActor annotation is required because FoveatedStreamingSession() initializer is main actor-isolated. Any other classes that access the session must also be marked with @MainActor.

Immersive View#

Before:

struct ImmersiveView: View {
    @Environment(AppModel.self) var appModel
    @State private var sessionEntity = Entity()

    var body: some View {
        RealityView { content in
            sessionEntity.name = "Session"
            if let session = appModel.session {
                sessionEntity.components[CloudXRSessionComponent.self] = .init(session: session)
            }
            content.add(sessionEntity)
        }
    }
}

After:

struct ImmersiveView: View {
    @Environment(AppModel.self) var appModel

    var body: some View {
        // ImmersiveSpace(foveatedStreaming:) handles session rendering automatically.
        // Just add any overlay content you need.
        RealityView { content in
            // Optional: Add gesture targets or other content
        }
    }
}

Data Channel Migration#

Channel Types#

CloudXRKit

Foveated Streaming

ChannelInfo

FoveatedStreamingSession.MessageChannel.ID

MessageChannel

FoveatedStreamingSession.MessageChannel

Getting Available Channels#

Before:

let channels: [ChannelInfo] = session.availableMessageChannels

After:

let channelIDs: Set<FoveatedStreamingSession.MessageChannel.ID> = session.availableMessageChannels

Getting a Channel#

Before:

if let channel = session.getMessageChannel(channelInfo) {
    // use channel
}

After:

if let channel = session.messageChannel(for: channelID) {
    // use channel
}

Note

messageChannel(for:) and availableMessageChannels are @MainActor-isolated. They must be called from the main actor (e.g., inside a Task { @MainActor in } block or from a @MainActor-annotated function).

MessageChannel.ID is an opaque identifier type and does not have a .uuid property like CloudXRKit’s ChannelInfo. Use the MessageChannel.ID value directly as dictionary keys or identifiers.

Sending Messages#

Before:

let success = channel.sendServerMessage(data)
if !success {
    // handle error
}

After:

do {
    try channel.sendMessage(data)
} catch {
    // handle error
}

Receiving Messages#

The two frameworks use similar async stream patterns:

for await message in channel.receivedMessageStream {
    if let text = String(data: message, encoding: .utf8) {
        print("Received: \(text)")
    }
}

Closing a Channel#

Before:

channel.close()

After:

channel.disconnect()

Common Issues#

Main Actor Isolation Errors#

Errors:

  • Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context

  • Main actor-isolated property 'session' can not be referenced from a nonisolated context

Cause: FoveatedStreamingSession is main actor-isolated. Any code that creates or accesses the session must run on the main actor.

Solution: Add @MainActor to your class, or access the session within a Task { @MainActor in } block:

@MainActor
@Observable
public class AppModel {
    public var session: FoveatedStreamingSession = FoveatedStreamingSession()
}

Immersive Space Not Opening#

Error: After calling session.connect(endpoint:), no immersive space appears.

Cause: Foveated Streaming does not use openImmersiveSpace(id:) to open its immersive space. The session must be told how to manage the immersive space lifecycle automatically.

Solution: Set immersivePresentationBehaviors on the session before calling connect(endpoint:):

session.immersivePresentationBehaviors = .automatic(openImmersiveSpace, dismissImmersiveSpace)
try await session.connect(endpoint: endpoint)

The openImmersiveSpace and dismissImmersiveSpace actions come from the SwiftUI environment (@Environment(\.openImmersiveSpace) and @Environment(\.dismissImmersiveSpace)).

MessageChannel.ID Has No uuid Property#

Error: Value of type 'FoveatedStreamingSession.MessageChannel.ID' has no member 'uuid'

Cause: Unlike CloudXRKit’s ChannelInfo which has a .uuid property (Data), Foveated Streaming’s MessageChannel.ID is an opaque identifier with no uuid accessor.

Solution: Use MessageChannel.ID directly as dictionary keys (it conforms to Hashable). If you need a Data key for compatibility with existing code, derive one from the hash value:

let syntheticKey = withUnsafeBytes(of: channelID.hashValue) { Data($0) }

Missing Foveated Streaming Entitlement#

Error: App crashes or fails to create streaming session.

Solution: Ensure your visionOS target’s entitlements file includes:

<key>com.apple.developer.foveated-streaming-session</key>
<true/>

Migration Checklist#

Use this checklist to ensure you have completed all migration steps:

  • Add com.apple.developer.foveated-streaming-session entitlement to your visionOS target.

  • Remove CloudXRKit package dependency from visionOS target.

  • Update deployment target to visionOS 26.4+.

  • Replace import CloudXRKit with import FoveatedStreaming.

  • Update session type from Session? to FoveatedStreamingSession.

  • Add @MainActor to classes that hold or access the session.

  • Replace ImmersiveSpace with ImmersiveSpace(foveatedStreaming:content:).

  • Remove CloudXRSessionComponent usage.

  • Set session.immersivePresentationBehaviors = .automatic(openImmersiveSpace, dismissImmersiveSpace) before connecting (replaces manual openImmersiveSpace(id:) calls).

  • Remove manual openImmersiveSpace(id:) and dismissImmersiveSpace() calls for session lifecycle (the session manages this automatically).

  • Update session.state to session.status.

  • Update connection code to use session.connect(endpoint:).

  • Make disconnect(), pause(), resume() calls async with await.

  • Update data channel code: getMessageChannel(_:)messageChannel(for:), sendServerMessage(_:)sendMessage(_:).

  • Handle @MainActor isolation on availableMessageChannels and messageChannel(for:) — access only from main actor.

  • Replace ChannelInfo.uuid keying with MessageChannel.ID (opaque, no .uuid property).

  • Remove unsupported features (GDN, authentication, HUD, feedback, etc.).

  • Test thoroughly on the device.

Additional Resources#