RealityKit Basics: Using ViewAttachmentComponent

visionOS 26 brings us a new way to create attachments right along side our entities.

Overview

visionOS 26 has a handful of new component. Let’s take a look at ViewAttachmentComponent. We’ll recreate the example from this post, using the new method for creating attachments.

RealityKit Basics: Placing attachments in a scene

First off, it is important to know that we can still create attachments using RealityView. This method involves using a closure to define all the attachments that a given scene will access. We generally do this up front when the view is created, but it is possible to do this at other times. Once an attachment has been added to this closure, it becomes available as an entity in the attachments value inside the RealityView make or update closures. We simply get it by ID and add it to our scene.

Now, with ViewAttachmentComponent, we get more control for when and where to create these attachments. Here is a minimum viable attachment.

let entity = Entity()
let attachment = ViewAttachmentComponent(rootView: SomeView())
entity.components.set(attachment)
content.add(entity)

We create an entity, create the attachment component with a view. Then we add the component to the entity and add the entity to our RealityView Content. Structurally speaking, once these are added to a scene nothing has really changed from the previous approach. They are still just entities with components that happen to draw SwiftUI content. The only thing that has changed is the way we create them.

Let’s step through the three signs from the example. We’ll start with the warning sign. This will add the attachment as a top-level entity in the scene.

let warningSign = Entity()
let attachment = ViewAttachmentComponent(rootView: WarningSignView())
warningSign.components.set(attachment)
warningSign.position = [1, 1.2, -2]
content.add(warningSign)

Notice that the rootView is set to WarningSignView().

fileprivate struct WarningSignView: View {
    var body: some View {
        VStack(spacing: 24) {
            Text("This scene contains gratuitous warnings")
                .font(.system(size: 96, weight: .bold))
                .textCase(.uppercase)
                .multilineTextAlignment(.center)
        }
        .padding(24)
        .foregroundStyle(.white)
        .background(.black)
        .clipShape(.rect(cornerRadius: 24.0))
    }
}

The other two signs create attachments as child entities that are positioned relative to their parent.

Wet Floor Sign

if let wetFloorSign = scene.findEntity(named: "wet_floor_sign") {

    // Create an entity and use the new ViewAttachmentComponent
    let wetFloorAttachment = Entity()
    let attachment = ViewAttachmentComponent(rootView: WetFloorSignView())
    wetFloorAttachment.components.set(attachment)


    // Add the attachment entity as a child of the wet floor sign
    wetFloorSign.addChild(wetFloorAttachment)

    // Adjust the transform to position it just in front of the sign
    let transform = Transform(scale: .init(repeating: 200), rotation: simd_quatf(Rotation3D(angle: Angle2D(degrees: 11), axis: RotationAxis3D(x: -1, y: 0, z: 0))), translation: [0, 30, 6.7])
    wetFloorAttachment.transform = transform
}

Traffic Cone

if let trafficCone = scene.findEntity(named: "traffic_cone_02") {

    // Create an entity and use the new ViewAttachmentComponent
    let traffiConeAttachment = Entity()
    let attachment = ViewAttachmentComponent(rootView: TrafficConeView())
    traffiConeAttachment.components.set(attachment)

    // For this example, we'll add the attachment directly to the scenc content
    content.add(traffiConeAttachment)

    // Then we'll use the data from the traffic cone entity to determine the transform for the attachment
    let transform = Transform(
        scale: .init(repeating: 1.0),
        rotation: simd_quatf(
            Rotation3D(angle: Angle2D(degrees: -24), axis: RotationAxis3D(x: 0, y: 1, z: 0))
        ),
        translation: trafficCone.position + [0, 0.8 , 0]
    )

    traffiConeAttachment.transform = transform
}

As you can see, all three examples use the same Entity > Component pattern we are used to.

When should we use this?

There are a handful of situations when I think we should use this component instead of the builder pattern.

  • It can be more convenient and easy to reason about. The attachments are managed just like any other component.
  • We can more easily pass data from our entities into the views for our attachments. For example, we may want to pass in a tint color from a material so we can use the same color in the view.
  • It can be a lot easier to manage scenes that need to dynamically add and remove entities with attachments. In Project Graveyard, I had to do some extra work to keep track of the gravestone entities and their child attachments. With this new component, I can simplify that code.

Full Example Code

struct Example082: View {
    var body: some View {
        RealityView { content in

            guard let scene = try? await Entity(named: "Caution", in: realityKitContentBundle) else { return }
            content.add(scene)

            // This lab can only run on version 26 and later
            guard #available(visionOS 26.0, *) else {
                print("WARNING: This lab requires VisionOS 26.0 or later.")
                return
            }

            // Example 01 - add an attachment as a child of an entity
            // Get the sign model from the scene
            if let wetFloorSign = scene.findEntity(named: "wet_floor_sign") {

                // Create an entity and use the new ViewAttachmentComponent
                let wetFloorAttachment = Entity()
                let attachment = ViewAttachmentComponent(rootView: WetFloorSignView())
                wetFloorAttachment.components.set(attachment)


                // Add the attachment entity as a child of the wet floor sign
                wetFloorSign.addChild(wetFloorAttachment)

                // Adjust the transform to position it just in front of the sign
                let transform = Transform(scale: .init(repeating: 200), rotation: simd_quatf(Rotation3D(angle: Angle2D(degrees: 11), axis: RotationAxis3D(x: -1, y: 0, z: 0))), translation: [0, 30, 6.7])
                wetFloorAttachment.transform = transform
            }

            // Example 02 - use an entity to position the attachment. Add the attachment to the scene content
            if let trafficCone = scene.findEntity(named: "traffic_cone_02") {

                // Create an entity and use the new ViewAttachmentComponent
                let traffiConeAttachment = Entity()
                let attachment = ViewAttachmentComponent(rootView: TrafficConeView())
                traffiConeAttachment.components.set(attachment)

                // For this example, we'll add the attachment directly to the scenc content
                content.add(traffiConeAttachment)

                // Then we'll use the data from the traffic cone entity to determine the transform for the attachment
                let transform = Transform(
                    scale: .init(repeating: 1.0),
                    rotation: simd_quatf(
                        Rotation3D(angle: Angle2D(degrees: -24), axis: RotationAxis3D(x: 0, y: 1, z: 0))
                    ),
                    translation: trafficCone.position + [0, 0.8 , 0]
                )

                traffiConeAttachment.transform = transform
            }

            // Example 03 - Add the attachment as a standalone entity
            let warningSign = Entity()
            let attachment = ViewAttachmentComponent(rootView: WarningSignView())
            warningSign.components.set(attachment)
            warningSign.position = [1, 1.2, -2]
            content.add(warningSign)

        }
    }


}

fileprivate struct WetFloorSignView: View {
    var body: some View {
        VStack(spacing: 24) {
            Text("CAUTION")
                .font(.largeTitle)
            ZStack {
                Image(systemName: "triangle")
                    .font(.system(size: 96, weight: .semibold))
                Image(systemName: "figure.fall")
                    .font(.system(size: 42, weight: .heavy))
                    .offset(y:12)
            }
            Text("No Floor")
                .font(.largeTitle)
        }
        .foregroundStyle(.black)
        .textCase(.uppercase)
        .padding()
    }
}

fileprivate struct TrafficConeView : View {
    var body: some View {
        VStack(spacing: 24) {
            Text("Watch Out")
                .font(.extraLargeTitle)
            ZStack {
                Image(systemName: "triangle")
                    .font(.system(size: 96, weight: .semibold))
                Image(systemName: "eyes")
                    .font(.system(size: 36, weight: .heavy))
                    .offset(y:12)
            }
            Text("for traffic cones")
                .font(.extraLargeTitle)
        }
        .padding(24)
        .foregroundStyle(.white)
        .textCase(.uppercase)
        .background(.trafficOrange)
        .clipShape(.rect(cornerRadius: 24.0))
    }
}

fileprivate struct WarningSignView: View {
    var body: some View {
        VStack(spacing: 24) {
            Text("This scene contains gratuitous warnings")
                .font(.system(size: 96, weight: .bold))
                .textCase(.uppercase)
                .multilineTextAlignment(.center)
        }
        .padding(24)
        .foregroundStyle(.white)
        .background(.black)
        .clipShape(.rect(cornerRadius: 24.0))
    }
}

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?