Spatial SwiftUI: Volumetric presentation with attachments

Exploring a workaround to system SwiftUI views that use presentations in RealityView attachments.

As of visionOS 2, we cannot use the SwiftUI presentations with RealityView attachments.

If we try to open an alert or sheet, we will see an error:

Presentations are not currently supported in Volumetric contexts.

Many SwiftUI views use presentations. For example, if we try to use the stock DatePicker we’ll crash the app with this error:

Presentations are not permitted within volumetric window scenes.

This means we can’t use common views and controls like sheets, alerts, pickers, etc. in our SwiftUI when we are using attachments. We’ll run into this issues in volumes and immersive spaces.

❌ For reference, here is the view now. The button and date picker in the attachment will both fail.

struct Example053: View {

    @State private var showingSheet: Bool = false
    @State private var someDate = Date()

    var body: some View {
        RealityView { content, attachments in

            guard let scene = try? await Entity(named: "SwiftUIScienceLab", in: realityKitContentBundle) else { return }
            content.add(scene)
            scene.position.y = -0.4

            if let panel = attachments.entity(for: "AttachmentContent") {
                panel.position.y = 0.1
                content.add(panel)
            }


        } update: { content, attachments in

        } attachments: {
            Attachment(id: "AttachmentContent") {
                VStack() {

                    Text("Presentations are not supported")
                        .font(.extraLargeTitle2)

                    Button("Show Sheet", action: {
                        showingSheet = true
                    })

                    DatePicker("Date",
                               selection: $someDate,
                               displayedComponents: .date
                    )
                    
                    Spacer()
                }
                .sheet(isPresented: $showingSheet) {
                    Text("some view")
                }
                .padding()
                .frame(width: 460, height: 500)
                .glassBackgroundEffect()
            }
        }
    }
}

One way we could work around this limitation is to use a second attachment. Let’s tackle the “alert” usecase since that one is easier to start with. We’ll add a second attachment that wills show the content of our alert.

Attachment(id: "AlertContent") {
    VStack() {
        Text("Wow, it works")
            .font(.extraLargeTitle2)
        Button("Close", action: {
            withAnimation {
                showingSheet = false
            }
        })
        Spacer()
    }
    .padding()
    .frame(width: 360, height: 200)
    .glassBackgroundEffect()
    .opacity(showingSheet ? 1 : 0)
}

Next, let’s create some transforms for our two attachments.

  • transformMain – this is where we will place the card in our volume
  • transformMainAlertShowing – we’ll move the main card here when the alert is showing
  • transformAlert – this is where we’ll place the alert
    let transformMain = Transform(
        scale: SIMD3<Float>(repeating: 1),
        rotation: simd_quatf(angle: 0, axis: SIMD3<Float>(0, 1, 0)),
        translation: SIMD3<Float>(0, 0, -0.1)
    )

    let transformMainAlertShowing = Transform(
        scale: SIMD3<Float>(repeating: 1),
        rotation: simd_quatf(angle: 0, axis: SIMD3<Float>(0, 1, 0)),
        translation: SIMD3<Float>(0, 0, -0.12)
    )

    let transformAlert = Transform(
        scale: SIMD3<Float>(repeating: 1),
        rotation: simd_quatf(angle: 0, axis: SIMD3<Float>(0, 1, 0)),
        translation: SIMD3<Float>(0, 0.4, -0.1)
    )

We’ll use the same showingSheet variable that we tried to use with the sheet. When this value is true, we need to move the main card back, and show the alert card.

We could improve this further by animating the opacity component on the attachment entity, instead of just setting isEnabled. We’ll look at some options for using pickers in another example post.

struct Example053: View {

    @State private var showingSheet: Bool = false

    let transformMain = Transform(
        scale: SIMD3<Float>(repeating: 1),
        rotation: simd_quatf(angle: 0, axis: SIMD3<Float>(0, 1, 0)),
        translation: SIMD3<Float>(0, 0, -0.1)
    )

    let transformMainAlertShowing = Transform(
        scale: SIMD3<Float>(repeating: 1),
        rotation: simd_quatf(angle: 0, axis: SIMD3<Float>(0, 1, 0)),
        translation: SIMD3<Float>(0, 0, -0.12)
    )

    let transformAlert = Transform(
        scale: SIMD3<Float>(repeating: 1),
        rotation: simd_quatf(angle: 0, axis: SIMD3<Float>(0, 1, 0)),
        translation: SIMD3<Float>(0, 0.4, -0.1)
    )

    var body: some View {
        RealityView { content, attachments in

            guard let scene = try? await Entity(named: "SwiftUIScienceLab", in: realityKitContentBundle) else { return }
            content.add(scene)
            scene.position.y = -0.4

            if let panel = attachments.entity(for: "AttachmentContent") {
                panel.move(to: transformMain, relativeTo: scene)
                content.add(panel)
            }

            if let alert = attachments.entity(for: "AlertContent") {
                alert.move(to: transformAlert, relativeTo: scene)
                alert.isEnabled = showingSheet
                content.add(alert)
            }


        } update: { content, attachments in

            if let panel = attachments.entity(for: "AttachmentContent") {
                panel
                    .move(
                        to: showingSheet ? transformMainAlertShowing : transformMain,
                        relativeTo: panel.parent,
                        duration: 0.25,
                        timingFunction: .easeOut
                    )
            }

            if let alert = attachments.entity(for: "AlertContent") {
                alert.isEnabled = showingSheet
            }

        } attachments: {
            Attachment(id: "AttachmentContent") {
                VStack() {

                    Text("Faking an alert")
                        .font(.extraLargeTitle2)

                    Text("Click the button to show a fake alert. We'll use another attachment for the alert content. We will also move this attachment back a bit on the z axis when the alert is shown.")

                    Button("Show Sheet", action: {
                        withAnimation {
                            showingSheet.toggle()
                        }
                    })

                    Spacer()
                }
                .opacity(showingSheet ? 0 : 1)
                .padding()
                .frame(width: 460, height: 500)
                .glassBackgroundEffect()
            }

            Attachment(id: "AlertContent") {
                VStack() {

                    Text("Wow, it works")
                        .font(.extraLargeTitle2)

                    Button("Close", action: {
                        withAnimation {
                            showingSheet = false
                        }
                    })

                    Spacer()
                }
                .padding()
                .frame(width: 360, height: 200)
                .glassBackgroundEffect()
                .opacity(showingSheet ? 1 : 0)
            }

        }
    }
}

Support our work so we can continue to bring you new examples and articles.

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?