20.2 C
Canberra
Tuesday, October 21, 2025

CAEmitterLayer randomly does not emit particles


I am implementing confetti impact with particles. I used for it CAEmitterLayer, and it really works nearly good, however generally particles will not be emitted. I attempted my finest and debugged it (set colours to emitter layers) and I do know that the layers are added. Here is the complete code:

import UIKit

closing class ViewController: UIViewController {
    
    // MARK: - UI
    
    personal lazy var actionButton: UIButton = { [weak self] in
        let button = UIButton(sort: .system)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitle("Begin confetti", for: .regular)
        button.setTitleColor(.white, for: .regular)
        button.backgroundColor = .systemBlue
        button.layer.cornerRadius = 16
        button.addAction(UIAction { _ in
            self?.startConfettiShow()
        }, for: .touchUpInside)
        return button
    }()
    
    personal let confettiContainer: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .white
        view.clipsToBounds = true
        return view
    }()
    
    // MARK: - Lifecycle
    
    override func viewDidLoad() {
        tremendous.viewDidLoad()
        view.backgroundColor = .grey
        
        view.addSubview(confettiContainer)
        view.addSubview(actionButton)
        
        NSLayoutConstraint.activate([
            confettiContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            confettiContainer.bottomAnchor.constraint(equalTo: actionButton.topAnchor, constant: -50),
            confettiContainer.widthAnchor.constraint(equalToConstant: 300),
            confettiContainer.heightAnchor.constraint(equalToConstant: 300),
            
            actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            actionButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 200),
            actionButton.widthAnchor.constraint(equalToConstant: 150),
            actionButton.heightAnchor.constraint(equalToConstant: 40),
        ])
    }
    
    // MARK: - Confetti Logic
    
    personal func startConfettiShow() {
        // Take away previous emitters
        confettiContainer.layer.sublayers?.removeAll(the place: { $0 is CAEmitterLayer })
        view.layoutIfNeeded()
        
        let xOffset: CGFloat = 90
        let emissionOffset: CGFloat = 0.1
        
        let left = makeEmitter(xOffset: -xOffset, emissionLongitudeOffset: emissionOffset)
        let proper = makeEmitter(xOffset: xOffset, emissionLongitudeOffset: -emissionOffset)
        
        confettiContainer.layer.addSublayer(left)
        confettiContainer.layer.addSublayer(proper)
        
        debugEmitter(left, colour: .crimson)
        debugEmitter(proper, colour: .yellow)
        
        DispatchQueue.foremost.async {
            CATransaction.start()
            CATransaction.setDisableActions(true)
            
            let now = CACurrentMediaTime()
            left.beginTime = now
            proper.beginTime = now
            left.birthRate = 1
            proper.birthRate = 1
            
            CATransaction.commit()
        }
        
        // Cease emission
        DispatchQueue.foremost.asyncAfter(deadline: .now() + 1.0) {
            left.birthRate = 0
            proper.birthRate = 0
        }
        
        // Take away the layers 
        DispatchQueue.foremost.asyncAfter(deadline: .now() + 5.0) {
            left.removeFromSuperlayer()
            proper.removeFromSuperlayer()
        }
    }
    
    personal func makeEmitter(xOffset: CGFloat,
                             emissionLongitudeOffset: CGFloat) -> CAEmitterLayer {
        let emitter = CAEmitterLayer()
        emitter.body = confettiContainer.bounds
        emitter.emitterShape = .level
        emitter.emitterPosition = CGPoint(
            x: confettiContainer.bounds.midX + xOffset,
            y: confettiContainer.bounds.maxY - 10
        )
        emitter.birthRate = 0
        
        let cells: [CAEmitterCell] = ViewController.colours.flatMap { colour in
            ViewController.shapes.map { form in
                let cell = CAEmitterCell()
                cell.birthRate = 15
                cell.lifetime = 3.5
                cell.velocity = .random(in: 250...400)
                cell.velocityRange = 80
                cell.emissionLongitude = -.pi / 2 + emissionLongitudeOffset
                cell.emissionRange = .pi / 8
                cell.yAcceleration = 200
                cell.spin = .random(in: -3...3)
                cell.spinRange = 4
                cell.scale = .random(in: 0.08...0.14)
                cell.alphaSpeed = -0.4
                cell.colour = colour.cgColor
                cell.contents = form
                return cell
            }
        }
        
        emitter.emitterCells = cells
        return emitter
    }
    
    personal func debugEmitter(_ emitter: CAEmitterLayer, colour: UIColor) {
        emitter.borderColor = colour.cgColor
        emitter.borderWidth = 1
        emitter.backgroundColor = colour.withAlphaComponent(0.15).cgColor
        print("Emitter body:", emitter.body)
        print("Emitter place:", emitter.emitterPosition)
    }
}

// MARK: - Static assets

personal extension ViewController {
    static let colours: [UIColor] = [
        .systemPink, .systemYellow, .systemBlue, .systemGreen, .systemPurple
    ]
    
    static let shapes: [CGImage] = [
        CGImage.cgImageCircle(diameter: 36),
        CGImage.cgImageSquare(side: 32),
        CGImage.cgImageTriangle(size: 36),
    ].compactMap { $0 }
}

// MARK: - Form mills

extension CGImage {
    static func cgImageCircle(diameter: CGFloat) -> CGImage? {
        let measurement = CGSize(width: diameter, top: diameter)
        return UIGraphicsImageRenderer(measurement: measurement).picture { ctx in
            UIColor.white.setFill()
            ctx.cgContext.fillEllipse(in: CGRect(origin: .zero, measurement: measurement))
        }.cgImage
    }
    
    static func cgImageSquare(aspect: CGFloat) -> CGImage? {
        let measurement = CGSize(width: aspect, top: aspect)
        return UIGraphicsImageRenderer(measurement: measurement).picture { ctx in
            UIColor.white.setFill()
            ctx.cgContext.fill(CGRect(origin: .zero, measurement: measurement))
        }.cgImage
    }
    
    static func cgImageTriangle(measurement: CGFloat) -> CGImage? {
        let s = CGSize(width: measurement, top: measurement)
        return UIGraphicsImageRenderer(measurement: s).picture { ctx in
            UIColor.white.setFill()
            let path = UIBezierPath()
            path.transfer(to: CGPoint(x: s.width / 2, y: 0))
            path.addLine(to: CGPoint(x: s.width, y: s.top))
            path.addLine(to: CGPoint(x: 0, y: s.top))
            path.shut()
            path.fill()
        }.cgImage
    }
}

I am setting beginTime and birthRate after emitters being added to view’s layer, I even used CATransaction.start() and CATransaction.commit() (ChatGPT recommendation), nevertheless it did not labored. With all these enhancements it began working higher, I see much less of emitters not working, however nonetheless generally it does not work, so the aspect is unstable.

Verify the screenshot. There’re two emitters, however just one is emitting. Not often they do not emit each.

CAEmitterLayer randomly does not emit particles

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

[td_block_social_counter facebook="tagdiv" twitter="tagdivofficial" youtube="tagdiv" style="style8 td-social-boxed td-social-font-icons" tdc_css="eyJhbGwiOnsibWFyZ2luLWJvdHRvbSI6IjM4IiwiZGlzcGxheSI6IiJ9LCJwb3J0cmFpdCI6eyJtYXJnaW4tYm90dG9tIjoiMzAiLCJkaXNwbGF5IjoiIn0sInBvcnRyYWl0X21heF93aWR0aCI6MTAxOCwicG9ydHJhaXRfbWluX3dpZHRoIjo3Njh9" custom_title="Stay Connected" block_template_id="td_block_template_8" f_header_font_family="712" f_header_font_transform="uppercase" f_header_font_weight="500" f_header_font_size="17" border_color="#dd3333"]
- Advertisement -spot_img

Latest Articles