How to make the contents in an immersive view clickable?
Asked Answered
M

1

2

I'm trying to add buttons to my immersive view. The buttons have been added perfectly, but when I click on them, nothing happens. The click event is not being recognized at all.

Here's my implementation

struct ImmersiveView: View {
    // ... other properties
    @State private var opacity: Double = 1.0
    @State private var hotspots: [Hotspot] = []
    @State private var selectedHotspot: Hotspot?
    @State private var isTransitioning = false
    
    var body: some View {
        ZStack {
            RealityView { content in
                // Load immersive content
            } update: { content in
                // Update immersive content
            }
            .opacity(opacity)
            
            // Interactive hotspots
            ForEach(hotspots, id: \.id) { hotspot in
                createHotspotEntity(hotspot: hotspot) // Function to create hotspots
            }
        }
        .task {
            await loadTextureForCurrentImage() // Loading images
        }
        .gesture(
            SpatialTapGesture()
                .targetedToAnyEntity()
                .onEnded { value in
                    handleTap(value) // Handling tap gesture
                }
        )
    }
    
    private func createHotspotEntity(hotspot: Hotspot) -> ModelEntity {
        let hotspotEntity = ModelEntity()
        
        let hudYellow = UIColor(red: 1.0, green: 0.8, blue: 0.0, alpha: 1.0)
        let radius: Float = 1.5
        
        let sphereMesh = MeshResource.generateSphere(radius: radius)
        var material = UnlitMaterial()
        material.color = .init(tint: hudYellow.withAlphaComponent(0.8))
        let sphereEntity = ModelEntity(mesh: sphereMesh, 
                                       materials: [material],
                                       collisionShape: .generateSphere(radius: radius),
                                       mass: 0.0)
        
        hotspotEntity.addChild(sphereEntity)
        
        let worldRadius: Float = 85
        let phi = Float.pi / 2 - hotspot.position.y
        let theta = hotspot.position.x
        let x = worldRadius * sin(phi) * cos(theta)
        let y = worldRadius * cos(phi)
        let z = worldRadius * sin(phi) * sin(theta)
        
        hotspotEntity.position = [x, y, z]
        hotspotEntity.look(at: [0, 0, 0], from: hotspotEntity.position, relativeTo: nil)
        hotspotEntity.name = "Hotspot_\(hotspot.id)"
        
        hotspotEntity.components[DestinationComponent.self] = DestinationComponent(destinationId: hotspot.destinationImageId ?? "", label: hotspot.label)
        hotspotEntity.components[HotspotComponent.self] = HotspotComponent()
        
        // Add components for interaction
        hotspotEntity.components.set(InputTargetComponent(allowedInputTypes: .indirect))
        hotspotEntity.components.set(CollisionComponent(shapes: [.generateSphere(radius: radius)], mode: .trigger, filter: .sensor))
        
        return hotspotEntity
    }

    private func handleTap(_ value: EntityTargetValue<SpatialTapGesture.Value>) {
        print("Handling tap on entity: \(value.entity.name)")
        
        if let destinationComponent = value.entity.components[DestinationComponent.self] {
            let destinationId = destinationComponent.destinationId
            print("Hotspot tapped. Destination ID: \(destinationId)")
            transitionToNextImage(destinationId: destinationId)
        } else {
            print("Tapped entity does not have a DestinationComponent")
        }
    }

    private func transitionToNextImage(destinationId: String) {
        guard !isTransitioning else { return }
        
        // Transition logic
    }
}
Mitrailleuse answered 30/9, 2024 at 3:36 Comment(0)
F
1

I think you are trying to make a Dashboard which is a buttons in a fixed place which not changing when the user turn his head, There are multiple solutions for that, all started with making an anchor to the head of the user and then add a child to it which is gonna be either an Attachment (SwiftUI Buttons and labels) or A 3d buttons you can create previously in some 3d creating tool like blender then export it as USDZ and add it inside the project then add a gesture on it to be clickable, Or to build the 3d buttons inside the immersive view as 3d boxes, set it as a child to the head then add a gesture to it. all methods are working but remember that for your application to be accepted by apple you need to make sure the 3d items are not overlapping the buttons which is hard to make if you use first method which is the easiest method unfortunately, that is why apple recommends the 3d buttons method and you can sort it as a create last while the other immersive view 3d objects are first this way you won't have an overlap, anyway lets explain each method as follows:

Method A) Attachments:

import SwiftUI
import RealityKit

struct ContentView3: 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()
                }) {
                    Image("img")
                        .resizable()
                        .scaledToFill()
                        .aspectRatio(contentMode: .fill)
                        .frame(maxWidth: 100, maxHeight: 100, alignment: .center)
                        .contentShape(Rectangle())
                }
                .background(Color.blue)
                .buttonStyle(.plain)
            }
        }
    }
}

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

The only problem with this method although it is simple is the overlapping

enter image description here

Method B) 3D entity (either pre-created USDZ or InCode 3d entities Like the example:

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()
            })
        )
  }
}

The only thing you need to remember while making the 2nd method is that it applies on the ModelEntity not the Entity and it does not apply on the Childs of the entity so you need to set the group order for each ModelEntity one by one not for a hierarchy of an entity. so it is kinda hard work but its the best solution.

enter image description here

Fieldpiece answered 30/9, 2024 at 17:34 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.