<!--
{
  "documentType" : "article",
  "framework" : "GroupActivities",
  "identifier" : "/documentation/GroupActivities/adding-spatial-persona-support-to-an-activity",
  "metadataVersion" : "0.1.0",
  "role" : "article",
  "title" : "Adding spatial Persona support to an activity"
}
-->

# 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`](/documentation/GroupActivities/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](/documentation/GroupActivities/defining-your-apps-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 <doc://com.apple.documentation/documentation/Foundation/NSUserActivity>
objects, and other incoming events. To specify activation conditions for
one of your scenes:

- For SwiftUI, add the <doc://com.apple.documentation/documentation/SwiftUI/View/handlesExternalEvents(preferring:allowing:)> modifier to your scene type.
- For UIKit, configure the scene’s <doc://com.apple.documentation/documentation/UIKit/UIScene/activationConditions> property in the <doc://com.apple.documentation/documentation/UIKit/UISceneDelegate/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`](/documentation/GroupActivities/GroupActivity/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`](/documentation/GroupActivities/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`](/documentation/GroupActivities/GroupActivityMetadata/sceneAssociationBehavior) property
of your activity object’s [`GroupActivityMetadata`](/documentation/GroupActivities/GroupActivityMetadata) structure. Assign
the [`default`](/documentation/GroupActivities/SceneAssociationBehavior/default) value to this property if you
use the activity identifier as the string. For any custom strings, assign
the [`content(_:)`](/documentation/GroupActivities/SceneAssociationBehavior/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`](/documentation/GroupActivities/SceneAssociationBehavior/none)
value.

Although the activation conditions and the activity’s `SceneAssociationBehavior` determine the automatically associated scene, you can explicitly specify a different scene using the <doc://com.apple.documentation/documentation/SwiftUI/View/groupActivityAssociation(_:)> modifier.
Use this modifier on your view to set its activity association to [`primary(_:)`](/documentation/GroupActivities/GroupActivityAssociationKind/primary(_:)), and include an identifier that reflects the content of the view.

The system shares the most recently opened window whose [`GroupActivityAssociationKind`](/documentation/GroupActivities/GroupActivityAssociationKind) is set to `.primary(_:)`.
To stop sharing a scene, set the `GroupActivityAssociationKind` to `nil`.

The following example shows how to dynamically change the shared scene depending on changes in your app:

```swift
struct GameApp: App {
    @State var isShowingVideo: Bool = true

    var body: some Scene {
        WindowGroup {
            VideoView()
                .groupActivityAssociation(isShowingVideo ? .primary("how-to-video") : nil)
        }


        WindowGroup {
            BoardGameView()
                .groupActivityAssociation(isShowingVideo ? nil : .primary("play-game"))
        }
    }
}
```

> Note: If multiple scenes have a primary group activity association, the most recently activated one is used.

For a demonstration, watch the “Support multiple windows” chapter of the WWDC25 session <doc://com.apple.documentation/videos/play/wwdc2025/318>.

For UIKit, create a [`GroupActivityAssociationInteraction`](/documentation/GroupActivities/GroupActivityAssociationInteraction) and use <doc://com.apple.documentation/documentation/UIKit/UIView/addInteraction(_:)> to add the scene association interaction.

### 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`](/documentation/GroupActivities/SystemCoordinator)
of your [`GroupSession`](/documentation/GroupActivities/GroupSession) before you call the session’s [`join()`](/documentation/GroupActivities/GroupSession/join())
method. Use the [`SystemCoordinator`](/documentation/GroupActivities/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(_:)`](/documentation/GroupActivities/SpatialTemplatePreference/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 in an immersive space](/documentation/GroupActivities/adding-spatial-persona-support-to-an-activity#Place-content-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.

> Note: Make sure you separate the messages that all participants
> require from the ones that only make sense when spatial Personas are
> visible. Maintaining the shared context is necessary when spatial
> Personas are visible, but isn’t always necessary at other times.
> For example, Freeform doesn’t synchronize the scroll position for
> someone without a spatial Persona.

To determine when someone’s spatial Persona is visible, monitor the
[`localParticipantStates`](/documentation/GroupActivities/SystemCoordinator/localParticipantStates) of your session’s
[`SystemCoordinator`](/documentation/GroupActivities/SystemCoordinator) object. The <doc://com.apple.documentation/documentation/Swift/AsyncSequence>
in this property reports participant-related state changes, including
changes to the visibility of their spatial Persona. Get the
[`isSpatial`](/documentation/GroupActivities/SystemCoordinator/ParticipantState/isSpatial) property of the returned
[`SystemCoordinator.ParticipantState`](/documentation/GroupActivities/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 {
    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`](/documentation/GroupActivities/SystemCoordinator/groupImmersionStyle) property of your
[`SystemCoordinator`](/documentation/GroupActivities/SystemCoordinator) object. This property contains an
<doc://com.apple.documentation/documentation/Swift/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 {
    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.

> Note: If a participant presses the Digital Crown while an immersive
> space is open, the system dismisses the space without notifying the
> rest of the group. Someone might use this feature to temporarily leave
> the activity and perform another task. After they perform that task,
> they can use the system-provided UI to rejoin the activity and return
> to the group immersive space.

### Place content 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.



![An illustration showing a vertical window at the origin of a space, with participants placed side by side in front of the window.](images/com.apple.GroupActivities/grid-side-by-side-template@2x.png)

In a side-by-side arrangement with a window, the origin is under the window.



![An illustration showing a box at the origin of a space, with participants placed at equal distances around it.](images/com.apple.GroupActivities/grid-surround-template-area@2x.png)

In a surround arrangement with a volume, the origin is under the volume.



![An illustration showing participants arranged in a circle facing each other, with no content visible.](images/com.apple.GroupActivities/grid-surround-template-point~dark@2x.png)

When no window or volume is present, the system uses the surround presentation style and places the origin at the center of the group.

> Note: Open your immersive space after starting the SharePlay session to provide a smooth experience.
> When you open the immersive space before SharePlay starts, the space briefly shifts positions when SharePlay establishes the shared coordinate space, which can be disorienting.

If the immersive space is already open before SharePlay begins, you can use the <doc://com.apple.documentation/documentation/SwiftUI/EnvironmentValues/immersiveSpaceDisplacement> environment value to detect when the coordinate space changes and get the current offset of the immersive space relative to its default position.
When the value is <doc://com.apple.documentation/documentation/Spatial/Pose3D/identity>, the immersive space has no offset.
When the value changes to a non-identity pose, SharePlay has established a shared coordinate space.

To position content relative to a participant in the shared coordinate space, use the [`localParticipantState`](/documentation/GroupActivities/SystemCoordinator/localParticipantState) property of your [`SystemCoordinator`](/documentation/GroupActivities/SystemCoordinator) to access the local participant’s [`pose`](/documentation/GroupActivities/SystemCoordinator/ParticipantState/pose).
This property provides the participant’s offset from the spatial template origin.
The following example shows how to place an object at a consistent position in front of the local participant:

```swift
@Environment(\.immersiveSpaceDisplacement) var immersiveSpaceDisplacement

func placeObject(systemCoordinator: SystemCoordinator) -> SIMD3<Float> {
    // Get the local participant's position relative to the spatial template origin.
    let localPose = systemCoordinator.localParticipantState.pose
    
    // Place the object in front of and slightly below the participant.
    let offset = SIMD3<Float>(x: 0, y: -0.2, z: -0.5)
    
    // Add the offset to the participant's position.
    return localPose.position + offset
}
```

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.

To place content in front of a specific participant, observe `remoteParticipantStates` and use the pose property of `SystemCoordinator.ParticipantState` to get each remote participant’s position in the shared space:

```swift
Task {
    let observedRemoteParticipantStates = Observations {
        return systemCoordinator.remoteParticipantStates
    }
    for await observedRemoteParticipantState in observedRemoteParticipantStates {
        for (participant, state) in observedRemoteParticipantState {
            // Place presented content relative to the remote participant pose.
            placeContentNear(participant, at: participantState.pose)
        }
    }
}
```

---

Copyright &copy; 2026 Apple Inc. All rights reserved. | [Terms of Use](https://www.apple.com/legal/internet-services/terms/site.html) | [Privacy Policy](https://www.apple.com/privacy/privacy-policy)