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.

Questions or feedback?