Rahul Iyer
Rahul Iyer

Reputation: 21025

How to access SCNSceneRendererDelegate methods when using SceneKit with SwiftUI?

I am using SceneKit with SwiftUI by following solution provided by Mehdi to this question:

SwiftUI - how to add a Scenekit Scene

Normally, when one creates a SceneKit project, implementing the renderer methods is as easy as just adding the the following extension in the GameViewController file and implementing each of the renderer methods:

extension GameViewController: SCNSceneRendererDelegate {
  // 2
  func renderer(renderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
    // 3
    doWhatever()
  }
}

But when using SwiftUI, we use a struct instead of a class (see above linked question), so we cannot simply add the extension, because Xcode complains:

Non-class type 'ScenekitView" cannot conform to class protocol 'NSObjectProtocol'
Non-class type 'ScenekitView' cannot conform to class protocol 'SCNSceneRendererDelegate'

What is the solution to this ?

Upvotes: 2

Views: 1287

Answers (2)

Damik Minnegalimov
Damik Minnegalimov

Reputation: 61

If you are brave and don't want to import anything but SwiftUI:

  1. Create helper class SceneRendererDelegate and implement anything you want:

    import SceneKit
    
    // the hackiest hack ever. And the stupidest one!
    // For handling touches in SceneView we need SCNSceneRenderer,
    // but SwiftUI's SceneView does not provide it.
    class SceneRendererDelegate: NSObject, SCNSceneRendererDelegate {
      var renderer: SCNSceneRenderer?
      var onEachFrame: (() -> ())? = nil
    
      func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        if self.renderer == nil {
          self.renderer = renderer
          let type = type(of: renderer)
    
          print("We got SceneRenderer: \(type)")
        }
    
        onEachFrame?()
      }
    }
    
  2. Add it to your GameView:

    import SwiftUI
    import SceneKit
    
    struct GameView: View {
     var sceneRendererDelegate = SceneRendererDelegate()
    
     var body: some View {
       ZStack {
         SceneView(
           scene: yourScene,
           options: [
             .temporalAntialiasingEnabled
           ],
           delegate: sceneRendererDelegate)
       }
       .onDisappear {
         // Make sure your code disabled in background
         sceneRendererDelegate.onEachFrame = nil
       }
       .onAppear {
         sceneRendererDelegate.onEachFrame = { 
           // Your code on every frame
         }
       }
     }
    }
    

You can check and run live demos from my old SwiftUI-Games

Upvotes: 2

Rahul Iyer
Rahul Iyer

Reputation: 21025

Found the solution in this answer:

SwiftUI – Passing data from SwiftUIView to SceneKit

At the lower half of Andy's question he describes how to use a coordinator to implement the delegate methods. Reproducing here for convenience:

struct ScenekitView: NSViewRepresentable {

    @Binding var showStats: Bool
    let sceneView = SCNView(frame: .zero)
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    final class Coordinator: NSObject, SCNSceneRendererDelegate {
        var control: ScenekitView

        init(_ control: ScenekitView) {
            self.control = control
        }

        func renderer(_ renderer: SCNSceneRenderer,
               updateAtTime time: TimeInterval) {

            control.sceneView.showsStatistics = control.showStats

            for i in 0...255 {
                control.sceneView.backgroundColor = NSColor(
                                  red: CGFloat(arc4random_uniform(UInt32(i))),
                                green: CGFloat(arc4random_uniform(UInt32(i))),
                                 blue: CGFloat(arc4random_uniform(UInt32(i))),
                                alpha: 1.0)
            }
        }
    }

    func scnScene(stat: Bool, context: Context) -> SCNView {
        sceneView.scene = scene
        sceneView.showsStatistics = stat
        sceneView.delegate = context.coordinator
        return sceneView
    }

    func makeNSView(context: Context) -> SCNView {
        scnScene(stat: true, context: context)
    }

    func updateNSView(_ uiView: SCNView, context: Context) { }
}

Upvotes: 5

Related Questions