はじめに

こんにちは!システム開発部のYです。

WWDC2025では、visionOS 26に追加されるSwiftUIの空間レイアウト機能が強化されました。
本記事では、以下のセッション内容をまとめています。詳細は WWDC セッションをご参照ください。

Meet SwiftUI spatial layout / SwiftUIの空間レイアウトの紹介

目次

SwiftUIレイアウトシステム

SwiftUIレイアウトシステムの3D拡張機能は、既存の2Dレイアウトの概念に基づいています。
初めてSwiftUIレイアウトを扱う方は、以下のWWDCセッションをご参照ください。

Building Custom Views with SwiftUI

Compose custom layouts with SwiftUI

新APIの概要

3D views

visionOSではビューは3Dです。レイアウトシステムも同じ動作をしますが、2次元ではなく3次元に適用されます。
それぞれのビューに対して、幅と高さに加えて、SwiftUIは深さとZ位置も計算します。

// Many views have 0 depth

HStack {
  Image("RobotHead")
    .debugBorder3D(.red)
  Text("Hello! I'm a piece of text. I have 0 depth.")
    .debugBorder3D(.red)
  Color.blue
    .debugBorder3D(.red)
    .frame(width: 200, height: 200)
}

ビューは 3D ですが、奥行きがゼロのものも存在します。
Image、Color、Textなど、平面的なエクスペリエンスを構築するために使用されるビューの多くは奥行きがゼロであり、iOSと同じ動作になります。

// RealityView takes up all available space including depth

RealityView { content in
  // Setup RealityView content
}
.debugBorder3D(.red)

visionOSでは、RealityViewのような特定のビューは、デフォルトで提案された利用可能な奥行きをすべて占有します。

// GeometryReader3D uses all available depth

GeometryReader3D { proxy in
  // GeometryReader3D content
}
.debugBorder3D(.red)

GeometryReader3Dは、Model3Dにresizableモディファイアを適用した場合と同様に、柔軟なサイズ変更が可能です。
このため、ロボットはウィンドウの幅に合わせて引き伸ばされます。
使用可能なスペースに合わせてスケーリングすることができます。

// Scaling a Model3D to fit available space

Model3D(url: robotURL) { resolved in
  resolved.resizable()
} placeholder: {
  ProgressView()
}
.scaledToFit3D()
.debugBorder3D(.red)

resizable()に加えて、新しいscaledToFit3Dモディファイアを使用することで、ロボットはモデルの縦横比を維持しながら、利用可能な幅・高さ、さらに奥行きに合わせてサイズを変更できるようになりました。

Window は幅・高さと同様にルートから奥行きの提案を受けますが、この奥行きは固定され、超過した部分はシステムによって切り取られます。
一方、Volumeも幅・高さ・奥行きの提案を受けますが、奥行きもリサイズ可能です。
Windowと Volumeの使い分けについては、ヒューマンインターフェースガイドライン「Designing for visionOS」を参照してください。

// ZStack composes subview depths

ZStack {
  Model3D(named: "LargeRobot")
    .debugBorder3D(.red)
  Model3D(named: "BabyBot")
    .debugBorder3D(.red)
}
.debugBorder3D(.yellow)

一部のビューでは、含まれるビューの奥行きの提案を変更できます。
VStackでサブビューの高さが決まるように、ZStackでは奥行きが決まります。
このZStackの奥行きは、前後に並ぶ 2 体のロボットが収まるために必要な奥行きです。

// HStack also composes subview depths

HStack {
  Model3D(named: "LargeRobot")
    .debugBorder3D(.red)
  Model3D(named: "BabyBot")
    .debugBorder3D(.red)
}
.debugBorder3D(.yellow)

既存のレイアウトタイプやスタックは visionOS 上で実際に 3D であり、奥行きにも適切なデフォルト動作が適用されます。
この例では、HStackは親から奥行きの提案を受け継ぎ、内部の2つのモデルにちょうど収まるように独自の奥行きを設定します。

Depth alignments

奥行きアライメントは、3Dビューと奥行きに効果的に対応するために、既存のSwiftUIレイアウトタイプをカスタマイズできる新しいツールです。

// Customizing depth alignments

struct RobotProfile: View {
  let robot: Robot

  var body: some View {
    VStackLayout().depthAlignment(.front) {
      ResizableRobotView(asset: robot.model3DAsset)
      RobotNameCard(robot: robot)
    }
    .frame(width: 300)
  }
}

デフォルトでは、スタックとレイアウトタイプは背面の奥行きアライメントを使用しています。
visionOS 26では、任意のレイアウトタイプのDepthAlignmentsをカスタマイズできるようになりました。
ここでは、depthAlignmentモディファイアに.frontアライメントを適用しています。

struct FavoriteRobotsRow: View {
  let robots: [Robot]

  var body: some View {
    HStack {
      RobotProfile(robot: robots[2])
      RobotProfile(robot: robots[0])
      RobotProfile(robot: robots[1])
    }
  }
}

HStackで3体のロボットを表示するボリュームを作成しています。
depthPodiumを使って、お気に入りのロボットほど近くに配置します。
これを実現するには、カスタム奥行きアライメントが必要です。

// Defining a custom depth alignment guide

struct DepthPodiumAlignment: DepthAlignmentID {
  static func defaultValue(in context: ViewDimensions3D) -> CGFloat {
    context[.front]
  }
}

extension DepthAlignment {
  static let depthPodium = DepthAlignment(DepthPodiumAlignment.self)
}

まず、DepthAlignmentIDプロトコルに準拠した新しい構造体を定義します。
次に、このアライメントのデフォルトとして 1 つの要件を実装します。ここではDepthPodiumAlignmentのデフォルトとして.frontアライメントガイドを使用します。
最後に、この新しいDepthAlignmentID型を利用する静的な定数を、奥行きアライメントに定義します。

// Views can customize their alignment guides

struct FavoritesRow: View {
  let robots: [Robot]

  var body: some View {
    HStackLayout().depthAlignment(.depthPodium) {
        RobotProfile(robot: robots[2])
        RobotProfile(robot: robots[0])
          .alignmentGuide(.depthPodium) {
            $0[DepthAlignment.back]
          }
        RobotProfile(robot: robots[1])
      		.alignmentGuide(.depthPodium) {
            $0[DepthAlignment.center]
          }
    }
  }
}

これで、各ロボットを含むHStackの奥行きアライメントとして、このdepthPodiumアライメントガイドを使用できるようになりました。
最後尾のロボットは、depthPodiumアライメントガイドの中央に奥行きが揃うようにカスタマイズします。
中央のロボットは、背面がdepthPodiumガイドに揃うように変更します。
先頭のロボットは、このアライメントのデフォルト通り、前面ガイドを使い続けます。

Rotation layout

回転レイアウトは、より高度な3Dユースケースに最適なツールです。

// Rotate views using visual effects

Model3D(named: "ToyRocket")
  .rotation3DEffect(.degrees(45), axis: .z)

既存のrotation3DEffectモディファイアはよく知られているかもしれません。
このモディファイアは基本的な回転には最適です。
しかし、HStackでモデルに説明カードをつけて配置し、Z軸に沿ってロケットを90度回転させると、
ロケットがカードにぶつかり、ボリュームからはみ出してしまいます。

rotation3DEffect を適用すると、見た目は赤い実線のワイヤーフレームのように回転しますが、レイアウトシステムは回転前のジオメトリを基準とした青い破線のワイヤーフレームを参照します。
これは、rotation3DEffectが描画上の視覚効果であり、レイアウト計算には影響を与えないためです。

// Rotate using any axis or angle

HStackLayout().depthAlignment(.front) {
  RocketDetailsCard()
  Model3D(named: "ToyRocket")
  	.rotation3DLayout(.degrees(isRotated ? 45 : 0), axis: .z)
}

visionOS 26では、レイアウトシステム内の回転ビューのフレームを変更する新しいrotation3DLayoutモディファイアが導入されます。
このモディファイアをロケットモデルに適用すると、HStackがサイズと配置を調整し、ロケットと詳細カードに余裕を持たせることができます。

struct RobotCarousel: View {
  let robots: [Robot]

  var body: some View {
	VStack {
      Spacer()
      MyRadialLayout {
        ForEach(robots) { robot in
          ResizableRobotView(asset: robot.model3DAsset)
          	.rotation3DLayout(.degrees(-90), axis: .x)
        }
      }
      .rotation3DLayout(.degrees(90), axis: .x)
  }
}

ロボットを円形に配置する方法を見ていきます。
ここではRadialLayoutを使用します。
このカスタムレイアウトタイプは、利用可能な幅と高さで定義された円周上にビューを配置します。

ロボットの3Dモデルを配置する場合でも、ForEachを使ってリサイズ可能な各ロボットのModel3Dをこのカスタムレイアウト内に配置できます。
今回はロボットをボリューム内で水平に設置したいので、rotation3DLayoutを放射状レイアウトに適用し、X軸に沿ってビューを90度回転させます。
ただし、このままだとロボットは横倒しになるため、ForEach内でrotation3DEffectをX軸に沿って -90度回転させ、ロボットを立たせます。
さらに、カルーセルをボリュームのベースプレートと同じ高さに配置したい場合は、2Dレイアウトと同様の方法を使えます。VStackの中にカルーセルを配置し、その上にスペーサーを置くことで高さを調整できます。

Spatial containers

モデルの下部にリングを表示します。
このリングはModel3Dとして表示されるため、軸方向に沿って他の要素と重ならないように配置を調整します。

// Aligning views in 3D space

SpatialContainer {
  LargeBox()
  MediumBox()
  SmallBox()
}

新しいSpatialContainer APIでは、入れ子人形のように複数のビューを同じ3D空間に配置できます。
さらに、各ビューには 3D アライメントを適用することが可能です。

// Aligning views in 3D space

SpatialContainer(alignment: .bottomFront) {
  LargeBox()
  MediumBox()
  SmallBox()
}

ここでは、すべての子ビューを.bottomFrontアライメントガイドに沿って並べています。

// Aligning views in 3D space

SpatialContainer(alignment: .topTrailingBack) {
  LargeBox()
  MediumBox()
  SmallBox()
}

次に、.topTrailingBackアライメントガイドに沿って並べた場合の例です。

// Aligning overlayed content

LargeBox()
  .spatialOverlay(alignment: .bottomLeadingFront) {
    SmallBox()
  }

spatialOverlayも同様のツールで、1つのビューを別のビューと同じ3D空間に重ね合わせることができます。
SpatialContainerと同様に、3Dアライメントがサポートされています。

struct RobotCarouselItem: View {
  let robot: Robot
  let isSelected: Bool

  var body: some View {
    ResizableRobotView(asset: robot.model3DAsset)
			.spatialOverlay(alignment; .bottom) {
        if isSelected {
          ResizableSelectionRingModel()
        }
  }
}

ロボットモデルにspatialOverlayモディファイアを追加します。
選択済みとしてマークされた場合、サイズ変更可能なリングビューをコンテンツとして配置します。
.bottomアライメントを使用して、リングの底辺とロボットの底辺を揃えます。

extension View {
    func debugBorder3D(_ color: Color) -> some View {
        spatialOverlay {
            ZStack {
                Color.clear.border(color, width: 4)
                
                ZStack {
                    Color.clear.border(color, width: 4)
                    Spacer()
                    Color.clear.border(color, width: 4)
                }
                .rotation3DLayout(.degrees(90), axis: .y)
                
                Color.clear.border(color, width: 4)
            }
        }
    }
}

debugBorder3Dモディファイアを実装します。

まず、Viewのextensionとして、debugBorder3Dメソッドを定義します。
変更したコンテンツにspatialOverlayを適用する事で、適用先のビューと同じ3D空間に境界線をレンダリングします。
ZStackに、2D境界線、スペーサ、もう一つの2D境界線を配置します。
次に、ZStack全体にrotation3DLayoutを適用する事で、先頭と末尾のビューに境界線を配置します。
最後に、この内部ZStackの背面と前面に2D境界線を配置します。
これで、デバッグ用debugBorder3Dが完成します。

まとめ

visionOS 26で追加されたSwiftUIの空間レイアウトについてまとめました。
今回のアップデートで、SwiftUIにおけるモデル表示がかなり実装しやすくなった印象です。

特に、rotation3DEffectを使用した際に発生しがちなモデルの見切れ問題については、他のViewと組み合わせたりレイアウト計算を行いたい場合にrotation3DLayoutを使うことで、より正確なレイアウトが可能になります。

また、spatialOverlayを活用したdebugBorder3Dモディファイアは、デバッグ時に非常に有効で、すぐに実装して活用したい機能だと感じました。

その他にも気になる新機能が多く、今後いろいろ試してみたいと思っています。
この記事が少しでも参考になれば幸いです。詳細はWWDCのセッションをご参照ください。

参考



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる