0

I adapted this code for SwiftUI to display confetti particles, but sometimes the particle emitter does not work. I've noticed that this often happens after sending to background (not killing the app entirely) and reopening it, or simply letting the app sit for a while then trying again.

I've tried using beginTime as other answers have mentioned (on both the emitter and cells), but that fully breaks things. I've also tried toggling various other emitter properties (birthRate, isHidden). It might have to do with the fact that I'm adapting this with UIViewRepresentable. It seems like the emitter layer just disappears, even though the debug console says its still visible.

class ConfettiParticleView: UIView {
    var emitter: CAEmitterLayer!
    public var colors: [UIColor]!
    public var intensity: Float!
    private var active: Bool!

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    func setup() {
        colors = [UIColor(Color.red),
                  UIColor(Color.blue),
                  UIColor(Color.orange),
                ]
        intensity = 0.7
        active = false
        emitter = CAEmitterLayer()

        emitter.emitterPosition = CGPoint(x: UIScreen.main.bounds.width / 2.0, y: 0) // emit from top of view
        emitter.emitterShape = .line
        emitter.emitterSize = CGSize(width: UIScreen.main.bounds.width, height: 100) // line spans the whole top of view
//        emitter.beginTime = CACurrentMediaTime()
        var cells = [CAEmitterCell]()
        for color in colors {
            cells.append(confettiWithColor(color: color))
        }

        emitter.emitterCells = cells
        emitter.allowsGroupOpacity = false
        self.layer.addSublayer(emitter)
    }

    func startConfetti() {
        emitter.lifetime = 1
        // i've tried toggling other properties here like birthRate, speed
        active = true
    }

    func stopConfetti() {
        emitter.lifetime = 0
        active = false
    }

    func confettiWithColor(color: UIColor) -> CAEmitterCell {
        let confetti = CAEmitterCell()
        confetti.birthRate = 32.0 * intensity
        confetti.lifetime = 15.0 * intensity
        confetti.lifetimeRange = 0
        confetti.name = "confetti"
        confetti.color = color.cgColor
        confetti.velocity = CGFloat(450.0 * intensity) // orig 450
        confetti.velocityRange = CGFloat(80.0 * intensity)
        confetti.emissionLongitude = .pi
        confetti.emissionRange = .pi / 4
        confetti.spin = CGFloat(3.5 * intensity)
        confetti.spinRange = 300 * (.pi / 180.0)
        confetti.scaleRange = CGFloat(intensity)
        confetti.scaleSpeed = CGFloat(-0.1 * intensity)
        confetti.contents = #imageLiteral(resourceName: "confetti").cgImage
        confetti.beginTime = CACurrentMediaTime()
        return confetti
    }

    func isActive() -> Bool {
        return self.active
    }
}

view representable

struct ConfettiView: UIViewRepresentable {
    @Binding var isStarted: Bool
    
    func makeUIView(context: Context) -> ConfettiParticleView {
        return ConfettiParticleView()
    }
    
    func updateUIView(_ uiView: ConfettiParticleView, context: Context) {
        if isStarted && !uiView.isActive() {
            uiView.startConfetti()
            print("confetti started")
        } else if !isStarted {
            uiView.stopConfetti()
            print("confetti stopped")
        }
    }
}

swiftui view for testing

struct ConfettiViewTest: View {
    @State var isStarted = false
    
    var body: some View {
        ZStack {
            ConfettiView(isStarted: $isStarted)
                .ignoresSafeArea()
            
            Button(action: {
                isStarted = true
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    isStarted = false
                }
            }) {
                Text("toggle")
                    .padding()
                    .background(Color.white)
            }
        }
    }
}
bze12
  • 727
  • 8
  • 20
  • Are you seeing the console "confetti started" and "confetti stopped" at the times you expect, even when the confetti itself fails to appear? – matt Apr 14 '21 at 04:29
  • Does this answer your question https://stackoverflow.com/a/61711171/12299030? – Asperi Apr 14 '21 at 04:46
  • @matt yes I see them show up in the console – bze12 Apr 15 '21 at 18:16
  • @Asperi is there something specific within that answer that might help me? I see it's setting up an emitter with uiviewrepresentable like I've done, but my issue is with toggling the emitter on and off. – bze12 Apr 15 '21 at 23:38
  • @bze12 did you manage to fix this? I am in a similar situation, my `CAEmitterLayer` in a `UIViewRepresentable` is not respecting the birthrate – Mattia C. Nov 22 '22 at 21:56
  • 1
    @MattiaC. no i never really figured it out sorry – bze12 Nov 27 '22 at 20:06

1 Answers1

0

I'm having the same issue.

I found solution after some research:

  • Don't reuse CAEmitterLayer (Create new layer when you start animation);
  • Don't change CAEmitterLayer.beginTime. Change CAEmitterCell.beginTime instead;