Adding spatial Persona support to an activity
Update your SharePlay activities to support spatial Personas and the shared context when running in visionOS.
Overview
A person who participates in SharePlay activities on Apple Vision Pro has the option to participate using their spatial Persona. The system arranges spatial Personas around the activity content, giving each person a clear view of the content and each other. Each person sees the facial expressions of other participants, what they’re looking at, and where they’re pointing. This experience creates the feeling that they’re in the same physical space interacting with shared content and each other.
To maintain the experience when spatial Personas are visible, apps share additional information to maintain the shared context for the activity. Because participants can see where others are looking, your app’s content must look the same for everyone. Share any additional information you need to keep everyone’s content in sync visually. For example, synchronize your window’s scroll position to ensure everyone sees the same portion of that window.
You don’t need to define new GroupActivity types specifically to support spatial Personas. The system automatically displays spatial Personas for existing activities that take place in a window or volume. However, if you support activities in a Full Space, you need to do additional work to support spatial Personas for your experience. For information about how to define activities in your app, see Defining your app’s SharePlay activities.
Associate SharePlay activities with your app’s scenes
You use scenes to manage the content for your app’s windows, volumes, and immersive spaces. You also use scenes to display any activity-related content. When a participant joins an activity, the system selects or opens the scene that supports the activity. If your app has only one scene and one window, the system has only one choice. However, if your app has multiple scenes, you need to help the system choose the correct one.
For each of your app’s scenes, activation conditions tell the system how to handle your app’s SharePlay activities. You also use activation conditions to specify how to handle NSUserActivity objects, and other incoming events. To specify activation conditions for one of your scenes:
For SwiftUI, add the handlesExternalEvents(preferring:allowing:) modifier to your scene type.
For UIKit, configure the scene’s activationConditions property in the scene(_:willConnectTo:options:) method of your scene delegate.
When you add an activation condition to a scene, you specify a string that uniquely identifies your SharePlay activity. The string can be anything you want, as long as it creates a unique connection between the activity and the specific scene. When there is a one-to-one correspondence between an activity and scene, the string in the activityIdentifier property of your activity object is a good choice. When there isn’t a one-to-one correspondence between scene and activity, you must create a string that uniquely identifies the scene. For example, in a document-based app, you might specify the name of a document to direct the system to the scene that contains the document.
The following code defines a SwiftUI scene for a window, and associates two activities with that window. Because the app uses the same scene to display content for two different activities, the activation conditions contain the identifiers for the two GroupActivity types. When either activity starts, the system directs the activity to the window containing the scene. If no such window is open, the system launches the app and creates the window as needed.
let activationConditions : Set = ["com.mycompany.MySharePlayActivity",
"com.mycompany.MyUserActivity"]
var body: some Scene {
WindowGroup {
ContentView()
.handlesExternalEvents(preferring: [], allowing: activationConditions)
}In addition to configuring your scene, you need to update your activity object’s metadata with the appropriate string. Specify the string using the sceneAssociationBehavior property of your activity object’s GroupActivityMetadata structure. Assign the default value to this property if you use the activity identifier as the string. For any custom strings, assign the content(_:) value and specify your string. To disable scene association for the activity and handle the presentation of activity-related UI yourself, specify the none value.
Configure your app’s support for spatial Personas
The system provides some default behaviors to make it easier for you to adopt spatial Personas in your app. For activities that take place in a window or volume, the system chooses a default arrangement of the spatial Personas around your scene. The system also adorns the scene with a Share menu, which provides a visual indication when sharing is active and contains controls to start activities. The system doesn’t display spatial Personas in a Full Space by default.
To change the default behavior of your app, update the SystemCoordinator of your GroupSession before you call the session’s join() method. Use the SystemCoordinator to:
Specify a different arrangement for spatial Personas.
Specify whether your app supports spatial Personas when a Full Space is visible.
The following example changes the arrangement of spatial Personas to be side by side facing the app’s content. The code also tells the system that the app supports spatial Personas when a Full Space is open.
if let coordinator = await newSession.systemCoordinator {
var config = SystemCoordinator.Configuration()
config.spatialTemplatePreference = .sideBySide.contentExtent(200)
config.supportsGroupImmersiveSpace = true
coordinator.configuration = config
}
// Join the session.
newSession.join()Spatial template preferences tell the system whether to arrange participants side by side, in a circle, or in an arrangement that supports conversation. For each template, the system uses the size of your content to determine how far away to place spatial Personas. For example, the system moves them farther away when your content is larger, and brings them closer together when your content is small. To override this distance, add the contentExtent(_:) modifier to your template preference. That modifier tells the system to use the distance you specify, instead of the content size.
If you enable support for spatial Personas in a Full Space, you must do additional work to support that experience. When an activity moves to a Full Space, the system creates a shared coordinate system for all participants and the content. In this new coordinate system, participants are no longer at the center of the coordinate system, which might require you to update the position of your content. For more information, see Place content relative to a participant in an immersive space.
Synchronize additional data when spatial Personas are visible
When spatial Personas are visible, it’s your responsibility to maintain the shared context for your activity. The shared context includes both the content you display and how that content appears to each participant. People can gesture or look at content using their spatial Persona, so it’s important that everyone sees the same thing when that happens. For example, Freeform synchronizes both the page content and the current scroll position to ensure everyone sees the same portion of the page.
When defining an activity, define additional data messages to synchronize any information you need to maintain the shared context. When the current participant shows their spatial Persona, send the extra messages when the participant makes relevant changes to the activity. For example, send them when the person scrolls the activity window. Similarly, receive those messages and incorporate the results into your app when the spatial Persona is visible.
To determine when someone’s spatial Persona is visible, monitor the localParticipantStates of your session’s SystemCoordinator object. The AsyncSequence in this property reports participant-related state changes, including changes to the visibility of their spatial Persona. Get the isSpatial property of the returned SystemCoordinator.ParticipantState structure and use it to configure your app’s behavior. The following example uses a task to update the game state to accommodate spatial Personas. When the current participant is spatial, the game sends additional messages to maintain the shared context.
Task.detached { @MainActor in
for await state in coordinator.localParticipantStates {
if state.isSpatial {
// Synchronize additional data for the shared context.
gameModel.isSpatial = true
} else {
// Don't synchronize additional data.
gameModel.isSpatial = false
}
}
}Update the immersion level automatically for a Full Space
If one participant opens an immersive space as part of an activity, the system doesn’t automatically open the same immersive space for other participants. Making a transition to an immersive space is a significant change, and some participants might not want to make the transition right away. For example, if a participant is on a phone call, they might not want another app to open an immersive space and hide their call. Instead, the system reports when transitions to immersive spaces occur, and lets you decide when to transition other participants automatically.
To determine when any participant transitions to an immersive space, monitor the groupImmersionStyle property of your SystemCoordinator object. This property contains an AsyncSequence that reports the most recent immersion style that a participant adopts. When a participant presents an immersive space, or when they change the immersion style of the current space, the system updates the sequence with the new value. The following example shows a task that opens the immersive space with the same style as the group immersion style.
Task.detached {
for await immersionStyle in systemCoordinator.groupImmersionStyle {
if let immersionStyle {
// Open an immersive space with the same style.
}
else {
// Dismiss the immersive space.
}
}
}The system reports nil for the immersion style when a participant dismisses their open immersive space. Use that option to dismiss the space for other participants and return to a window or volume.
Place content relative to a participant in an immersive space
When an activity takes place in an immersive space, the system creates a shared coordinate system for the participants and content. In this new coordinate space, the origin of the coordinate space is not the same as the origin of any of the participants. If an activity-related window or volume is visible, the system places the window or volume at the new origin. If the activity doesn’t use a window or volume, the system arranges the participants in a circle and sets the origin of the coordinate space to the circle’s center. The following figure shows the origin of the shared coordinate system for a window, volume, and immersive space relative to several spatial Personas.
If you place content directly into your immersive space, you must adjust the position of that content to account for the modified coordinate space. To do that, you first determine the person’s location in the new coordinate system. Read the person’s location in the scene using a GeometryProxy3D. The proxy for this reader contains a doc://com.apple.documentation/documentation/swiftui/geometryproxy3d/immersivespacedisplacement(in:) method, which you can use to locate the origin of the shared coordinate space. Call the method using the global value as a parameter to get the displacement of the coordinate space’s origin, relative to the person’s location. Invert that displacement value to get the location of the participant relative in the new coordinate space. The following example shows how to use a GeometryProxy3D to get the offset to the current participant and use that information to place a custom view:
var body: some Scene {
ImmersiveSpace(id:"earth") {
GeometryReader3D { proxy in
let displacement =
proxy.immersiveSpaceDisplacement(in: .global).inverse
CustomView()
.offset(displacement.position)
.rotation3DEffect(displacement.rotation)
}
}
}The displacement between a person and the origin of the scene doesn’t change during the course of your activity even when the participant moves. The system sets the origin of an immersive space when you first present it, and updates it only once when changing it to a shared coordinate space.