Spatial SwiftUI: Custom alignment with spatialOverlay

We can use AlignmentID to customize the alignment of a view within a spatialOverlay.

Overview

As we saw in the example for spatialOverlay, we can use common alignment options like leading, trailing, top, bottom, etc. What if we want a bit more control of how our content will be aligned inside the overlay? We can create custom alignments using AlignmentID.

Let’s make one that will position a view at 1/3 of the width of the container.

private struct ThirdHorizontalAlignment: AlignmentID {
    static func defaultValue(in context: ViewDimensions) -> CGFloat {
        context.width / 3
    }
}

Usage:

.init(
    horizontal: .init(ThirdHorizontalAlignment.self), // our custom alignment
    vertical: .center,
    depth: .front
)

We can create as many of these as we need. Using the type directly anytime we need an alignment can be a little cumbersome. We can extends alignments to add custom values.

private struct ExplicitHorizontalAlignment: AlignmentID {
    static func defaultValue(in context: ViewDimensions) -> CGFloat {
        context.width * 0.66
    }
}
extension HorizontalAlignment {
    static let explicit = HorizontalAlignment(ExplicitHorizontalAlignment.self)
}

Usage:

.init(horizontal: .explicit, // our custom alignment
      vertical: .center,
      depth: .front)

Video Demo

Example Code

struct Example101: View {

    @State private var alignmentSign: Alignment3D = .init(horizontal: .center, vertical: .center, depth: .front)

    var body: some View {
        VStack {

            Spacer()

            // Use an Earth Model with a simple 2D view as an overlay
            ModelViewSimple(name: "Earth", bundle: realityKitContentBundle)
                .frame(width: 600, height: 600)
                .debugBorder3D(.white)
                .spatialOverlay(alignment: alignmentSign) {
                    VStack {
                        Text("Earth")
                            .font(.headline)
                        Text("The only planet known to serve ice cream")
                            .font(.caption)
                    }
                    .padding()
                    .background(.black)
                    .cornerRadius(24)
                    .breakthroughEffect(.prominent)
                }
        }
        .ornament(attachmentAnchor: .scene(.trailing), ornament: {
            VStack(alignment: .leading) {
                Button(action: {
                    withAnimation {
                        alignmentSign = .init(horizontal: .center, vertical: .center, depth: .front)
                    }
                }, label: {
                    Text("Center Front")
                })

                Button(action: {
                    withAnimation {
                        alignmentSign = .init(horizontal: .leading, vertical: .top, depth: .front)
                    }
                }, label: {
                    Text("Top Left Front")
                })

                Button(
                    action: {
                        withAnimation {
                            alignmentSign = .init(
                                horizontal: .init(ThirdHorizontalAlignment.self),
                                vertical: .init(ThirdVerticalAlignment.self),
                                depth: .front
                            )
                        }
                    },label: {
                        Text("Fractional Alignment")
                    })

                Button(
                    action: {
                        withAnimation {
                            alignmentSign = .init(horizontal: .explicit,
                                                  vertical: .explicit,
                                                  depth: .front)

                        }
                    },label: {
                        Text("Explicit Alignment")
                    })

            }
            .frame(width: 240)
            .padding()
            .glassBackgroundEffect()
        })

    }

}

// Custom Fractional Examples
fileprivate struct ThirdHorizontalAlignment: AlignmentID {
    static func defaultValue(in context: ViewDimensions) -> CGFloat {
        context.width / 3
    }
}
fileprivate struct ThirdVerticalAlignment: AlignmentID {
    static func defaultValue(in context: ViewDimensions) -> CGFloat {
        context.height / 3
    }
}

// Explicite Examples: 0.66 on X and 1.0 on Y
fileprivate struct ExplicitHorizontalAlignment: AlignmentID {
    static func defaultValue(in context: ViewDimensions) -> CGFloat {
        context.width * 0.66
    }
}
fileprivate extension HorizontalAlignment {
    static let explicit = HorizontalAlignment(ExplicitHorizontalAlignment.self)
}

fileprivate struct ExplicitVerticalAlignment: AlignmentID {
    static func defaultValue(in context: ViewDimensions) -> CGFloat {
        context.height * 1.0
    }
}
fileprivate extension VerticalAlignment {
    static let explicit = VerticalAlignment(ExplicitVerticalAlignment.self)
}

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?