Spatial SwiftUI: Volume Ornaments
Ornaments on Volumes work much like Ornaments on Windows. Volumes have more anchors and a feature to reorient them when them when a user moves around the volume.
Overview
Volumes have the same anchor points that we saw on windows. Volumes also have anchors for the front and back.

It helps to think of these as three collections of anchors on three planes.
- Front: TopLeadingFront, TopFront, TopTrailingFront and so on…
- Standard / window: Leading, Center, Trailing, and so on…
- Back: BackLeadingFront, BackFront, BackTrailingFront and so on…
The back anchors are particularly useful. We can design our volumes with controls and information at the back while our 3D content takes center stage.
When a user moves around a volume any ornaments will re-orient and reposition based on the users viewpoint. This means the ornaments will always appear in the same formation relative to each other, instead of relative to the content. We’ll look at a way to customize this behavior in the future.
Full Example Code
struct Example043: View {
static let info = ItemInfo(
title: "Spatial SwiftUI: Volume Ornaments",
description: "Ornaments on Volumes work much like Ornaments on Windows. Volumes have more anchors and a feature to reorient them when them when a user moves around the volume.",
scene: .volume
)
@State private var ornamentAnchor: UnitPoint3D = .topFront
@State private var showBounds = true
@State private var isAutoMode = false
@State private var autoModeTimer: Timer?
@State private var animationPhase = 0
private static let autoAnchors: [UnitPoint3D] = {
let perimeter: [UnitPoint] = [
.topLeading, .top, .topTrailing, .trailing,
.bottomTrailing, .bottom, .bottomLeading, .leading, .topLeading
]
let planes: [UnitPoint3D] = [.front, .center, .back]
return planes.flatMap { plane in
perimeter.map { UnitPoint3D(x: $0.x, y: $0.y, z: plane.z) }
}
}()
private func startAutoMode() {
stopAutoMode()
animationPhase = 0
ornamentAnchor = Self.autoAnchors[0]
autoModeTimer = Timer.scheduledTimer(withTimeInterval: 2.5, repeats: true) { _ in
Task { @MainActor in
guard isAutoMode else { return }
animationPhase = (animationPhase + 1) % Self.autoAnchors.count
withAnimation(.easeInOut(duration: 2.0)) {
ornamentAnchor = Self.autoAnchors[animationPhase]
}
}
}
}
private func stopAutoMode() {
autoModeTimer?.invalidate()
autoModeTimer = nil
}
var body: some View {
GeometryReader3D { geometry in
Color.clear
.frame(width: geometry.size.width, height: geometry.size.height)
.frame(depth: geometry.size.depth)
.debugBorder3D(showBounds ? .white : .clear)
}
.ornament(attachmentAnchor: .scene(.center)) {
Example043Controls(
ornamentAnchor: $ornamentAnchor,
showBounds: $showBounds,
isAutoMode: $isAutoMode
)
}
.ornament(attachmentAnchor: .scene(ornamentAnchor)) {
Text("Ornament")
.padding(20)
.background(.stepBlue)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
.onChange(of: isAutoMode) { _, newValue in
if newValue {
startAutoMode()
} else {
stopAutoMode()
}
}
.onDisappear {
stopAutoMode()
isAutoMode = false
}
}
}
private struct Example043Controls: View {
@Binding var ornamentAnchor: UnitPoint3D
@Binding var showBounds: Bool
@Binding var isAutoMode: Bool
private func setDepth(_ point: UnitPoint3D) {
withAnimation {
ornamentAnchor.z = point.z
}
}
var body: some View {
VStack(spacing: 20) {
HStack(spacing: 32) {
VStack {
Text("Anchor")
.font(.headline)
Grid {
GridRow {
Example043AnchorButton(anchor: $ornamentAnchor, point: .topLeading, symbol: "arrow.up.left", title: "Top Leading")
Example043AnchorButton(anchor: $ornamentAnchor, point: .top, symbol: "arrow.up", title: "Top")
Example043AnchorButton(anchor: $ornamentAnchor, point: .topTrailing, symbol: "arrow.up.right", title: "Top Trailing")
}
GridRow {
Example043AnchorButton(anchor: $ornamentAnchor, point: .leading, symbol: "arrow.left", title: "Leading")
Example043AnchorButton(anchor: $ornamentAnchor, point: .center, symbol: "square", title: "Center")
Example043AnchorButton(anchor: $ornamentAnchor, point: .trailing, symbol: "arrow.right", title: "Trailing")
}
GridRow {
Example043AnchorButton(anchor: $ornamentAnchor, point: .bottomLeading, symbol: "arrow.down.left", title: "Bottom Leading")
Example043AnchorButton(anchor: $ornamentAnchor, point: .bottom, symbol: "arrow.down", title: "Bottom")
Example043AnchorButton(anchor: $ornamentAnchor, point: .bottomTrailing, symbol: "arrow.down.right", title: "Bottom Trailing")
}
}
}
VStack {
Text("Depth")
.font(.headline)
Button("Front") { setDepth(.front) }
Button("Center") { setDepth(.center) }
Button("Back") { setDepth(.back) }
}
}
.disabled(isAutoMode)
Toggle("Auto Mode", isOn: $isAutoMode)
Button(showBounds ? "Hide Bounds" : "Show Bounds") {
showBounds.toggle()
}
}
.padding(24)
.glassBackgroundEffect()
}
}
private struct Example043AnchorButton: View {
@Binding var anchor: UnitPoint3D
let point: UnitPoint
let symbol: String
let title: LocalizedStringKey
var body: some View {
Button {
withAnimation {
anchor.x = point.x
anchor.y = point.y
}
} label: {
Image(systemName: symbol)
}
.accessibilityLabel(Text(title))
}
}Support our work so we can continue to bring you new examples and articles.

Follow Step Into Vision