Here is an example that may be helpful using a SCNBox
.
BoxNode
is a custom SCNNode
that uses an SCNBox
for its geometry. Each side is set to a different colour so that it is easy to see, you can use UIImage
s if you have something more elaborate that you want to show. As we are using SceneKit
, UIKit
, and SwiftUI
make sure you import them where needed.
class BoxNode: SCNNode {
override init() {
let length: CGFloat = 5
let box = SCNBox(width: length, height: length, length: length, chamferRadius: 0)
box.materials = [UIColor.red, .yellow, .green, .blue, .purple, .brown].map {
let material = SCNMaterial()
material.diffuse.contents = $0
material.isDoubleSided = true
material.transparencyMode = .aOne
return material
}
super.init()
self.geometry = box
self.name = "boxNode"
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
This is then used inside a custom SCNScene
, which adds the BoxNode
as a root node and sets up a rotation animation and pivots the cube.
class GameScene: SCNScene {
override init() {
super.init()
let cubeNode = BoxNode()
self.rootNode.addChildNode(cubeNode)
let xAngle = SCNMatrix4MakeRotation(.pi / 3.8, 1, 0, 0)
let zAngle = SCNMatrix4MakeRotation(-.pi / 4, 0, 0, 1)
cubeNode.pivot = SCNMatrix4Mult(SCNMatrix4Mult(xAngle, zAngle), cubeNode.transform)
// Rotate the cube
let animation = CAKeyframeAnimation(keyPath: "rotation")
animation.values = [SCNVector4(1, 1, 0.3, 0 * .pi),
SCNVector4(1, 1, 0.3, 1 * .pi),
SCNVector4(1, 1, 0.3, 2 * .pi)]
animation.duration = 5
animation.repeatCount = HUGE
cubeNode.addAnimation(animation, forKey: "rotation")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The GameScene
is then set inside a SCNView
inside a UIViewController
. The SCNView
is pinned to the edges of the ViewController and we override touchesBegan
so that we can interact with the SceneView
. I also create a variable sideTapped
as this will allow you to inject a callback so that you can tell which side has been tapped on your cube.
class GameSceneViewController: UIViewController {
private let sceneView: SCNView = .init(frame: .zero)
private let scene = GameScene()
var sideTapped: ((Int) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
sceneView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sceneView)
NSLayoutConstraint.activate([
sceneView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
sceneView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
sceneView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
sceneView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
sceneView.scene = scene
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touchPoint = touches.first?.location(in: sceneView) else { return }
guard let hitTestResult = sceneView.hitTest(touchPoint, options: nil).first else { return }
guard hitTestResult.node is BoxNode else { return }
sideTapped?(hitTestResult.geometryIndex)
}
}
As we are using this inside SwiftUI
we need a UIViewControllerRepresentable
. Here we pass the sideTapped
function to the GameSceneViewController
.
struct GSViewControllerRepresentable: UIViewControllerRepresentable {
let sideTapped: ((Int) -> Void)?
func makeUIViewController(context: Context) -> GameSceneViewController {
let vc = GameSceneViewController()
vc.sideTapped = sideTapped
return vc
}
func updateUIViewController(_ uiViewController: GameSceneViewController, context: Context) {
}
}
Now we can add this to a SwiftUI
view.
struct ContentView: View {
var body: some View {
GSViewControllerRepresentable { side in
print(side)
// Perform additional logic here
}
.frame(width: 200, height: 200)
}
}
The end result should look something like this:

Tapping on a side of cube will print a number from 0 to 5 representing which side you tapped. Based on the materials used the numbers should correspond to the index of the material, so in this case:
- 0 is red
- 1 is yellow
- 2 is green
- 3 is blue
- 4 is purple
- 5 is brown
That should give you something that you should be able to switch your navigation off of.
I am no expert in SceneKit but I was playing with it at the weekend and had pretty much done what you were asking about. Hopefully this gives you an idea, there are many ways to achieve what you want to do, this is one possibility.
If you are wanting to swipe the cube to changes faces, rather than just having it rotate by itself, you might want to look at this answer.