WWDC Quick Look 💓 By SwiftGGTeam
Build spatial SharePlay experiences

Build spatial SharePlay experiences

Watch original video

Highlight

SharePlay on visionOS introduces Shared Context and the System Coordinator API, placing Spatial Personas in FaceTime at real spatial positions. The system automatically manages participant and app spatial arrangement through templates—developers only need to focus on visual consistency to build natural collaboration experiences.

Core Content

New Concepts in Spatial Sharing

FaceTime on visionOS lets Spatial Personas occupy physical space in the user’s room rather than being confined to windows. This brings two core concepts:

Shared Context: Everyone sees each other and shared content at the same relative positions. When Connor points at Mia’s Persona, Mia sees Connor pointing at her. The system handles this spatial consistency through templates automatically—the app does not need to calculate displacements itself.

Visual Consistency: Everyone sees the same content in the app. When Connor points at a block in the app, Mia must also see Connor pointing at the block. The app must synchronize content state itself (scroll position, selected items, 3D object orientation).

System Coordinator

SystemCoordinator is a new API in the GroupActivities framework responsible for two things: receiving system state for SharePlay sessions and letting apps provide additional configuration.

Three Spatial Templates

The system uses templates to determine spatial arrangement of participants and apps:

  • Side-by-Side: Participants face the app in an arc; the default template, suitable for regular window scenarios.
  • Conversational: Participants sit in a semicircle with the app in front; suitable for music playback and other non-content-focused scenarios.
  • Surround: Participants form a circle with the app at the center; only available for Volume scenes.

Apps can provide preferences through spatialTemplatePreference, and FaceTime will try to apply them.

The isSpatial Flag

localParticipantState.isSpatial indicates whether the local participant is in spatial mode. This determines whether content position needs to be synchronized:

  • In spatial mode, participants have spatial context; scroll position and similar operations need synchronization
  • In non-spatial mode (such as regular FaceTime), these operations are not synchronized to avoid unexpected UI changes for the other party

Scene Association

Multi-window apps need to tell the system which window hosts the SharePlay activity. Single-window apps handle this automatically. If multi-window apps do not set this, the system randomly selects a window, which may be wrong.

Scene association is configured in two ways:

  1. Scene Activation Conditions: Use handlesExternalEvents (SwiftUI) or UIScene.activationConditions (UIKit) to declare which activity identifiers a scene can handle.
  2. Scene Association Behavior: Set .content("identifier") in GroupActivity metadata, matching scenes with content identifiers (such as document IDs).

Starting SharePlay from the Share Menu

Starting in iOS 17, SharePlay can be launched from the Share menu. Apps expose relevant GroupActivities in the window’s Share banner, letting users start sharing directly from the menu without a dedicated in-app SharePlay button.

Implementation registers GroupActivities through UIActivityItemsConfiguration; the system searches along the responder chain and displays them in the Share menu.

SharePlay for Immersive Apps

Immersive apps use ImmersiveSpace to create borderless experiences. For SharePlay, configure the supportsGroupImmersiveSpace flag so the system knows the Immersive Space is shared.

When participants join a group session with this flag enabled, their Immersive Space becomes a Group Immersive Space. The system moves the spatial origin to the shared position defined by the template, establishing a shared coordinate system.

Group Immersion Style: The groupImmersionStyle async sequence provides the immersion style other participants enter (such as .full, .mixed) or nil (departure). Apps can automatically open or close matching Immersive Spaces to keep everyone together.

System Experience Displacement: The immersiveSpaceDisplacement(in:) method on GeometryReader3D returns the displacement of the spatial origin relative to the local user. Inverting it gives the local user’s position relative to the spatial origin, used to place private UI (such as control menus).

Content Extent: The distance between participants and the app in templates is dynamically adjusted based on app size. For apps with only a Group Immersive Space, use the contentExtent modifier to specify the distance from the center to the farthest edge (in points), helping the system calculate appropriate distance.

Detailed Content

Observing Local Participant Spatial State

04:08

for await session in ExploreActivity.sessions() {
    guard let systemCoordinator = await session.systemCoordinator else { continue }

    let isLocalParticipantSpatial = systemCoordinator.localParticipantState.isSpatial

    Task.detached {
        for await localParticipantState in systemCoordinator.localParticipantStates {
            if localParticipantState.isSpatial {
                // Start syncing scroll position
            } else {
                // Stop syncing scroll position
            }
        }
    }
}

Key points:

  • session.systemCoordinator gets the system coordinator; may be nil
  • localParticipantState.isSpatial determines if the local participant is in spatial mode
  • localParticipantStates is an async sequence producing new values on state changes
  • Sync scroll position and similar operations in spatial mode; stop syncing in non-spatial mode
  • Use Task.detached to avoid blocking the main thread

Configuring Spatial Template Preference

06:10

for await session in ExploreActivity.sessions() {
    guard let systemCoordinator = await session.systemCoordinator else { continue }

    var configuration = SystemCoordinator.Configuration()
    configuration.spatialTemplatePreference = .sideBySide
    systemCoordinator.configuration = configuration

    session.join()
}

Key points:

  • SystemCoordinator.Configuration() creates a configuration object
  • spatialTemplatePreference sets template preference; options include .none, .sideBySide, .conversational
  • Configuration must be set before calling session.join()
  • .none follows system default: Side-by-Side for regular windows, Surround for Volume scenes

Configuring Scene Activation Conditions (SwiftUI)

09:10

@main
struct ExploreTogetherApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .handlesExternalEvents(
                    preferring: ["com.example.explore-together.activity"],
                    allowing: ["com.example.explore-together.activity"]
                )
        }
    }
}

Key points:

  • handlesExternalEvents(preferring:allowing:) declares which external events a scene can handle
  • preferring indicates the scene is better suited to handle these identifiers
  • allowing indicates the scene can handle these identifiers
  • Use the GroupActivity identifier as the matching condition

Configuring Scene Activation Conditions (UIKit)

09:30

class SceneDelegate: NSObject, UISceneDelegate {

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // ...

        scene.activationConditions.canActivateForTargetContentIdentifierPredicate =
                NSPredicate(format: "self == %@", "com.example.explore-together.activity")

        scene.activationConditions.prefersToActivateForTargetContentIdentifierPredicate =
                NSPredicate(format: "self == %@", "com.example.explore-together.activity")
    }
}

Key points:

  • canActivateForTargetContentIdentifierPredicate declares activity identifiers the scene can handle
  • prefersToActivateForTargetContentIdentifierPredicate declares activity identifiers the scene is better suited to handle
  • The system evaluates all scenes in can → prefers order and selects the best association
  • If no scene matches, the system launches a new window scene

Setting Scene Association Behavior

10:40

struct ExploreActivity: GroupActivity {
    var metadata: GroupActivityMetadata {
        var metadata = GroupActivityMetadata()
        // ...
        metadata.sceneAssociationBehavior = .content("document-1")
        return metadata
    }
}

Key points:

  • sceneAssociationBehavior controls scene association
  • .default matches scenes using the GroupActivity identifier (default behavior)
  • .content("identifier") matches using a custom content identifier, suitable for document apps
  • .none disables scene association, suitable for immersive apps or scenarios with different content per participant
  • All participants use the same identifier to match scenes

Starting SharePlay from the Share Menu

13:44

// Create the activity
let activity = ExploreActivity()

// Register the activity on the item provider
let itemProvider = NSItemProvider()
itemProvider.registerGroupActivity(activity)

// Create the activity items configuration
let configuration = UIActivityItemsConfiguration(itemProviders: [itemProvider])

// Provide the metadata for the group activity
configuration.metadataProvider = { key in
    guard key == .linkPresentationMetadata else { return nil }
    let metadata = LPLinkMetadata()
    metadata.title = "Explore Together"
    metadata.imageProvider = NSItemProvider(object: UIImage(named: "explore-activity")!)
    return metadata
}
self.activityItemsConfiguration = configuration

Key points:

  • NSItemProvider.registerGroupActivity(activity) registers the activity on the item provider
  • UIActivityItemsConfiguration wraps item providers
  • metadataProvider provides the title and icon displayed in the Share menu
  • The system searches along the responder chain for activityItemsConfiguration
  • Setting this on a view controller with SharePlay content lets the system discover it automatically

Configuring Group Immersive Space

16:03

for await session in ExploreActivity.sessions() {
    guard let systemCoordinator = await session.systemCoordinator else { continue }

    var configuration = SystemCoordinator.Configuration()
    configuration.supportsGroupImmersiveSpace = true
    systemCoordinator.configuration = configuration
}

Key points:

  • supportsGroupImmersiveSpace = true declares the app supports shared immersive space
  • After participants join, their Immersive Space becomes a Group Immersive Space
  • The system automatically moves the spatial origin to the shared position
  • Participants can see each other’s Personas in space

System Experience Displacement

17:51

var body: some Scene {
    ImmersiveSpace(id: "earth") {
        GeometryReader3D { proxy in
            let displacement = proxy.immersiveSpaceDisplacement(in: .global).inverse

            Control()
                .offset(displacement.position)
                .rotation3DEffect(displacement.rotation)
        }
    }
}

Key points:

  • GeometryReader3D provides 3D geometry information
  • immersiveSpaceDisplacement(in: .global) gets displacement of the spatial origin relative to the local user
  • .inverse inverts to get the local user’s position relative to the spatial origin
  • Use displacement position and rotation to place private UI
  • Displacement is calculated only at initial spatial placement, not updated as the user moves
  • Suitable for placing relative-position content that does not need precise user tracking

Template Configuration with Content Extent

20:46

for await session in ExploreSolarActivity.sessions() {
    guard let systemCoordinator = await session.systemCoordinator else { continue }

    var configuration = SystemCoordinator.Configuration()
    configuration.supportsGroupImmersiveSpace = true
    configuration.spatialTemplatePreference = .sideBySide.contentExtent(200)
    systemCoordinator.configuration = configuration
}

Key points:

  • .sideBySide.contentExtent(200) sets content extent for the Side-by-Side template
  • contentExtent is the distance from content center to farthest edge (in points)
  • The system uses this value to calculate appropriate distance between participants and content
  • Especially useful for apps with only a Group Immersive Space
  • Also applies to window-based SharePlay activities

Receiving Group Immersion Style

22:32

for await session in ExploreSolarActivity.sessions() {
    guard let systemCoordinator = await session.systemCoordinator else { continue }

    Task.detached {
        for await immersionStyle in systemCoordinator.groupImmersionStyle {
            if let immersionStyle {
                // Open an immersive space with the same immersion style
            } else {
                // Dismiss the immersive space
            }
        }
    }
}

Key points:

  • groupImmersionStyle is an async sequence providing other participants’ immersion styles
  • When receiving a specific style (such as .full, .mixed), open a matching Immersive Space
  • When receiving nil, other participants have left the Group Immersive Space; you can close yours
  • Ensures all participants remain in the same immersive environment
  • When users temporarily exit via the Digital Crown, the system does not notify others of state changes

Core Takeaways

  • What to do: Add SharePlay support to document collaboration apps for real-time multi-user editing and annotation.

  • Why it’s worth it: Spatial SharePlay makes collaborators feel like they’re sitting at the same table. System Coordinator handles spatial arrangement automatically; the app only needs to sync document state.

  • How to start: Implement the GroupActivity protocol, configure SystemCoordinator’s spatialTemplatePreference to .sideBySide, use GroupSessionMessenger to sync document edits, and listen to isSpatial to decide whether to sync scroll position.

  • What to do: Develop a multi-user immersive stargazing app for exploring planets together in a virtual universe.

  • Why it’s worth it: Group Immersive Space puts all participants in the same shared coordinate system where they can see each other’s Personas and shared 3D content. groupImmersionStyle ensures everyone automatically enters the same environment.

  • How to start: Set supportsGroupImmersiveSpace = true, use immersiveSpaceDisplacement to place private control panels, listen to groupImmersionStyle to automatically open/close Immersive Space, and use GroupSessionMessenger to sync 3D object state.

  • What to do: Integrate SharePlay into a music playback app so distant friends can listen together.

  • Why it’s worth it: The Conversational template arranges participants in a semicircle with the app in front, creating a sitting-together-listening atmosphere. Spatial audio lets everyone feel music coming from different directions.

  • How to start: Configure spatialTemplatePreference = .conversational, expose GroupActivity in the Share menu, and sync playback progress and playlist state.

  • What to do: Add spatial collaboration to whiteboard/mind-map apps.

  • Why it’s worth it: Freeform has demonstrated the feasibility of this experience. The Side-by-Side template faces all participants toward the same content; visual consistency ensures pointing and annotations are visible to everyone.

  • How to start: Use the .sideBySide template, sync canvas zoom and scroll position, broadcast stroke and annotation data via GroupSessionMessenger, and set sceneAssociationBehavior = .content(documentID) for multi-window apps.

Comments

GitHub Issues · utterances