WWDC Quick Look 💓 By SwiftGGTeam
Share visionOS experiences with nearby people

Share visionOS experiences with nearby people

元の動画を見る

ハイライト

visionOS 26 では Nearby Window Sharing が導入されました:各ウィンドウの右側に新しく共有ボタンが追加され、クリックすると近くの人を選択でき、ウィンドウは全員の空間内で同じ位置とサイズで表示されます。ウィンドウバーが緑色になっていることは、共有中であることを示します。


主要内容

二人が Vision Pro を装着して同じリビングに座り、一緒に映画を見たりボードゲームをプレイしたいとします。以前はそれぞれのデバイスでアプリを開くしかなく、画面の位置はずれ、音声は同期されず、画面を指差して議論することさえできませんでした。リモートの SharePlay は相手を仮想の Persona にするだけでしたが、物理的に同じ空間にいる体験は無視されていました。

visionOS 26 は Nearby Window Sharing でこの問題を解決します。各ウィンドウバーの右側に共有ボタンが追加され、クリックするとリストから近くの人を選択できます。ウィンドウは全く同じ位置とサイズで全員の空間に表示され、ウィンドウバーが緑色になっていることは共有中を示します(01:02)。誰でもウィンドウをドラッグしてサイズ変更したり、壁に貼り付けたりでき、操作は全員に同期されます。手をウィンドウ越しに伸ばすと内容は自動的にフェードアウトし、ジェスチャーの視認性が確保されます。デジタルクラウンを押して再センタリングすると、ウィンドウは再び全員に適した位置に戻ります。既に SharePlay に対応しているアプリは変更なしでサポートでき、SharePlay に対応していないアプリでも「読み取り専用モード」で共有可能です。FaceTime のリモート参加者は spatial Persona として近くの参加者の隣に自動的に配置され、volumetric ウィンドウ共有時には volume 周囲の空き位置を埋めるために移動します。


詳細

nearby sharing を適切に実装するために、セッションでは一つの主線を提示しています:まず GroupActivity を Share メニューに公開し、次に参加者の位置に基づいて体験を強化し、最後に ARKit でコンテンツを物理空間に固定します。

ステップ 1:Activity を Share メニューに公開06:21)。volumetric ウィンドウの Share メニューは、アプリが activity を公開した場合のみ使用可能になります。SwiftUI での記述方法は、隠し ShareLink を追加することです:

import SwiftUI
import GroupActivities

struct BoardGameActivity: GroupActivity, Transferable {
    var metadata: GroupActivityMetadata = {
        var metadata = GroupActivityMetadata()
        metadata.title = "Play Together"
        return metadata
    }()
}

struct BoardGameApp: App {
    var body: some Scene {
        WindowGroup {
            BoardGameView()
            ShareLink(item: BoardGameActivity(), preview: SharePreview("Play Together"))
                .hidden()
        }
        .windowStyle(.volumetric)
    }
}

重要ポイント:

  • BoardGameActivityGroupActivityTransferable の両方に準拠し、後者により ShareLink で認識されます。
  • metadata.title は Share メニューでユーザーに表示されるアクティビティ名です。
  • ShareLink(item:) は activity インスタンスを受け取り、.hidden() は UI に影響を与えないようにするため——Share メニューに activity を登録するためだけに使用されます。
  • .windowStyle(.volumetric) これは volumetric ウィンドウであることを宣言し、共有時に他の人も volume 形態で確認できます。

ステップ 2:Session を監視して参加07:14)。Share メニューから activity をアクティベートすると自動的に GroupSession が作成され、手動で activate() を呼び出した場合と同じ効果になります:

func observeSessions() async {
    for await session in BoardGameActivity.sessions() {
        guard let systemCoordinator = await session.systemCoordinator else { continue }
        systemCoordinator.configuration.supportsGroupImmersiveSpace = true
        session.join()
    }
}

重要ポイント:

  • BoardGameActivity.sessions() は async sequence で、activity がアクティベートされるたびに新しい session をプッシュします。
  • systemCoordinator は SharePlay のシステムレベル設定への入り口を提供します。
  • supportsGroupImmersiveSpace = true は immersive space が全員の前で同じ位置になることを示し、そうでない場合は各人がその場で起動します。
  • session.join() が SharePlay に参加する実際の呼び出しで、この行がないと session は観察されるだけで起動しません。

ステップ 3:近くの参加者を識別09:59)。FaceTime のリモート参加者が参加したら、ローカルとリモートを区別する必要があります:

func observeParticipants(session: GroupSession<BoardGameActivity>) async {
    for await activeParticipants in session.$activeParticipants.values {
        let nearbyParticipants = activeParticipants.filter {
            $0.isNearbyWithLocalParticipant && $0 != session.localParticipant
        }
    }
}

重要ポイント:

  • session.$activeParticipants.values は参加者のコレクションを async sequence に変換し、参加者の増減時にプッシュされます。
  • isNearbyWithLocalParticipant は visionOS 26 で追加されたプロパティで、相手がローカルユーザーと同じ物理空間にいるかを判断します。
  • localParticipant 自分を除外し、「近くの人」に自分を含めないようにします。
  • このリストを取得した後、「近く vs リモート」の対戦ゲームのグループ分けが可能です。

ステップ 4:参加者の位置を読み取る11:42)。Immersive space では参加者がシーンのどこにでも立っている可能性があり、全員が原点にいると仮定できません:

func observeLocalParticipantState(session: GroupSession<BoardGameActivity>) async {
    guard let systemCoordinator = await session.systemCoordinator else { return }

    for await localParticipantState in systemCoordinator.localParticipantStates {
        let localParticipantPose = localParticipantState.pose
        // Place presented content relative to the local participant pose
    }
}

重要ポイント:

  • localParticipantStates は system coordinator 上の async sequence で、状態変化時にプッシュされます。
  • pose プロパティは ImmersiveSpace シーン内の参加者の位置姿勢(位置と向き)を提供します。
  • pose はリアルタイムで追跡されず、共有開始時やデジタルクラウンによる再センタリングなどの主要イベント後にのみ一度だけ更新されます。
  • pose を取得した後、全員の前にスコアボードや UI コントロールを配置でき、全てのコンテンツをシーンの原点に積み上げる必要がありません。

ステップ 5:マルチウィンドウでメインを選択15:54)。アプリ内に複数の WindowGroup がある場合、.groupActivityAssociation(.primary("InstructionalVideo")) modifier でどのウィンドウが SharePlay のメインウィンドウかを指定します。

ステップ 6:World Anchor を共有20:24)。ARKit は visionOS 26 で WorldAnchor に新しい初期化パラメータを追加しました:

import ARKit

class SharedAnchorController {
    func createAnchor(at transform: simd_float4x4, provider: WorldTrackingProvider) async throws {
        let anchor = WorldAnchor(originFromAnchorTransform: transform,
                                 sharedWithNearbyParticipants: true)
        try await provider.addAnchor(anchor)
    }

    func observeWorldTracking(provider: WorldTrackingProvider) async {
       for await update in provider.anchorUpdates {
            switch update.event {
            case .added, .updated, .removed:
                let anchorIdentifier = update.anchor.id
            }
        }
    }
}

重要ポイント:

  • sharedWithNearbyParticipants: true はアンカーを近くの参加者と共有し、全員が同じ物理位置で確認できます。
  • 呼び出す前に provider.worldAnchorSharingAvailability で可用性を確認し、利用可能でない場合は共有を試みないようにしてください。
  • update.anchor.id は全デバイスで一貫しており、コンテンツ同期識別子として直接使用可能です。
  • 共有アンカーは SharePlay 終了後永続化されず、通常の world anchor とは異なります——この点はハマりやすいです。

メディア再生に関しては、visionOS 26 の AVPlayerAVPlaybackCoordinator を通じて自動的に近くの参加者の音声を同期し、あるデバイスの音声が先に到着し、別のデバイスが後から到着してエコーが発生するのを防ぎます。


重要ポイント

  • 何をするか:既存の SharePlay アプリを同じ部屋の 2 台の Vision Pro で実行し、nearby sharing がそのまま使えることを確認します。

    • なぜ価値があるのか:Apple は変更なしで利用可能と明言していますが、実際の効果(位置同期、Persona 配置、AVPlayer 同期)は実機での検証が必要で、詳細な問題を発見できます。
    • どのように始めるか:visionOS 26 デバイスを一台用意し、同僚にもう一台を装着してもらい、既存の ShareLink で GroupSession を起動し、ウィンドウバーが緑色になることと位置の一貫性を確認します。
  • 何をするか:純粋な Immersive Space 体験を「非没入ウィンドウ + 没入シーン」のデュアルモードに改造します。

    • なぜ価値があるのか:Immersive Space にはウィンドウバーがなく、ユーザーは Share ボタンを見つけられず、アプリ独自のエントリに依存するしかありません。非没入の入り口ウィンドウを提供することで Share メニューが常にアクセス可能になります。
    • どのように始めるか:まず通常の WindowGroup をロビーページとして作成し、ShareLink(item: YourActivity()) で activity を公開します。全員が参加した後、ImmersiveSpace に移行し、supportsGroupImmersiveSpace = true を設定します。
  • 何をするかParticipantState.pose を使って各近くの参加者の前に UI を配置します。

    • なぜ価値があるのか:全員がウィンドウの正面に集まっているというデフォルトの仮定は成立しません——共有開始時、人は部屋のどこにでもいる可能性があり、スコアボードを原点に配置すると空に向けていることになります。
    • どのように始めるか:systemCoordinator.localParticipantStates を監視し、pose 更新時にスコアボードやツールバーなどのコンテンツを各人の pose の前方 0.5 メートルに配置し、デジタルクラウンによる再センタリングイベント後に位置を更新します。
  • 何をするか:マルチユーザー協調の物理レイアウト(ボード、デスクトップツール、3D モデル)を Shared World Anchor で固定します。

    • なぜ価値があるのか:仮想コンテンツを部屋の実際のテーブルに「貼り付ける」ことは、各人の相対座標系に浮かせるのではなく、同じ空間にいる体験の最も説得力のある部分です。
    • どのように始めるか:まず observe worldAnchorSharingAvailability を行い、利用可能な場合は WorldAnchor(originFromAnchorTransform:sharedWithNearbyParticipants: true) でアンカーを作成します。update.anchor.id を同期識別子として使用し、セッション終了後に再作成を忘れないようにしてください(永続化されません)。

関連セッション

コメント

GitHub Issues · utterances