WWDC Quick Look 💓 By SwiftGGTeam
Build spatial SharePlay experiences

Build spatial SharePlay experiences

元の動画を見る

ハイライト

visionOS の SharePlay は共有コンテキストと System Coordinator API を導入し、FaceTime の Spatial Persona を実際の空間位置に配置します。システムがテンプレートで参加者と App の空間配置を自動管理するため、開発者は視覚的一貫性に集中するだけで自然なコラボレーション体験を構築できます。

主要内容

空間共有の新概念

visionOS の FaceTime では Spatial Persona がウィンドウに限定されず、ユーザーの部屋の物理空間を占めます。これにより 2 つの中核概念が生まれます:

共有コンテキスト(Shared Context):全員が互いと共有コンテンツを同じ相対位置で見ます。Connor が Mia の Persona を指すと、Mia は Connor が自分を指しているのを見ます。システムがテンプレートでこの空間的一貫性を自動処理するため、App は変位を自分で計算する必要がありません。

視覚的一貫性(Visual Consistency):全員が App 内で同じコンテンツを見ます。Connor が App 内のブロックを指すと、Mia も Connor がブロックを指しているのを見る必要があります。App はコンテンツ状態(スクロール位置、選択項目、3D オブジェクトの向きなど)を自分で同期する必要があります。

System Coordinator

SystemCoordinator は GroupActivities フレームワークの新 API で、2 つの役割を担います:SharePlay セッションのシステム状態を受信し、App に追加設定を提供します。

3 つの空間テンプレート

システムはテンプレートで参加者と App の空間配置を決定します:

  • Side-by-Side:参加者が弧を描いて App に向かう。デフォルトテンプレートで、通常のウィンドウシーンに適している
  • Conversational:参加者が半円に座り、App が前方に配置。音楽再生などコンテンツ非重視のシーンに適している
  • Surround:参加者が円を描き、App が中心。Volume(立体)シーンのみで使用可能

App は spatialTemplatePreference で好みを提供でき、FaceTime が可能な限り適用します。

isSpatial フラグ

localParticipantState.isSpatial はローカル参加者が空間モードかどうかを示します。コンテンツ位置の同期が必要かどうかを決定します:

  • 空間モードでは参加者に空間コンテキストがあり、スクロール位置などの操作を同期する必要がある
  • 非空間モード(通常の FaceTime など)では同期せず、相手に予期しない UI 変化を見せない

シーン関連付け(Scene Association)

マルチウィンドウ App は SharePlay アクティビティをホストするウィンドウをシステムに伝える必要があります。シングルウィンドウ App は自動処理されます。マルチウィンドウ App が設定しないと、システムがランダムにウィンドウを選択し、誤った選択になる可能性があります。

シーン関連付けは 2 つの方法で設定します:

  1. Scene Activation ConditionshandlesExternalEvents(SwiftUI)または UIScene.activationConditions(UIKit)でシーンが処理できるアクティビティ識別子を宣言
  2. Scene Association Behavior:GroupActivity の metadata で .content("identifier") を設定し、コンテンツ識別子(ドキュメント ID など)でシーンをマッチング

Share メニューから SharePlay を開始

iOS 17 から SharePlay は Share メニューから起動できます。App はウィンドウの Share バナーで関連する GroupActivity を公開し、App 内の専用 SharePlay ボタンなしでメニューから直接共有を開始できます。

UIActivityItemsConfiguration で GroupActivity を登録し、システムが responder chain をたどって Share メニューに表示します。

没入型 App の SharePlay

没入型 App は ImmersiveSpace で境界のない体験を作成します。SharePlay では supportsGroupImmersiveSpace フラグを設定し、Immersive Space が共有されることをシステムに伝えます。

このフラグが有効なグループセッションに参加者が参加すると、Immersive Space が Group Immersive Space になります。システムが空間原点をテンプレートで定義された共有位置に移動し、共有座標系を確立します。

Group Immersion StylegroupImmersionStyle 非同期シーケンスは他の参加者が入る没入スタイル(.full.mixed など)または nil(退出)を提供します。App は一致する Immersive Space を自動的に開閉し、全員を一緒に保てます。

System Experience DisplacementGeometryReader3DimmersiveSpaceDisplacement(in:) メソッドは空間原点のローカルユーザーからの変位を返します。反転するとローカルユーザーの空間原点からの位置が得られ、プライベート UI(コントロールメニューなど)の配置に使用します。

Content Extent:テンプレートでの参加者と App の距離は App サイズに基づいて動的に調整されます。Group Immersive Space のみの App では、contentExtent 修飾子で中心から最遠端までの距離(ポイント単位)を指定し、システムが適切な距離を計算するのを助けます。

詳細

ローカル参加者の空間状態を監視

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
            }
        }
    }
}

キーポイント:

  • session.systemCoordinator でシステムコーディネーターを取得(nil の可能性あり)
  • localParticipantState.isSpatial でローカル参加者が空間モードかどうかを判定
  • localParticipantStates は非同期シーケンスで、状態変化時に新しい値を生成
  • 空間モードではスクロール位置などを同期、非空間モードでは同期を停止
  • Task.detached でメインスレッドのブロックを回避

空間テンプレートの好みを設定

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()
}

キーポイント:

  • SystemCoordinator.Configuration() で設定オブジェクトを作成
  • spatialTemplatePreference でテンプレートの好みを設定(.none.sideBySide.conversational
  • 設定は session.join() の前に行う必要がある
  • .none はシステムデフォルトに従う:通常ウィンドウは Side-by-Side、Volume シーンは Surround

シーンアクティベーション条件を設定(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"]
                )
        }
    }
}

キーポイント:

  • handlesExternalEvents(preferring:allowing:) でシーンが処理できる外部イベントを宣言
  • preferring はシーンがこれらの識別子の処理に適していることを示す
  • allowing はシーンがこれらの識別子を処理できることを示す
  • GroupActivity の識別子をマッチング条件として使用

シーンアクティベーション条件を設定(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")
    }
}

キーポイント:

  • canActivateForTargetContentIdentifierPredicate でシーンが処理できるアクティビティ識別子を宣言
  • prefersToActivateForTargetContentIdentifierPredicate でシーンがより適しているアクティビティ識別子を宣言
  • システムは can → prefers の順で全シーンを評価し、最適な関連付けを選択
  • マッチするシーンがなければ、システムが新しいウィンドウシーンを起動

シーン関連付け動作を設定

10:40

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

キーポイント:

  • sceneAssociationBehavior でシーン関連付けを制御
  • .default は GroupActivity 識別子でシーンをマッチング(デフォルト動作)
  • .content("identifier") はカスタムコンテンツ識別子でマッチング、ドキュメント App に適している
  • .none はシーン関連付けを無効化、没入型 App や参加者ごとに異なるコンテンツのシーンに適している
  • 全参加者が同じ識別子でシーンをマッチング

Share メニューから SharePlay を開始

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

キーポイント:

  • NSItemProvider.registerGroupActivity(activity) で item provider にアクティビティを登録
  • UIActivityItemsConfiguration で item provider をラップ
  • metadataProvider で Share メニューに表示されるタイトルとアイコンを提供
  • システムは responder chain をたどって activityItemsConfiguration を検索
  • SharePlay コンテンツを持つ view controller に設定すれば自動的にシステムが発見

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
}

キーポイント:

  • supportsGroupImmersiveSpace = true で共有没入空間をサポートすることを宣言
  • 参加者参加後、Immersive Space が Group Immersive Space になる
  • システムが自動的に空間原点を共有位置に移動
  • 参加者は空間内で互いの Persona を見られる

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)
        }
    }
}

キーポイント:

  • GeometryReader3D で 3D ジオメトリ情報を提供
  • immersiveSpaceDisplacement(in: .global) で空間原点のローカルユーザーからの変位を取得
  • .inverse で反転し、ローカルユーザーの空間原点からの位置を取得
  • 変位の positionrotation でプライベート UI を配置
  • 変位は空間の初期配置時のみ計算され、ユーザーの移動に追従しない
  • ユーザーの精密追跡が不要な相対位置コンテンツの配置に適している

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
}

キーポイント:

  • .sideBySide.contentExtent(200) で Side-by-Side テンプレートのコンテンツ範囲を設定
  • contentExtent はコンテンツ中心から最遠端までの距離(ポイント)
  • システムがこの値で参加者とコンテンツの適切な距離を計算
  • Group Immersive Space のみの App に特に有用
  • ウィンドウベースの SharePlay アクティビティにも適用可能

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
            }
        }
    }
}

キーポイント:

  • groupImmersionStyle は他の参加者の没入スタイルを提供する非同期シーケンス
  • 具体的なスタイル(.full.mixed など)を受信したら一致する Immersive Space を開く
  • nil を受信したら他の参加者が Group Immersive Space を退出したので、自分のも閉じられる
  • 全参加者が常に同じ没入環境にいることを保証
  • ユーザーがデジタルクラウンで一時退出しても、システムは他の参加者に状態変化を通知しない

重要ポイント

  • やること:ドキュメントコラボレーション App に SharePlay サポートを追加し、複数人が同じドキュメントをリアルタイムで編集・注釈

  • 価値:空間 SharePlay で協力者が同じテーブルに座っているような感覚。System Coordinator が空間配置を自動処理し、App はドキュメント状態の同期のみ

  • 始め方GroupActivity プロトコルを実装し、SystemCoordinatorspatialTemplatePreference.sideBySide に設定、GroupSessionMessenger でドキュメント編集を同期、isSpatial を監視してスクロール位置の同期を決定

  • やること:複数人で仮想宇宙の惑星を探索する没入型観星 App を開発

  • 価値:Group Immersive Space で全参加者が同じ共有座標系に入り、互いの Persona と共有 3D コンテンツを見られる。groupImmersionStyle で全員が自動的に同じ環境に入る

  • 始め方supportsGroupImmersiveSpace = true を設定し、immersiveSpaceDisplacement でプライベートコントロールパネルを配置、groupImmersionStyle を監視して Immersive Space を自動開閉、GroupSessionMessenger で 3D オブジェクト状態を同期

  • やること:音楽再生 App に SharePlay を統合し、遠くの友人と一緒に音楽を聴く

  • 価値:Conversational テンプレートで参加者を半円に配置し、App を前方に置いて一緒に聴く雰囲気を演出。空間オーディオで音楽が異なる方向から聞こえる

  • 始め方spatialTemplatePreference = .conversational を設定し、Share メニューで GroupActivity を公開、再生進捗とプレイリスト状態を同期

  • やること:ホワイトボード/マインドマップ App に空間コラボレーション機能を追加

  • 価値:Freeform がこの体験の実現可能性を示している。Side-by-Side テンプレートで全参加者が同じコンテンツに向かい、視覚的一貫性で指差しと注釈が全員に見える

  • 始め方.sideBySide テンプレートを使用し、キャンバスのズームとスクロール位置を同期、GroupSessionMessenger でストロークと注釈データをブロードキャスト、マルチウィンドウ App では sceneAssociationBehavior = .content(documentID) を設定

関連セッション

コメント

GitHub Issues · utterances