iossprite-kitswiftuiskemitternodeuiviewrepresentable

How can I get SKEmitterNode to work in SwiftUI?


I'm trying to update the colour of an SKEmitterNode within a UIViewRepresentable. The hue value is passed in from the state on the parent View, and the emitter colour should update when the hue value in the parent state updates.

It initially displays, and although it updates on the first call to updateUIView it does not respond to any subsequent calls, even though the function definitely gets called with the new value of hue each time.

Has anyone any idea why the emitter won't update? I'm at the hair-tearing-out stage...

  import SwiftUI
  import UIKit
  import SpriteKit

  struct EmitterView: UIViewRepresentable {
    private let view = SKView()

    let scene = SKScene(fileNamed: "myScene")!
    var emitter: SKEmitterNode = SKEmitterNode()
    var hue: Double

    func makeUIView(context: UIViewRepresentableContext<EmitterView>) -> SKView {

      // Lets make it manually
      emitter.particleTexture = SKTexture(imageNamed: "spark")
      emitter.particleBirthRate = 80
      emitter.particleLifetime = 2.5
      emitter.particlePositionRange = CGVector(dx: 200, dy: 150)
      emitter.particleScale = 0.2
      emitter.particleScaleSpeed = 0.45
      emitter.particleColor = SKColor.blue
      emitter.particleColorBlendFactor = 1.0

      scene.addChild(emitter)
      view.presentScene(scene)

      return view
    }

    func updateUIView(_ uiView: SKView, context: UIViewRepresentableContext<EmitterView>) {

      let color: SKColor = SKColor(
        hue: CGFloat(hue),
        saturation: 1.0,
        brightness: 1.0,
        alpha: 1.0
      )

      print("Hue is now", hue)

      emitter.resetSimulation()
      emitter.particleColor = color
    }
  }

  struct EmitterView_Previews: PreviewProvider {
    static var previews: some View {
      EmitterView(hue: 0.5)
    }
  }

Solution

  • Now that 11 is out I can finally play with Swift UI.

    The problem being experienced is that your UIViewRepresentable is a struct.

    The way structs work is they are copy on write.

    I am going to assume that in your scene delegate, you are not reassigning the struct in your root controller when you set the hue.

    You need to move your code into class for best results (Not the coordinator, they are used for application flow. Research Coordinator Pattern for more info.)

    The best class would be your SKScene.

    I recommend making a GameScene class and changing your sks file to point to it.

    You can then create a function to this game scene class that will allow you to alter the emitter without altering the struct.

    import SwiftUI
    import UIKit
    import SpriteKit
    
    struct EmitterView: UIViewRepresentable {
        private let view = SKView()
        let scene = GameScene(fileNamed: "myScene")!
        var hue : Double{
            get{
                return scene.hue
            }
            set{
                scene.hue = newValue
            }
        }
        func makeUIView(context: UIViewRepresentableContext<EmitterView>) -> SKView {
            view.presentScene(scene)
            return view
        }
    
        func updateUIView(_ uiView: SKView, context: UIViewRepresentableContext<EmitterView>) {
    
    
        }
    }
    
    struct EmitterView_Previews: PreviewProvider {
        static var previews: some View{
              EmitterView()
        }
    }
    
    
    class GameScene : SKScene{
        var hue : Double = 0{
            didSet{
                let color: SKColor = SKColor(
                    hue: CGFloat(hue),
                    saturation: 1.0,
                    brightness: 1.0,
                    alpha: 1.0
                )
    
                print("Hue is now", hue)
    
                emitter.resetSimulation()
                emitter.particleColor = color
            }
        }
    
        var emitter: SKEmitterNode = SKEmitterNode()
        override func didMove(to view: SKView) {
            emitter.particleTexture = SKTexture(imageNamed: "spark")
            emitter.particleBirthRate = 80
            emitter.particleLifetime = 2.5
            emitter.particlePositionRange = CGVector(dx: 200, dy: 150)
            emitter.particleScale = 0.2
            emitter.particleScaleSpeed = 0.45
            emitter.particleColor = SKColor.blue
            emitter.particleColorBlendFactor = 1.0
            addChild(emitter)
        }
    
    }
    

    The reason why this works is because on copy, the view will copy the pointer, where as hue will copy the value.

    If you need to do things in the updateUIView, you can always cast uiView.scene to GameScene and access it that way.