CAEmitterLayer not timing correctly with CACurrentMediaTime() and sometimes not showing at all

919 views Asked by At

I am currently making a particle emitter using CAEmitterLayer and ran into the issue of the layer preloading the animation when I start it and hence the particles all over the place when I show it.

Many answers have said the culprit is CAEmitterLayer being preloaded and we simply have to set its beginTime to CACurrentMediaTime() on the emitter.

See:

CAEmitterLayer emits random unwanted particles on touch events

Initial particles from CAEmitterLayer don't start at emitterPosition

iOS 7 CAEmitterLayer spawning particles inappropriately

For me this solution has not worked, when running it on device, iPad Air running iOS 12.1, the emitter often does not show and sometimes it is showed with a great delay.

To illustrate this issue I made a project on github: https://github.com/roodoodey/CAEmitterLayer/tree/master/CAEmitterLayerApp

Here is the main code, I have 7 different images for particles chosen at random and a button to show the emitter when pressed.

import UIKit

class ViewController: UIViewController {

    var particleImages = [UIImage]()
    var emitter: CAEmitterLayer?

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        view.backgroundColor = UIColor.black

        // Populate the random images array for the particles
        for index in 1..<8 {
            if let image = UIImage(named: "StarParticle00\(index)") {
                particleImages.append(image)
            }

        }

        // Button pressed to make the emitter emit the particles
        let button = UIButton(frame: CGRect(x: view.frame.width * 0.5 - 60, y: view.frame.height * 0.5 - 40, width: 120, height: 80))
        button.setTitle("Emit!", for: .normal)
        button.setTitleColor(UIColor.white, for: .normal)
        button.backgroundColor = UIColor.blue
        button.addTarget(self, action: #selector(changeButton(sender:)), for: .touchDown)
        button.addTarget(self, action: #selector(addEmitter(sender:)), for: .touchUpInside)
        view.addSubview(button)

    }

    @objc func changeButton(sender: UIButton) {
        sender.alpha = 0.5
    }

    @objc func addEmitter(sender: UIButton) {

        sender.alpha = 1.0

        // IF an emitter already exists remove it.
        if emitter?.superlayer != nil {
            emitter?.removeFromSuperlayer()
        }

        emitter = CAEmitterLayer()
        emitter?.emitterShape = CAEmitterLayerEmitterShape.point
        emitter?.position = CGPoint(x: self.view.frame.width * 0.5, y: self.view.frame.height * 0.5)
        // So that the emitter starts now, and is not preloaded. 
        emitter?.beginTime = CACurrentMediaTime()

        var cells = [CAEmitterCell]()
        for _ in 0..<40 {
            let cell = CAEmitterCell()
            cell.birthRate = 1
            cell.lifetime = 3
            cell.lifetimeRange = 0.5
            cell.velocity = 500
            cell.velocityRange = 100
            cell.emissionRange = 2 * CGFloat(Double.pi)
            cell.contents = getRandomImage().cgImage
            cell.scale = 1
            cell.scaleRange = 0.5
            cells.append(cell)
        }

        emitter?.emitterCells = cells

        view.layer.addSublayer( emitter! )

    }

    func getRandomImage() -> UIImage {

        let upperBound = UInt32(particleImages.count)
        let randomIndex = Int(arc4random_uniform( upperBound ))

        return particleImages[randomIndex]
    }


}

Here is a short 20 second video of the app running on device, iPad Air running iOS 12.1, not run through xcode. https://www.dropbox.com/s/f9uol3yot67drm8/ScreenRecording_11-25-2018%2013-19-29.MP4?dl=0

If somebody could see if they can reproduce this issue or shed some light on this strange behavior it would be greatly appreciated.

1

There are 1 answers

1
agibson007 On BEST ANSWER

I have a ton of experience with Core Animation although I have to admit not a lot with CAEmitterLayer. Everything looks right and for a person that knows CALayer pretty well, setting the beginTime with CACurrentMediaTime() makes sense. However, I ran your project and saw it was not working. For me setting the beginTime on the cell had the effect I would expect.
Meaning

//delay for 5.0 seconds
cell.beginTime = CACurrentMediaTime() + 5.0
cell.beginTime = CACurrentMediaTime() //immediate
cell.beginTime = CACurrentMediaTime() - 5.0 //5 seconds ago

Entire File

import UIKit

class ViewController: UIViewController {

    var particleImages = [UIImage]()
    var emitter: CAEmitterLayer?

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        view.backgroundColor = UIColor.black

        // Populate the random images array for the particles
        for index in 1..<8 {
            if let image = UIImage(named: "StarParticle00\(index)") {
                particleImages.append(image)
            }

        }

        // Button pressed to make the emitter emit the particles
        let button = UIButton(frame: CGRect(x: view.frame.width * 0.5 - 60, y: view.frame.height * 0.5 - 40, width: 120, height: 80))
        button.setTitle("Emit!", for: .normal)
        button.setTitleColor(UIColor.white, for: .normal)
        button.backgroundColor = UIColor.blue
        button.addTarget(self, action: #selector(changeButton(sender:)), for: .touchDown)
        button.addTarget(self, action: #selector(addEmitter(sender:)), for: .touchUpInside)
        view.addSubview(button)

    }

    @objc func changeButton(sender: UIButton) {
        sender.alpha = 0.5
    }

    @objc func addEmitter(sender: UIButton) {

        sender.alpha = 1.0

        // IF an emitter already exists remove it.
        if emitter?.superlayer != nil {
            emitter?.removeFromSuperlayer()
        }

        emitter = CAEmitterLayer()
        emitter?.emitterShape = CAEmitterLayerEmitterShape.point
        emitter?.position = CGPoint(x: self.view.frame.width * 0.5, y: self.view.frame.height * 0.5)
        // So that the emitter starts now, and is not preloaded.
        var cells = [CAEmitterCell]()
        for _ in 0..<40 {
            let cell = CAEmitterCell()
            cell.birthRate = 1
            cell.lifetime = 3
            cell.lifetimeRange = 0.5
            cell.velocity = 500
            cell.velocityRange = 100
            cell.emissionRange = 2 * CGFloat(Double.pi)
            cell.contents = getRandomImage().cgImage
            cell.scale = 1
            cell.scaleRange = 0.5
            cell.beginTime = CACurrentMediaTime()
            cells.append(cell)
        }

        emitter?.emitterCells = cells
        view.layer.addSublayer( emitter! )
    }

    func getRandomImage() -> UIImage {

        let upperBound = UInt32(particleImages.count)
        let randomIndex = Int(arc4random_uniform( upperBound ))

        return particleImages[randomIndex]
    }


}