Spatial SwiftUI: rotation3DLayout

A rotation modifier that will impact frame and layout.

Overview

We have already taken a look at rotation3DEffect in a previous example. Let’s check out the new rotation3DLayout modifier and see how these differ.

We can use rotation3DLayout to rotate a view. This will rotate a view by 10 degrees on the Y axis.

.rotation3DLayout(.degrees(10), axis: .y)

We could just as easily do the same thing with rotation3DEffect, so why use rotation3DLayout? This line in the documentation says it all.

The layout system will use a bounding box that completely contains the rotated view, meaning this modifier can change the size of the view it is applied to.

When we use rotation3DEffect, it is important to think of it as a visual effect. It will rotate the view, but it won’t have any other impact. But rotation3DLayout will impact frame and layout. The bounding box may nudge other views in a stack and may cause the parent view size to change.

rotation3DEffect vs. rotation3DLayout

The first row uses rotation3DEffect on the Earth. Notice that the blue box sticks out of the parent view and overlaps with the card next to it.

The second row uses rotation3DLayout. This one maintains some space between the Earth and the card view. It also caused the parent view to expand slightly, to accommodate the rotated bounding box.

Using rotation3DLayout will be particularly important when working on Spatial Layouts. Apple used it in the carousel example in this WWDC session to great effect. We’ll make use of this as we create our own Spatial Layouts.

As of visionOS Beta 3, rotation3DLayout does not include options to change the rotation anchor or perspective like we can with rotation3DEffect.

Video Demo

Example Code

struct Example092: View {

    @State private var alignment: DepthAlignment = .front
    @State private var showDebugLines = false

    @State private var angle: Angle = .degrees(0)

    var body: some View {
        VStackLayout().depthAlignment(alignment) {

            HStackLayout().depthAlignment(alignment) {

                ModelView(name: "Earth")
                    .frame(width: 150, height: 150)
                    .debugBorder3D(showDebugLines ? .blue : .clear)
                    .rotation3DEffect(angle, axis: .y)

                VStack {
                    Text("rotation3DEffect")
                        .font(.title)
                    Text("This will *not* impact the parent frame or layout.")
                        .font(.caption)
                }
                .padding()
                .background(.black)
                .cornerRadius(24)
                .shadow(radius: 20)
                .frame(width: 220, height: 160)

            }
            .debugBorder3D(showDebugLines ? .white : .clear)

            HStackLayout().depthAlignment(alignment) {

                ModelView(name: "Earth")
                    .frame(width: 150, height: 150)
                    .debugBorder3D(showDebugLines ? .green : .clear)
                    .rotation3DLayout(angle, axis: .y)

                VStack {
                    Text("rotation3DLayout")
                        .font(.title)
                    Text("This *will* impact the parent frame or layout.")
                        .font(.caption)
                }
                .padding()
                .background(.black)
                .cornerRadius(24)
                .shadow(radius: 20)
                .frame(width: 240, height: 160)

            }
            .debugBorder3D(showDebugLines ? .white : .clear)

        }
        .ornament(attachmentAnchor: .scene(.trailing), contentAlignment: .trailing, ornament: {
            VStack(alignment: .center, spacing: 8) {
                Button(action: {
                    withAnimation {
                        angle = .degrees(0)
                    }
                }, label: {
                    Text("Angle: 0")
                })

                Button(action: {
                    withAnimation {
                        angle = .degrees(45)
                    }
                }, label: {
                    Text("Angle: 45")
                })

                Button(action: {
                    showDebugLines.toggle()
                }, label: {
                    Text("Debug")
                })
            }
            .padding()
            .controlSize(.small)
            .glassBackgroundEffect()

        })

    }
}

#Preview {
    Example092()
}

// Adapted from Example 051 - Spatial SwiftUI: Model3D
fileprivate struct ModelView: View {

    @State var name: String = ""

    var body: some View {
        Model3D(named: name, bundle: realityKitContentBundle)
        { phase in
            if let model = phase.model {
                model
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else if phase.error != nil {
                Text("Could not load model \(name).")
            } else {
                ProgressView()
            }
        }
    }
}

Download the Xcode project with this and many more examples from Step Into Vision.

Some examples are provided as standalone Xcode projects. You can find those here.

Questions or feedback?