WWDC Quick Look 💓 By SwiftGGTeam
Better together: SwiftUI and RealityKit

Better together: SwiftUI and RealityKit

元の動画を見る

Highlight

visionOS 26 で SwiftUI と RealityKit が本格的に融合します。Model3D は直接アニメーションを再生でき、RealityKit の Entity は SwiftUI ビューやジェスチャーを受け止め、座標系も双方向で貫通するようになりました。

主要内容

これまで visionOS で 3D ロボットを表示しようとすると、開発者は Model3D と RealityView のどちらを使うかという二者択一を迫られがちでした。Model3D は 1 行のコードで SwiftUI のレイアウトに組み込めますが、アニメーションやパーティクルは扱えません。一方で着せ替えやコリジョン、パーティクルエミッタを使いたければ RealityView に切り替える必要があり、すると今度は別の問題に直面します。RealityView はデフォルトで利用可能な領域をすべて占有してしまうため、隣に並べた NameSign がたちまち画面の端に押しやられてしまうのです。

visionOS 26 では、この経路が完全に整備されました。Apple は Model3D に Model3DAsset を追加し、モデルに同梱されたアニメーショントラックを直接読み込めるようにしました。さらに新しい ConfigurationCatalog と組み合わせることで外観を切り替えられます。RealityView へとアップグレードする際は、新たに追加された .realityViewLayoutBehavior(.fixedSize) によって、Model3D と同じようにモデルの境界にぴったり沿わせることができます。RealityKit 側でも SwiftUI スタイルの新コンポーネントが 3 つ公開されました。ViewAttachmentComponent、GestureComponent、PresentationComponent の 3 種です。加えて新しい ManipulationComponent によって、ユーザーは片手または両手で仮想オブジェクトを掴めるようになりました。Entity は Observable になり、AnimationPlaybackController も Observable になっています。SwiftUI ビューはこれらの RealityKit オブジェクトを直接 @Bindable でき、状態が両者を自由に行き来します。

詳細

Model3D でアニメーションとコントローラーをロードする03:34)。Model3DAsset がモデルファイルを非同期にロードし、それを Model3D に渡して表示します。選択中のアニメーションは animationPlaybackController 経由で公開され、SwiftUI の Slider と組み合わせて時間スクラブを実現できます。

struct RobotView: View {
  @State private var asset: Model3DAsset?
  var body: some View {
    if asset == nil {
      ProgressView().task { asset = try? await Model3DAsset(named: "sparky") }
    } else if let asset {
      VStack {
        Model3D(asset: asset)
        AnimationPicker(asset: asset)
        if let animationController = asset.animationPlaybackController {
          RobotAnimationControls(playbackController: animationController)
        }
      }
    }
  }
}

ポイント:

  • Model3DAsset(named:) は非同期イニシャライザなので、.task の中で待機する必要があります。
  • asset == nil の段階では ProgressView でフォールバックし、ロードが完了してから本物の Model3D に切り替えます。
  • asset.animationPlaybackController はアニメーション選択後に Model3DAsset が生成するもので、外部で自分で new してはいけません。
  • AnimationPlaybackController は visionOS 26 で Observable になっているため、そのまま view model として使えます。

@Bindable で SwiftUI コントロールを RealityKit のコントローラーに繋ぐ04:32)。

struct RobotAnimationControls: View {
  @Bindable var controller: AnimationPlaybackController

  var body: some View {
    HStack {
      Button(controller.isPlaying ? "Pause" : "Play") {
        if controller.isPlaying { controller.pause() }
        else { controller.resume() }
      }

      Slider(
        value: $controller.time,
        in: 0...controller.duration
      ).id(controller)
    }
  }
}

ポイント:

  • @Bindable var controller によって、RealityKit のクラスをそのまま SwiftUI の observable なデータソースとして使えます。
  • controller.isPlaying の変化はビューの再評価をトリガーし、ボタンの表示が自動的に同期します。
  • $controller.time は Slider のドラッグをそのままコントローラーに書き戻し、時間スクラブを実現します。
  • .id(controller) はアニメーション切り替え時に Slider を強制的に再構築し、前のアニメーションのレンジが残ってしまうのを防ぎます。

Model3D から RealityView へスムーズに移行する07:25)。RealityView はデフォルトで利用可能な領域をすべて占有しますが、.fixedSize を付けると初回レイアウト時の Entity の visual bounds でサイズが決まります。

struct RobotView: View {
  let url: URL = Bundle.main.url(forResource: "sparky", withExtension: "reality")!

  var body: some View {
    HStack {
      NameSign()
      RealityView { content in
        if let sparky = try? await Entity(contentsOf: url) {
          content.add(sparky)
        }
      }
      .realityViewLayoutBehavior(.fixedSize)
    }
  }
}

ポイント:

  • .fixedSize.flexible.centered の 3 つから選びます。それぞれ境界に密着、領域いっぱいに占有、中央に配置という挙動に対応します。
  • サイズは make クロージャ完了後に一度だけ評価されるため、その後で Entity のボリュームが変化しても RealityView が再び広がることはありません。
  • これら 3 つの修飾子は RealityView の原点を移動するだけで、Entity 自体の位置やスケールを書き換えることはありません。

ManipulationComponent で Entity を両手で掴めるようにする13:18)。configureEntity を 1 行呼ぶだけで、ManipulationComponent、CollisionComponent、InputTargetComponent、HoverEffectComponent をまとめて装着してくれます。

RealityView { content in
  let sparky = await loadSparky()
  content.add(sparky)
  ManipulationComponent.configureEntity(
    sparky,
    hoverEffect: .spotlight(.init(color: .purple)),
    allowedInputTypes: .all,
    collisionShapes: myCollisionShapes()
  )
}

ポイント:

  • configureEntity は static な便利メソッドで、インタラクションに必要なコンポーネント一式を自動的に補ってくれます。
  • hoverEffect は注視やホバー時の視覚的フィードバックを制御します。ここでは紫色のスポットライトを使用しています。
  • allowedInputTypes: .all を指定すると、ダイレクトタッチと gaze + pinch の間接入力の両方を許可します。
  • collisionShapes は掴める範囲を決定します。nil を渡せば自動生成のシェイプが使われます。
  • 掴む/離す際のサウンドエフェクトを差し替えたい場合は、audioConfiguration.none にしたうえで ManipulationEvents.DidHandOff を購読し、自前で再生してください。

ViewAttachmentComponent + GestureComponent で Entity に SwiftUI を生やす17:10)。

struct AttachmentComponentAttachments: View {
  var body: some View {
    RealityView { content in
      let bolts = await loadAndSetupBolts()
      let attachment = ViewAttachmentComponent(
          rootView: NameSign("Bolts"))
      let nameSign = Entity(components: attachment)
      place(nameSign, above: bolts)
      bolts.components.set(GestureComponent(
        TapGesture().onEnded {
          nameSign.isEnabled.toggle()
        }
      ))
      content.add(bolts)
      content.add(nameSign)
    }
    .realityViewLayoutBehavior(.centered)
  }
}

ポイント:

  • ViewAttachmentComponent は任意の SwiftUI ビューをそのまま rootView として受け取れるため、RealityView の初期化時に attachments を宣言する必要はもうありません。
  • Entity(components: attachment) はコンポーネントを初期化引数として注入できるので、set を 1 回省略できます。
  • GestureComponent は標準的な SwiftUI のジェスチャーを受け取り、コールバック内の座標はデフォルトで Entity のローカル空間になります。
  • GestureComponent を使う際、対象 Entity には依然として InputTargetComponent と CollisionComponent が必要です。これらが欠けるとイベントを受け取れません。

重要ポイント

  • やること: 既存の Model3D を Model3DAsset ベースのプレイヤーへアップグレードする

    • なぜ取り組む価値があるか: もともと静的に表示していた 3D アセットが、アーティストがエクスポートしたアニメーションをそのまま再利用できるようになり、ユーザーは着せ替えやタイムラインのスクラブを行えます。
    • どこから始めるか: モデルをアニメーション付きの reality ファイルとしてエクスポートし、Model3DAsset(named:) でロードして Picker + Slider で最小限の動作デモを組み立てましょう。
  • やること: visionOS アプリ内で「領域を埋め尽くす」RealityView を .realityViewLayoutBehavior(.fixedSize) に置き換える

    • なぜ取り組む価値があるか: レイアウト崩れの多くは、RealityView がデフォルトで領域を奪うことに起因します。修飾子を 1 つ追加するだけで、周囲の SwiftUI コントロールと並べて配置できるようになります。
    • どこから始めるか: プロジェクト内のすべての RealityView を検索し、表示用途のものには .fixedSize を付け、インタラクティブなシーンには .flexible を残しましょう。
  • やること: 掴める 3D オブジェクトに ManipulationComponent.configureEntity を 1 行追加する

    • なぜ取り組む価値があるか: よく使う 4 つのコンポーネントが一気に揃い、システム提供の握る音も加わって、ほぼゼロコストで仮想オブジェクトを両手のインタラクションに対応させられます。
    • どこから始めるか: まずプロトタイプの中核となる展示オブジェクトに対して有効化し、ManipulationEvents.DidUpdateTransform を購読してポーズをビジネス側の状態へ書き戻しましょう。
  • やること: ViewAttachmentComponent を使って情報カード、ボタン、説明テキストを直接 Entity に取り付ける

    • なぜ取り組む価値があるか: SwiftUI ビューがコンポーネントとしてマウントされた後は、Entity と一緒にシーンから管理され、ジェスチャーに反応し、Observable な状態に駆動されます。
    • どこから始めるか: 現在 RealityViewAttachments で書いている nameSign / tooltip を ViewAttachmentComponent に置き換えてから、GestureComponent を取り付けてタップに反応させてみましょう。

関連 Session

コメント

GitHub Issues · utterances