RealityKit Basics: attachments
We can create 2D SwiftUI attachments and add them to 3D scenes as entities.
Overview
Attachments in RealityKit are one of my favorite features. We can create SwiftUI views, then pass them into a RealityView as an attachment. When we retrieve them in the make or update closures, RealityKit transforms them into entities that we can use along with our other assets.
Adding an attachment is always a two step process
- Define an attachment with an id and a view
- Retrieve the attachment by id and add it to the scene. We do this by calling
attachments.entity(for: ).
RealityView { content, attachments in
if let scene = try? await Entity(named: "RKBasicsLoading", in: realityKitContentBundle) {
content.add(scene)
if let earth = scene.findEntity(named: "Earth") {
// Step 2: Retrieve the attachment by id and add it to the scene
if let earthPanel = attachments.entity(for: "EarthPanel") {
earth.addChild(earthPanel, preservingWorldTransform: true)
earthPanel.setPosition([0, -0.1, 0.1], relativeTo: earth)
}
}
}
} update: { content, attachments in
} attachments: {
// Step 1: Define an attachment with an id and a view
Attachment(id: "EarthPanel") {
HStack {
Image(systemName: "globe.europe.africa")
Text("Earth")
.font(.headline)
}
.padding()
.glassBackgroundEffect()
}
}We can work with attachments in two main ways.
- As child entities of other entities in our scene.
- As standalone entities in our scene
We have examples of both here. This adds the earthPanel as a child of the earth entity.
if let earthPanel = attachments.entity(for: "EarthPanel") {
// ❌ Don't add an attachment like this unless you want the SwiftUI content to scale with the entity
// earth.addChild(earthPanel)
// ✅ Instead, use preservingWorldTransform to keep the attachment entity transform/scale
earth.addChild(earthPanel, preservingWorldTransform: true)
earthPanel.setPosition([0, -0.1, 0.1], relativeTo: earth)
}Notice that we use addChild(earthPanel, preservingWorldTransform: true). This will let us keep the intrinsic size and transform of the attachment entity, regardless of the parent. There are situations where you may want to scale the attachments with the parent though. For example, I do this for the text on the gravestones in Project Graveyard. In those cases, set preservingWorldTransform to false or use addChild().

We can also add attachments as standalone entities in the scene.
RealityView { content, attachments in
if let scene = try? await Entity(named: "RKBasicsLoading", in: realityKitContentBundle) {
content.add(scene)
// Get the attachment by id
if let titlePanel = attachments.entity(for: "TitlePanel") {
// add it to the content
content.add(titlePanel)
titlePanel.setPosition([0, 0.2, -0.2], relativeTo: nil)
}
}
} update: { content, attachments in
} attachments: {
Attachment(id: "TitlePanel") {
VStack {
Text("Earth & Moon")
.font(.largeTitle)
Text("Best of friends for 4.53 billion years")
.font(.caption)
}
.padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 24))
}
}Attachments are an incredibly powerful feature. They can display simple bits of data or more complex UI. Be away that many SwiftUI controls that rely on the presentation API are not supported in attachments. Common controls like picker require some additional work.
Full Example Code
struct Example050: View {
var body: some View {
RealityView { content, attachments in
if let scene = try? await Entity(named: "RKBasicsLoading", in: realityKitContentBundle) {
content.add(scene)
scene.position.y = -0.4
if let earth = scene.findEntity(named: "Earth"), let moon = scene.findEntity(named: "Moon") {
if let earthPanel = attachments.entity(for: "EarthPanel") {
// ❌ Don't add an attachment like this unless you want the SwiftUI content to scale with the entity
// earth.addChild(earthPanel)
// ✅ Instead, use preservingWorldTransform to keep the attachment entity transform/scale
earth.addChild(earthPanel, preservingWorldTransform: true)
earthPanel.setPosition([0, -0.1, 0.1], relativeTo: earth)
}
if let moonPanel = attachments.entity(for: "MoonPanel") {
moon.addChild(moonPanel, preservingWorldTransform: true)
moonPanel.setPosition([0, -0.05, 0.15], relativeTo: moon)
}
}
if let titlePanel = attachments.entity(for: "TitlePanel") {
content.add(titlePanel)
titlePanel.setPosition([0, 0.2, -0.2], relativeTo: nil)
}
}
} update: { content, attachments in
} attachments: {
Attachment(id: "EarthPanel") {
HStack {
Image(systemName: "globe.europe.africa")
Text("Earth")
.font(.headline)
}
.padding()
.glassBackgroundEffect()
}
Attachment(id: "MoonPanel") {
HStack {
Image(systemName: "moon.circle.fill")
Text("Moon")
.font(.headline)
}
.padding()
.glassBackgroundEffect()
}
Attachment(id: "TitlePanel") {
VStack {
Text("Earth & Moon")
.font(.largeTitle)
Text("Best of friends for 4.53 billion years")
.font(.caption)
}
.padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 24))
}
}
}
}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.

Follow Step Into Vision