Lab 078 – Building a glass material box

Using spatialOverlay wrap any SwiftUI view in glass material panes.

In Lab 075 we took a look at debugBorder3D. Apple shared this useful utility function at WWDC. Today I decided to adapt it to render glass material panes around a SwiftUI View.

I started with the four sides they provided in debugBorder3D. This creates the back, front, left, and right sides of a box. This uses spatialOverlay to draw a Spatial Container on top of the parent view, then uses rotation3DLayout to rotate the left and right panes.

spatialOverlay {
    ZStack {
        // Back
        Color.clear
            .glassBackgroundEffect(displayMode: backDisplayMode)
            .padding(padding)


        // Create the sides just the like back and front, but rotate them on y
        ZStack {
            Color.clear
                .glassBackgroundEffect(displayMode: leadingDisplayMode)
                .padding(padding)
            Spacer()
            Color.clear
                .glassBackgroundEffect(displayMode: trailingDisplayMode)
                .padding(padding)
        }
        .rotation3DLayout(.degrees(90), axis: .y)

        // Front
        Color.clear
            .glassBackgroundEffect(displayMode: frontDisplayMode)
            .padding(padding)
    }
}

For the top and bottom I added a second spatial overlay to this one. This use the same concept but will rotate these panes on the X axis instead of Y.

.spatialOverlay {
// see above
}
.spatialOverlay {
    ZStack {
        Color.clear
            .glassBackgroundEffect(displayMode: bottomDisplayMode)
            .padding(padding)
        Spacer()
        Color.clear
            .glassBackgroundEffect(displayMode: topDisplayMode)
            .padding(padding)
    }
    .rotation3DLayout(.degrees(90), axis: .x)
}

Next I wanted to be able show or hide panes. I decided to use Edge3D.Set.

func glassBackgroundBox(padding: CGFloat = 0, _ directions: Edge3D.Set) -> some View {
...
}

This will let me do things like this.

.glassBackgroundBox(padding: 12, .top, .bottom)
.glassBackgroundBox(padding: 12, .vertical)
.glassBackgroundBox(padding: 12, .all)

I used the displayMode value of glassBackgroundEffect to show and hide each pane based on the current value of directions. For example, the top pane looks at three conditions.

let topDisplayMode: GlassBackgroundDisplayMode = directions.contains(.top) || directions.contains(.all) || directions.contains(.vertical) ? .always : .never

Let’s see it in action.

Full Lab Code

This still needs some work, but here is the full function.

xtension View {
    func glassBackgroundBox(padding: CGFloat = 0, _ directions: Edge3D.Set) -> some View {

        // compute the display mode based on the edge set
        let topDisplayMode: GlassBackgroundDisplayMode = directions.contains(.top) || directions.contains(.all) || directions.contains(.vertical) ? .always : .never
        let bottomDisplayMode: GlassBackgroundDisplayMode = directions.contains(.bottom) || directions.contains(.all) || directions.contains(.vertical) ? .always : .never
        let leadingDisplayMode: GlassBackgroundDisplayMode = directions.contains(.leading) || directions.contains(.all) || directions.contains(.horizontal) ? .always : .never
        let trailingDisplayMode: GlassBackgroundDisplayMode = directions.contains(.trailing) || directions.contains(.all) || directions.contains(.horizontal) ? .always : .never
        let frontDisplayMode: GlassBackgroundDisplayMode = directions.contains(.front) || directions.contains(.all) || directions.contains(.depth) ? .always : .never
        let backDisplayMode: GlassBackgroundDisplayMode = directions.contains(.back) || directions.contains(.all) || directions.contains(.depth) ? .always : .never

        return spatialOverlay {
            ZStack {
                // Back
                Color.clear
                    .glassBackgroundEffect(displayMode: backDisplayMode)
                    .padding(padding)


                // Create the sides just the like back and front, but rotate them on y
                ZStack {
                    Color.clear
                        .glassBackgroundEffect(displayMode: leadingDisplayMode)
                        .padding(padding)
                    Spacer()
                    Color.clear
                        .glassBackgroundEffect(displayMode: trailingDisplayMode)
                        .padding(padding)
                }
                .rotation3DLayout(.degrees(90), axis: .y)

                // Front
                Color.clear
                    .glassBackgroundEffect(displayMode: frontDisplayMode)
                    .padding(padding)
            }



        }
        // Create the top and bottom with another overlay and rotate the panes on the X axis
        .spatialOverlay {
            ZStack {
                Color.clear
                    .glassBackgroundEffect(displayMode: bottomDisplayMode)
                    .padding(padding)
                Spacer()
                Color.clear
                    .glassBackgroundEffect(displayMode: topDisplayMode)
                    .padding(padding)
            }
            .rotation3DLayout(.degrees(90), axis: .x)
        }
    }
}

The rest of the lab shows a usage example and adds some buttons to toggle each value of the edge set.

struct Lab078: View {

    @State var panes: Edge3D.Set = [.all]
    @State var padding: CGFloat = 24

    var body: some View {
        VStack {
            ModelViewSimple(name: "ToyRocket", bundle: realityKitContentBundle)
                .frame(width: 500, height: 500)
                .frame(depth: 500)
                .glassBackgroundBox(padding: padding, panes)
        }
        .ornament(attachmentAnchor: .scene(.trailing), contentAlignment: .leading, ornament: {
            VStack(spacing: 6) {

                Slider(value: $padding,
                       in: 0...96,
                       step: 1,
                       minimumValueLabel: Text("0"),
                       maximumValueLabel: Text("96"),
                       label: {
                    Text("Padding")
                })
                .frame(width: 200)

                HStack {

                    Button(action: {
                        if panes.contains(.vertical) {
                            panes.remove(.vertical)
                        } else {
                            panes.insert(.vertical)
                        }

                    }, label: {
                        Text("Vertical")
                            .frame(width: 80)
                    })

                    Button(action: {
                        if panes.contains(.top) {
                            panes.remove(.top)
                        } else {
                            panes.insert(.top)
                        }

                    }, label: {
                        Text("Top")
                            .frame(width: 80)
                    })

                    Button(action: {
                        if panes.contains(.bottom) {
                            panes.remove(.bottom)
                        } else {
                            panes.insert(.bottom)
                        }

                    }, label: {
                        Text("Bottom")
                            .frame(width: 80)
                    })

                }

                HStack {

                    Button(action: {
                        if panes.contains(.horizontal) {
                            panes.remove(.horizontal)
                        } else {
                            panes.insert(.horizontal)
                        }

                    }, label: {
                        Text("Horizontal")
                            .frame(width: 80)
                    })

                    Button(action: {
                        if panes.contains(.leading) {
                            panes.remove(.leading)
                        } else {
                            panes.insert(.leading)
                        }

                    }, label: {
                        Text("Leading")
                            .frame(width: 80)
                    })

                    Button(action: {
                        if panes.contains(.trailing) {
                            panes.remove(.trailing)
                        } else {
                            panes.insert(.trailing)
                        }

                    }, label: {
                        Text("Trailing")
                            .frame(width: 80)
                    })
                }

                HStack {

                    Button(action: {
                        if panes.contains(.depth) {
                            panes.remove(.depth)
                        } else {
                            panes.insert(.depth)
                        }

                    }, label: {
                        Text("Depth")
                            .frame(width: 80)
                    })

                    Button(action: {
                        if panes.contains(.front) {
                            panes.remove(.front)
                        } else {
                            panes.insert(.front)
                        }

                    }, label: {
                        Text("Front")
                            .frame(width: 80)
                    })

                    Button(action: {
                        if panes.contains(.back) {
                            panes.remove(.back)
                        } else {
                            panes.insert(.back)
                        }

                    }, label: {
                        Text("Back")
                            .frame(width: 80)
                    })

                }

                Button(action: {
                    if panes.contains(.all) {
                        panes.remove(.all)
                    } else {
                        panes.insert(.all)
                    }

                }, label: {
                    Text("All")
                        .frame(width: 80)
                })


            }
            .controlSize(.small)
            .padding()
            .glassBackgroundEffect()
        })
    }
}

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

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

Questions or feedback?