Created
October 10, 2025 16:48
-
-
Save irace/ab4bb8ded83fb13012f7c346ec996ac0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // RainingParticleView.swift | |
| // Provider | |
| // | |
| // Created by Bryan Irace on 1/26/17. | |
| // | |
| // | |
| import ServiceKit | |
| import UIKit | |
| import QuartzCore | |
| struct ParticleDescriptor { | |
| let image: UIImage | |
| let color: UIColor? | |
| } | |
| public enum ParticleType { | |
| case confetti | |
| case money | |
| func descriptors() -> [ParticleDescriptor] { | |
| switch self { | |
| case .confetti: | |
| return [ | |
| .confettiPurple, | |
| .confettiRed, | |
| .confettiBlue, | |
| .confettiOrange, | |
| ].map({ colorName in | |
| ParticleDescriptor(image: UIImage(asset: .confetti), color: UIColor(named: colorName)) | |
| }) | |
| case .money: | |
| return [ParticleDescriptor(image: UIImage(asset: .cash), color: nil)] | |
| } | |
| } | |
| } | |
| /// A view that makes it look like money is raining down from the sky. | |
| final class RainingParticleView: UIView { | |
| private enum Constants { | |
| static let emitterHeight: CGFloat = 1 | |
| static let birthRate: Float = 120 | |
| static let lifetime: Float = 10.5 | |
| static let velocity: CGFloat = 262.5 | |
| static let velocityRange: CGFloat = 60 | |
| static let emissionLongitude = CGFloat(Double.pi) | |
| static let emissionRange = CGFloat(Double.pi/4) | |
| static let spin: CGFloat = 2.625 | |
| static let spinRange: CGFloat = 3 | |
| static let yAcceleration: CGFloat = 200 | |
| } | |
| // MARK: - Mutable state | |
| private var rain = PerformOnce() | |
| // MARK: - Sublayers | |
| private let emitter: CAEmitterLayer | |
| init(frame: CGRect, type: ParticleType) { | |
| emitter = CAEmitterLayer().then { | |
| $0.beginTime = CACurrentMediaTime() | |
| $0.emitterPosition = CGPoint(x: frame.midX, y: 0) | |
| $0.emitterSize = CGSize(width: frame.size.width, height: Constants.emitterHeight) | |
| $0.emitterShape = kCAEmitterLayerLine | |
| $0.emitterCells = RainingParticleView.cells(from: type.descriptors()) | |
| } | |
| super.init(frame: frame) | |
| } | |
| required init?(coder aDecoder: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| // MARK: - Private | |
| private static func cells(from particleDescriptors: [ParticleDescriptor]) -> [CAEmitterCell] { | |
| return particleDescriptors.map({ descriptor in | |
| CAEmitterCell().then { | |
| $0.contents = descriptor.image.cgImage | |
| $0.birthRate = Constants.birthRate | |
| $0.lifetime = Constants.lifetime | |
| $0.velocity = Constants.velocity | |
| $0.velocityRange = Constants.velocityRange | |
| $0.emissionLongitude = Constants.emissionLongitude | |
| $0.emissionRange = Constants.emissionRange | |
| $0.spin = Constants.spin | |
| $0.spinRange = Constants.spinRange | |
| $0.yAcceleration = Constants.yAcceleration | |
| if let color = descriptor.color { | |
| $0.color = color.cgColor | |
| } | |
| } | |
| }) | |
| } | |
| // MARK: - Public | |
| /// Start raining down money. Won’t do anything if you call it more than once. | |
| func start() { | |
| rain.performOnce({ | |
| layer.addSublayer(emitter) | |
| }) | |
| } | |
| /// Stop producing new money. | |
| func stop() { | |
| emitter.birthRate = 0 | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment