How can I get SKEmitterNode to work in SwiftUI?
Asked Answered
K

2

2

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)
    }
  }
Kaseykasha answered 15/9, 2019 at 22:11 Comment(0)
W
1

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.

Witted answered 22/9, 2019 at 3:46 Comment(2)
Thanks for taking the time out to look at this. All makes sense and is a clean solution.Kaseykasha
@raffjones no problem, I should have spotted it sooner with seeing struct, but it slipped my mind. Once I got to playing with it and having the compiler immediately yell at me about contentView not being mutable, it raised the red flag on why it was failing.Witted
J
0

In order to make the UIViewRepresentable struct mutable, add the @Binding prefix to the hue variable.

@Binding var hue: Double

You can then change the color of the particles from a parent View in the following manner:

struct ContentView: View {
    @State var hue: Double = 0.5
    var body: some View {
        VStack {
            EmitterView(hue: self.$hue)
            Slider(value: $hue)
        }
    }
}
Jasun answered 4/5, 2020 at 11:5 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.