iosswiftcaemitterlayercaemittercell

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


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.


Solution

  • 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]
        }
    
    
    }