Search code examples
iosswiftscenekitarkitscnnode

How to attach a struct or class object to a SCNNode


I'm trying to create a scene where different users can display different cars. When the user first signs into the app they get uid, username etc. When they choose a car the car model has the type of car and a user object that identifies them.

I can create a car and implement the hitTest to find which car was tapped. What I can't figure out is how do I assign a data model to the car that was tapped so that I know who's who?

The only property I found on the node so far is the node.name, I can attach the userId to it and just go through an array of users to find out who's who but that doesn;t seem practical if I have a lot of users. I also need that property when removing a node:

sceneView.session.pause()
sceneView.scene.rootNode.enumerateChildNodes { (node, _) in
    // this would actually remove all the ferraries on the scene but this is just an example
    if node.name == "ferrari" {
        node.removeFromParentNode()
    }
}

How can I attach a data model to a SCNNode?

class User {
    var userId: String?
    var username: String?
}

class RaceCar {
    var user: User?
    var type: String?
}

// user creates an account and picks a ferrari

let raceCar = RaceCar()
let type = raceCar.type!

switch type {

    case "ferrari":

        createFerrariAndAddItToScene(raceCar)

    case "bmw":
    ...etc
}

func createFerrariAndAddItToScene(_ raceCar: RaceCar) {

      let image = art.scnassests/"\(raceCar.typ!e).jpg"

      let material = SCNMaterial()
      material.diffuse.contents = image

      let plane = SCNPlane(width: 0.1, height: 0.1)
      plane.materials = [material]

      let node = SCNNode()
      node.name = raceCar.type!
      node.geometry = plane
      node.position = SCNVector(0.0, 0.0, -0.2)

      sceneView.scene.rootNode.addChildNode(node)
}

@objc func nodeWasTapped(_ sender: UITapGestureRecognizer) {

    guard let sceneView = sender.view as? ARSCNView else { return }  
    let touchLocation = sender.location(in: sceneView)

    let hitResults = sceneView.hitTest(touchLocation, options: [:])

    if !hitResults.isEmpty {

        guard let hitResult = hitResults.first  else { return }  
        let node = hitResult.node

        print(node.name) // I need to get some info from the dataModel here like node.raceCar.user.username or node.raceCar.user.userId
    }
}

Solution

  • You can subclass SCNNode and add any data model you need to it:

    class CarNode: SCNNode {
      var raceCar: RaceCar?
    
      init(raceCar: RaceCar, geometry: SCNGeometry) {
        super.init()
    
        self.geometry = geometry
    
        self.raceCar = raceCar
      }
    
      required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    }
    

    To create new node simply use this new class:

    func createFerrariAndAddItToScene(_ raceCar: RaceCar) {
      ...
      let node = CarNode(raceCar: raceCar, geometry: plane)
      ...
    }
    

    To access your data model you can do like so:

    @objc func nodeWasTapped(_ sender: UITapGestureRecognizer) {
      ...
      if let node = hitResult.node as? CarNode {
        print(node.raceCar.user.username)
      }
    }