WWDC Quick Look đź’“ By SwiftGGTeam
Customize spatial Persona templates in SharePlay

Customize spatial Persona templates in SharePlay

Watch original video

Highlight

visionOS 2 adds custom spatial templates so developers can precisely define each seat’s position, orientation, and role label in 3D space, letting spatial Personas auto-group and sit by activity phase.


Core Content

FaceTime on visionOS uses spatial Personas so callers feel like they’re in the same room. When someone starts a SharePlay activity, the system rearranges all participants according to a spatial template—defining each person’s initial position relative to the shared app. visionOS 1 shipped three built-in templates: side-by-side (arc layout, good for video), conversational (open circle, good for music), and surround (closed circle around shared content, good for 3D modeling).

Those three templates don’t cover every scenario. Board games need two players face-to-face with others watching; card games need dealer and players on opposite sides. visionOS 2 custom spatial templates solve this. Developers define each “seat” position and orientation in 3D space and assign role labels (e.g., “Blue Team,” “Red Team,” “Current Player”); when participants join, the system moves their spatial Persona to the matching seat based on role.

Presenter Ethan demos end-to-end with a word-guessing game called “Guess Together.” The game has four phases (welcome, category selection, team formation, gameplay), each using a different template—category selection uses the default side-by-side; team formation and gameplay use custom templates to separate blue team, red team, current player, and opposing team seats. Kevin shares core design principles for custom templates in the second half.

Detailed Content

Defining a custom spatial template

A custom template is a struct conforming to SpatialTemplate; the core is an elements array where each element is a seat with position and optional role (14:59).

Example from Guess Together’s team-selection phase:

import GroupActivities

struct TeamSelectionTemplate: SpatialTemplate {
    enum Role: String, SpatialTemplateRole {
        case blueTeam
        case redTeam
    }

    let elements: [any SpatialTemplateElement] = [
        // Blue team:
        .seat(position: .app.offsetBy(x: -2.5, z: 3.5), role: Role.blueTeam),
        .seat(position: .app.offsetBy(x: -3.0, z: 3.0), role: Role.blueTeam),
        .seat(position: .app.offsetBy(x: -3.5, z: 2.5), role: Role.blueTeam),

        // Starting positions:
        .seat(position: .app.offsetBy(x: 0, z: 4)),
        .seat(position: .app.offsetBy(x: 1, z: 4)),
        .seat(position: .app.offsetBy(x: -1, z: 4)),
        .seat(position: .app.offsetBy(x: 2, z: 4)),
        .seat(position: .app.offsetBy(x: -2, z: 4)),

        // Red team:
        .seat(position: .app.offsetBy(x: 2.5, z: 3.5), role: Role.redTeam),
        .seat(position: .app.offsetBy(x: 3.0, z: 3.0), role: Role.redTeam),
        .seat(position: .app.offsetBy(x: 3.5, z: 2.5), role: Role.redTeam)
    ]
}

Key points:

  • SpatialTemplate requires only an elements array (12:32)
  • Seat positions use .app.offsetBy(x:z:) with the shared app as origin, in meters (12:09)
  • Roles are String enums conforming to SpatialTemplateRole, attached to seats to reserve them for participants with that role (13:49)
  • Seats without roles fill in elements array order, so order determines join sequence (30:41)

Activating templates and assigning roles

Configure a custom template in one line (14:59):

systemCoordinator.configuration.spatialTemplatePreference = .custom(TeamSelectionTemplate())

When a participant joins a team, call assignRole to move them to the matching seat (15:39):

systemCoordinator.assignRole(TeamSelectionTemplate.Role.blueTeam)

To return to an unassigned seat (e.g., audience), call resignRole (16:00):

systemCoordinator.resignRole()

Key points:

  • systemCoordinator comes from GroupSession and is the core object for spatial behavior
  • After role assignment, FaceTime moves the participant’s spatial Persona to an empty seat for that role
  • Use resignRole() to leave a role; the system places them in an unassigned seat

Seat direction

By default all seats face the shared app, but custom templates can control each seat’s orientation—useful in games where the current player faces teammates and teammates face the current player (18:42):

struct GameTemplate: SpatialTemplate {
    enum Role: String, SpatialTemplateRole {
        case player
        case activeTeam
    }

    var elements: [any SpatialTemplateElement] {
        let activeTeamCenterPosition = SpatialTemplateElementPosition.app.offsetBy(x: 2, z: 3)

        let playerSeat = SpatialTemplateSeatElement(
            position: .app.offsetBy(x: -2, z: 3),
            direction: .lookingAt(activeTeamCenterPosition),
            role: Role.player
        )

        let activeTeamSeats: [any SpatialTemplateElement] = [
            .seat(
                position: activeTeamCenterPosition.offsetBy(x: 0, z: -0.5),
                direction: .lookingAt(playerSeat),
                role: Role.activeTeam
            ),
            .seat(
                position: activeTeamCenterPosition.offsetBy(x: 0, z: 0.5),
                direction: .lookingAt(playerSeat),
                role: Role.activeTeam
            )
        ]

        let audienceSeats: [any SpatialTemplateElement] = [
            .seat(position: .app.offsetBy(x: 0, z: 5)),
            .seat(position: .app.offsetBy(x: 1, z: 5)),
            .seat(position: .app.offsetBy(x: -1, z: 5)),
            .seat(position: .app.offsetBy(x: 2, z: 5)),
            .seat(position: .app.offsetBy(x: -2, z: 5))
        ]

        return audienceSeats + [playerSeat] + activeTeamSeats
    }
}

Key points:

  • direction: .lookingAt(...) faces a position or another seat (31:35)
  • direction: .alignedWith(appAxis: .z) aligns with an app axis (31:46)
  • direction: .lookingAt(.app).rotatedBy(.degrees(30)) rotates from the default facing (32:02)
  • Audience seats need no direction—they default to facing the app

Group immersive space

The gameplay phase shows the current puzzle to the active player. With group immersive space enabled, the immersive space origin matches the spatial template origin, so you can place 3D elements using seat positions (21:41):

systemCoordinator.configuration.supportsGroupImmersiveSpace = true

Key points:

  • By default immersive spaces in SharePlay are private—participants can’t see each other’s spatial Personas
  • Setting supportsGroupImmersiveSpace = true makes the space shared; everyone is in the same space
  • RealityKit and GroupActivities use meters by default, so seat positions map directly to RealityKit entity positions without conversion

Simulating FaceTime calls in Xcode 16

Xcode 16 can create simulated FaceTime calls in the visionOS simulator (08:28). Use Features > FaceTime to pick remote participant count and test SharePlay apps without hardware. Simulated participants don’t auto-assign roles, so role seats won’t be occupied by them.

Core Takeaways

  • What to do: Design competitive seat templates for board/card apps—two player seats face-to-face, audience on the side. Why it’s worth it: These apps naturally need “opponent” vs. “spectator” spatial separation; built-in templates can’t do that. How to start: Define two face-to-face seats with roles (e.g., player1, player2) plus unassigned audience seats; use assignRole at game start.

  • What to do: In collaborative whiteboard/design tools, use lookingAt so seats face each other instead of the app. Why it’s worth it: Discussion matters more than screen-watching in collaboration; facing each other makes gesture and expression communication more natural. How to start: Use .lookingAt() so adjacent seats’ direction points at the opposite seat.

  • What to do: Switch templates per activity phase (e.g., team pick → game → wrap-up) and sync UI state. Why it’s worth it: Participant relationships change each phase—undifferentiated during team pick, role-based during play; matching templates reflect current relationships. How to start: Update spatialTemplatePreference on state transitions; call assignRole or resignRole in sync; avoid switching templates during transitional states.

  • What to do: Place 3D hints (puzzle panels, props) from seat positions in group immersive space. Why it’s worth it: Shared space origin matches the template origin, so you can place UI precisely in front of specific seats without users turning their heads. How to start: Enable supportsGroupImmersiveSpace, position RealityKit entities with seat positions plus a small offset.

Comments

GitHub Issues · utterances