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 |
|
|
Identical |
Session Lifecycle |
|
|
Very similar |
Session State |
|
|
Very similar |
Data Channels |
|
|
Very similar |
Session Management |
|
|
Similar concept |
Immersive Space |
Standard |
|
Similar concept |
Connection Configuration |
|
|
Simplified |
Sending Messages |
|
|
Different name and error handling |
Concurrency Model |
Mix of sync and async methods |
Fully async ( |
More consistent |
Actor Isolation |
Not strictly enforced |
|
Different requirement |
Immersive Presentation |
Manual via |
Automatic via |
Different approach |
Immersive Rendering |
Manual via |
Automatic via |
Different approach |
Channel API (get) |
|
|
Renamed, actor-isolated |
Channel API (available) |
|
|
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 |
|
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#
Open your Xcode project.
Select your project in the navigator.
Select the visionOS target.
Go to General > Frameworks, Libraries, and Embedded Content.
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
.localendpoint now usesNetwork.frameworktypes (IPv4Address/IPv6AddressandNWEndpoint.Port).The
.remoteendpoint supports signaling headers for cloud deployments, similar to CloudXR Framework’sremoteSecureconnection 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 |
|---|---|
|
|
|
|
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 contextMain 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-sessionentitlement to your visionOS target.Remove CloudXRKit package dependency from visionOS target.
Update deployment target to visionOS 26.4+.
Replace
import CloudXRKitwithimport FoveatedStreaming.Update session type from
Session?toFoveatedStreamingSession.Add
@MainActorto classes that hold or access the session.Replace
ImmersiveSpacewithImmersiveSpace(foveatedStreaming:content:).Remove
CloudXRSessionComponentusage.Set
session.immersivePresentationBehaviors = .automatic(openImmersiveSpace, dismissImmersiveSpace)before connecting (replaces manualopenImmersiveSpace(id:)calls).Remove manual
openImmersiveSpace(id:)anddismissImmersiveSpace()calls for session lifecycle (the session manages this automatically).Update
session.statetosession.status.Update connection code to use
session.connect(endpoint:).Make
disconnect(),pause(),resume()calls async withawait.Update data channel code:
getMessageChannel(_:)→messageChannel(for:),sendServerMessage(_:)→sendMessage(_:).Handle
@MainActorisolation onavailableMessageChannelsandmessageChannel(for:)— access only from main actor.Replace
ChannelInfo.uuidkeying withMessageChannel.ID(opaque, no.uuidproperty).Remove unsupported features (GDN, authentication, HUD, feedback, etc.).
Test thoroughly on the device.