2
votes

Goal: SceneKit hit test with SwiftUI (instead of UIKit)

Problem: When I embed the default ship scene on a SwiftUI "UIViewRepresentable", the example handleTap function doesn't work. and I get his error:

"Argument of '#selector' refers to instance method 'handleTap' that is not exposed to Objective-C"

How an I create a hit test, and pass data to another SwiftUI view?

import SwiftUI
import SceneKit

 var handleTap: (() -> Void)

struct ScenekitView : UIViewRepresentable {
    let scene = SCNScene(named: "ship.scn")!

    func makeUIView(context: Context) -> SCNView {
        // create and add a camera to the scene
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)

        // place the camera
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)



        // retrieve the ship node
        let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!



        // retrieve the SCNView
        let scnView = SCNView()
        return scnView
    }

    func updateUIView(_ scnView: SCNView, context: Context) {
        scnView.scene = scene

        // allows the user to manipulate the camera
        scnView.allowsCameraControl = true

        // show statistics such as fps and timing information
        scnView.showsStatistics = true

        // configure the view
        scnView.backgroundColor = UIColor.black

        // add a tap gesture recognizer
      let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
      scnView.addGestureRecognizer(tapGesture)
    }



    func handleTap(_ gestureRecognize: UIGestureRecognizer) {
        // retrieve the SCNView
        let scnView = SCNView()


        // check what nodes are tapped
        let p = gestureRecognize.location(in: scnView)
        let hitResults = scnView.hitTest(p, options: [:])
        // check that we clicked on at least one object
        if hitResults.count > 0 {
            // retrieved the first clicked object
            let result = hitResults[0]

            // get material for selected geometry element
            let material = result.node.geometry!.firstMaterial




            // highlight it
            SCNTransaction.begin()
           SCNTransaction.animationDuration = 0.5

            // on completion - unhighlight
            SCNTransaction.completionBlock = {
                SCNTransaction.begin()
                SCNTransaction.animationDuration = 0.5

                material?.emission.contents = UIColor.black

                SCNTransaction.commit()
            }

            material?.emission.contents = UIColor.green

            SCNTransaction.commit()
        }
    }
}

#if DEBUG
struct ScenekitView_Previews : PreviewProvider {
    static var previews: some View {
        ScenekitView()
    }
}
#endif
1
Adding “@objc” before "func handleTap(...” doesn’t work because the SwiftUI views have to be a classChris McElroy

1 Answers

1
votes

Just hit this issue myself and finally found a solution: make a dummy struct that pulls from a class that actually holds your SCNView.

This works for me:

struct ScenekitView : UIViewRepresentable {
    let scenekitClass = ScenekitClass()

    func makeUIView(context: Context) -> SCNView {
        return scenekitClass.view
    }

    func updateUIView(_ scnView: SCNView, context: Context) {
        // your update UI view contents look like they can all be done in the initial creation
    }
}

class ScenekitClass {
    let view = SCNView()
    let scene = SCNScene(named: "ship.scn")!
    
    init() {
        // create and add a camera to the scene
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)

        // place the camera
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
        
        // attach the scene
        view.scene = scene

        // allows the user to manipulate the camera
        view.allowsCameraControl = true

        // show statistics such as fps and timing information
        view.showsStatistics = true

        // configure the view
        view.backgroundColor = UIColor.black

        // add a tap gesture recognizer
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        view.addGestureRecognizer(tapGesture)
    }
    
    @objc func handleTap(_ gestureRecognize: UIGestureRecognizer) {
        // check what nodes are tapped
        let p = gestureRecognize.location(in: view)
        let hitResults = view.hitTest(p, options: [:])
        // check that we clicked on at least one object
        if hitResults.count > 0 {
            // retrieved the first clicked object
            let result = hitResults[0]

            // get material for selected geometry element
            let material = result.node.geometry!.firstMaterial
            
            // highlight it
            SCNTransaction.begin()
            SCNTransaction.animationDuration = 0.5

            // on completion - unhighlight
            SCNTransaction.completionBlock = {
                SCNTransaction.begin()
                SCNTransaction.animationDuration = 0.5

                material?.emission.contents = UIColor.black

                SCNTransaction.commit()
            }

            material?.emission.contents = UIColor.green

            SCNTransaction.commit()
        }
    }
}

Based on this question.