Lab 053 – Using Attachments in Portals
We can render SwiftUI views as attachments inside portals, but they are not interactive.
This lab creates two attachment views that are added to the content inside a portal. The portal is a plane mesh presented as a visionOS window. The content of the attachments render as expected, but unfortunately they are not interactive. Buttons, tap gestures, and hover effects are all silently ignored.
Video Demo
Video demo showing several entities inside a portal with attachments above them.
Lab Code
This lab loads a scene from Reality Composer Pro which contains a skybox and two entities. Then we set up the portal in a window and render the scene. Attachments are created and added to the scene content inside the portal.
struct Lab053: View {
// The root for our scene *outside* of the portal
@State var rootEntity = Entity()
// An entity that will render the portal
@State var portalEntity = ModelEntity(
mesh: .generatePlane(width: 1.0, height: 1.0),
materials: [PortalMaterial()]
)
var body: some View {
GeometryReader3D { geometry in
ZStack {
RealityView { content, attachments in
content.add(rootEntity)
// The root for the content that will appear *inside* the portal
let portalContentRoot = Entity()
portalContentRoot.components.set(WorldComponent())
rootEntity.addChild(portalContentRoot)
portalEntity.components.set(PortalComponent(target: portalContentRoot))
rootEntity.addChild(portalEntity)
// We'll load some content to add to the portalContentRoot
guard let scene = try? await Entity(named: "NoFloor", in: realityKitContentBundle) else { return }
if let wetFloorSign = scene.findEntity(named: "wet_floor_sign"),
let wetFloorAttachment = attachments.entity(for: "wet_floor_attachment") {
wetFloorSign.addChild(wetFloorAttachment)
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
}
if let trafficCone = scene.findEntity(named: "traffic_cone_02"),
let traffiConeAttachment = attachments.entity(for: "traffic_cone_attachment") {
scene.addChild(traffiConeAttachment)
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
}
portalContentRoot.addChild(scene)
scene.position.y = -1.4
} update: { content, attachments in
// Resize the scene based on the size of the reality view content.
// Based on the example from Apple https://developer.apple.com/documentation/visionos/displaying-a-3d-environment-through-a-portal
let size = content.convert(geometry.size, from: .local, to: .scene)
portalEntity.model?.mesh = .generatePlane(
width: size.x, height: size.y, cornerRadius: 0.03)
} attachments: {
Attachment(id: "wet_floor_attachment") {
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)
}
.symbolEffect(.bounce)
.onTapGesture {
print("does this do anything?")
}
.hoverEffect()
Text("No Ground")
.font(.largeTitle)
}
.foregroundStyle(.black)
.textCase(.uppercase)
.padding()
}
Attachment(id: "traffic_cone_attachment") {
VStack(spacing: 24) {
Text("Attachments Render")
.font(.extraLargeTitle)
ZStack {
Image(systemName: "triangle")
.font(.system(size: 96, weight: .semibold))
Image(systemName: "eyes")
.font(.system(size: 36, weight: .heavy))
.offset(y:12)
}
.symbolEffect(.breathe)
.onTapGesture {
print("does this do anything?")
}
.hoverEffect()
Text("but are not interactive")
.font(.extraLargeTitle)
}
.padding(24)
.foregroundStyle(.white)
.textCase(.uppercase)
.background(.trafficOrange)
.clipShape(.rect(cornerRadius: 24.0))
}
}
// Size and align the Reality View to sit flush against the window
.frame(depth: 1)
.frame(maxDepth: .infinity, alignment: .back)
}
}
}
}Support our work so we can continue to bring you new examples and articles.

Follow Step Into Vision