Search code examples
xcodeswiftuirealitykitvisionos

How to prevent Entity from Hiding an attachment SwiftUI Button in VisionOS Znear in RealityView


I have an app which have an Immersive Space view and it needs the user to have a button in the bottom which have a fixed place in front of the user head like a dashboard in game or so but when the user get too close to any3d object in the view it could cover the button and make it inaccessible and it mainly would prevent the app for being approved like that in appstoreconnect I was working before on SceneKit and there was something like camera view Znear and Zfar which decide when to hide the 3d model if it comes too close or gets too far and I wonder if there is something like that in realityView / RealityKit 4. Here is My Code and the screenshots follows

import SwiftUI

import RealityKit

struct ContentView: View {

@State var myHead: Entity = {
    let headAnchor = AnchorEntity(.head)
    headAnchor.position = [-0.02, -0.023, -0.24]
    return headAnchor
}()

@State var clicked = false

var body: some View {
    RealityView { content, attachments in
        // create a 3d box
        let mainBox = ModelEntity(mesh: .generateBox(size: [0.1, 0.1, 0.1]))
        mainBox.position = [0, 1.6, -0.3]
        
        content.add(mainBox)
        
        content.add(myHead)
        guard let attachmentEntity = attachments.entity(for: "Dashboard") else {return}
        
        myHead.addChild(attachmentEntity)

    }
    attachments: {
        // SwiftUI Inside Immersivre View
        Attachment(id: "Dashboard") {
            VStack {
                Spacer()
                    .frame(height: 300)
                Button(action: {
                    goClicked()
                }) {
                    Text(clicked ? "⏸️" : "▶️")
                        .frame(maxWidth: 48, maxHeight: 48, alignment: .center)
                        .font(.extraLargeTitle)
                }
                .buttonStyle(.plain)
            }
        }
    }
}

func goClicked() {
    clicked.toggle()
}
}

enter image description here

enter image description here


Solution

  • Thanks to Apple Pro Engineer, they have a very useful answer

    It is not currently possible to render a reality view attachment such that it always appears in front of other 3D models in the scene.

    but they also provide a useful workaround as

    import SwiftUI
    import RealityKit
    
    struct ContentView: View {
    
    @State var myHead: Entity = {
        let headAnchor = AnchorEntity(.head)
        headAnchor.position = [0, -0.15, -0.4]
        return headAnchor
    }()
    
    // Use a model entity to act as a "dashboard" instead of an attachment.
    @State var dashboardEntity: ModelEntity = {
        let dashboardEntity = ModelEntity(mesh: .generateSphere(radius: 0.02), materials: [])
        dashboardEntity.generateCollisionShapes(recursive: false)
        dashboardEntity.components.set(InputTargetComponent())
        return dashboardEntity
    }()
    
    @State var clicked = false
    
    var clickedMaterial = SimpleMaterial(color: .green, isMetallic: false)
    
    var unclickedMaterial = SimpleMaterial(color: .red, isMetallic: false)
    
    var body: some View {
        
        RealityView { content in
            // create a 3d box
            let mainBox = ModelEntity(mesh: .generateBox(size: [0.1, 0.1, 0.1]), materials: [SimpleMaterial()])
            mainBox.position = [0, 1.6, -0.3]
            
            content.add(mainBox)
            
            content.add(myHead)
            myHead.addChild(dashboardEntity)
    
            // Create a model sort group for both entities.
            let group = ModelSortGroup(depthPass: .postPass)
            // Sort the box entity so that it is drawn first.
            let mainBoxSortComponent = ModelSortGroupComponent(group: group, order: 1)
            mainBox.components.set(mainBoxSortComponent)
            // Sort the dashboard entity so that it is drawn second, on top of the box.
            let dashboardSortComponent = ModelSortGroupComponent(group: group, order: 2)
            dashboardEntity.components.set(dashboardSortComponent)
        }
        update: { content in
            // Update the dashboard entity's material when the value of `clicked` changes.
            dashboardEntity.model?.materials = clicked ? [clickedMaterial] : [unclickedMaterial]
        }
        .gesture(
            TapGesture()
                .targetedToEntity(dashboardEntity)
                .onEnded({ value in
                    // Toggle `clicked` when the dashboard entity is tapped.
                    clicked.toggle()
                })
            )
     }
    }