Search code examples
swiftuirealitykitvisionos

How to create entity with attachment using RealityView in visionOS?


The challenge is this: Assuming I have a button in the scene, on every tap, I spawn a new entity(added as a child entity to the root).

class ViewModel {
  var emptyRoot: Entity = Entity()

  func addCube() {
    let newCube = generateCube()
    emptyRoot.addChild(newCube)
    // more code....
  }
}

On each spawn, I want to add an attachment to the newly spawned cube(i.e. all spawned entities will have an attachment).

Based on my current knowledge, we typically define all attachments in the attachments closure. However, in my case, I won't know how many entities the user would spawn, so I couldn't define all possible attachments when initializing the RealityView.

RealityView { content, attachments in
    // Load initial content (empty scene at this point NO entities in the scene)
    content.add(viewModel.emptyRoot)
    // At this point I won't have any attachments available.
} update: { updateContent, updateAttachments in
    //
} attachments: {
    // Based on my knowledge, we have to define all available Attachment here and give them ids
}

Solution

  • RealityView Attachments

    Use my code that will help you add cube primitives with 3 (or as many attachments as you want) attachments to your scene. However, nobody forbids you to use the regular text model with zero depth, instead of attachments. In info.plist, set Immersive Space Application Session Role value for a corresponding key.

    import SwiftUI
    
    @main struct AttachmentsApp: App {
        var body: some Scene {
            ImmersiveSpace(id: "AR_ImmersiveSpace") {
                ContentView()
            }
        }
    }
    

    enter image description here

    import SwiftUI
    import RealityKit
    
    struct ContentView : View {
        @State var isClicked: Bool = false
        @State var counter: Int = 0
    
        var body: some View {
            ZStack {
                RealityView { content, attachments in
                    print("App's started")
                } update: { content, attachments in
                    if isClicked {
                        let box = ModelEntity(mesh: .generateBox(size: 0.1))
                        box.position.x = 0.25 * Float(counter)
                        box.name = "box_\(counter)"
                        content.add(box)
                                            
                        if let text = attachments.entity(for: "\(counter)") {
                            text.position.x = box.position.x
                            text.position.y = 0.12
                            content.add(text)
                        }
                    }
                } attachments: {
                    // hard coding
                    Attachment(id: "1") { Text("Cube 1").font(.extraLargeTitle) }
                    Attachment(id: "2") { Text("Cube 2").font(.extraLargeTitle) }
                    Attachment(id: "3") { Text("Cube 3").font(.extraLargeTitle) }
                    // etc...
                }
            }
            Button("Add Model with Attachment") {
                counter += 1
                isClicked = true
                Task {
                    try await Task.sleep(nanoseconds: 10_000)
                    isClicked = false
                }
            }
            .font(.extraLargeTitle)
        }
    }
    

    I believe it's the only way to add attachments in RealityView, since all the attachments must be explicitly registered with unique IDs in their own @ViewBuilder which will be executed before the moment RealityView's make {..} and update {..} closures are run. There is nothing stopping you from creating 100 or 200 аttachments at once. As I said earlier, an alternative to Attachments is to use RealityKit's Text primitives with a zero extrusion depth.

    Text Primitives

    A solution with text primitives is as simple as that:

    } update: { content in
        if isClicked {
            let box = ModelEntity(mesh: .generateBox(size: 0.1))
            box.position.x = 0.25 * Float(counter)
            box.name = "box_\(counter)"
            content.add(box)
                    
            let text = ModelEntity(mesh: .generateText("Cube \(counter)", 
                         extrusionDepth: 0, 
                                   font: .boldSystemFont(ofSize: 0.03)))
            text.model?.materials = [UnlitMaterial()]
            let offset = text.visualBounds(relativeTo: nil).extents.x / 2
            text.position.x = box.position.x - offset
            text.position.y = 0.12
            content.add(text)
        }
    }
    

    This approach has one undeniable advantage over attachments - here's no hard coding.

    enter image description here