CAEmitterLayer Stops Displaying

296 views Asked by At

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)
            }
        }
    }
}
1

There are 1 answers

0
Денис Попов On

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;