ハイライト
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 つの方法で設定します:
- Scene Activation Conditions:
handlesExternalEvents(SwiftUI)またはUIScene.activationConditions(UIKit)でシーンが処理できるアクティビティ識別子を宣言 - 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 Style:groupImmersionStyle 非同期シーケンスは他の参加者が入る没入スタイル(.full、.mixed など)または nil(退出)を提供します。App は一致する Immersive Space を自動的に開閉し、全員を一緒に保てます。
System Experience Displacement:GeometryReader3D の immersiveSpaceDisplacement(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で反転し、ローカルユーザーの空間原点からの位置を取得- 変位の
positionとrotationでプライベート 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プロトコルを実装し、SystemCoordinatorのspatialTemplatePreferenceを.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)を設定
関連セッション
- Design spatial SharePlay experiences — 空間 SharePlay 体験の設計原則とベストプラクティス
- Build spatial experiences with RealityKit — visionOS の RealityKit 基礎、没入型シーンの構築
- Discover Quick Look for spatial computing — Quick Look の SharePlay による 3D コンテンツ共有
- Meet ARKit for spatial computing — visionOS の ARKit API、共有空間トラッキング
コメント
GitHub Issues · utterances