I'm working on adding 4 CAEmitterLayers
to a subview. Each CAEmitterLayer
is a line, and will be positioned on each side. Rotation will be controlled via CGAffineTransform
My problem:
I can't get the .Left and .Right emitterPosition to properly line up with the left and right side of the view
Here's my progress:
override func awakeFromNib() {
super.awakeFromNib()
self.layer.cornerRadius = GlobalConstants.smallCornerRadius
createParticles(direction: .Up)
createParticles(direction: .Down)
createParticles(direction: .Left)
createParticles(direction: .Right)
}
enum EmitTo {
case Up
case Down
case Left
case Right
}
func createParticles(direction: EmitTo) {
self.layoutSubviews()
let particleEmitter = CAEmitterLayer()
particleEmitter.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
particleEmitter.bounds = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
if direction == .Up {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: 0))
} else if (direction == .Down) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: .pi))
} else if (direction == .Left) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: .pi / 2))
} else if (direction == .Right) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: -.pi / 2))
}
if direction == .Up || direction == .Down {
particleEmitter.emitterPosition = CGPoint(x: self.frame.width / 2, y: 0)
}
if direction == .Left {
particleEmitter.emitterPosition = CGPoint(x: self.frame.height / 2, y: self.frame.height)
}
if direction == .Right {
particleEmitter.emitterPosition = CGPoint(x: self.frame.height / 2, y: -50)
}
particleEmitter.emitterSize = self.bounds.size
particleEmitter.emitterShape = CAEmitterLayerEmitterShape.line
particleEmitter.zPosition = 1
let triangle = makeEmitterCell(direction: direction)
particleEmitter.emitterCells = [triangle]
self.layer.addSublayer(particleEmitter)
}
func makeEmitterCell(direction: EmitTo) -> CAEmitterCell {
let cell = CAEmitterCell()
cell.birthRate = 25
cell.lifetime = 3
cell.lifetimeRange = 0
cell.velocity = 10
cell.velocityRange = 5
cell.emissionLongitude = .pi
cell.spin = 2
cell.spinRange = 3
cell.scale = 0.4
cell.scaleRange = 0.6
cell.scaleSpeed = -0.10
cell.contents = UIImage(named: "greenTriangle")?.cgImage
return cell
}
What's happening:
My question
How can I get the .Left
and .Right
CAEmitterLayer
to properly align with the left side and right side of the gray view?
Green Triangle Image:
Your approach to try to use CGAffineTransform
is promising because a line emitter is a horizontal line that has only a width dimension. By rotating it, you can have it emit from the left or right.
However you need to use emitter bounds with the view height as width and the view width as height.
particleEmitter.bounds = CGRect(x: 0, y: 0, width: bounds.size.height, height: bounds.size.width)
Accordingly, one sets the width of the emitterSize to the height of the view:
particleEmitter.emitterSize = CGSize(width: bounds.size.height, height: 0)
The emitter position must be adjusted in the same way.
Why is this so? Take a look at the following illustration with an emission from the top:
If you rotate the layer by 90 degrees you would only cover a small area in the middle. Also the emissions would not be visible, because they would be to much outside to the left.
The difference between having the emitter on the left or the right is just a call to setAffineTransform
with either -.pi / 2
or .pi / 2
.
The emitters from top and bottom of course should get the normal bounds, they only need to be rotated as you do in your question.
Hints
In your createParticles
method you call self.layoutSubviews()
. That should be avoided.
Apple docs explicitly state:
You should not call this method directly.
see https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews.
Instead you could override layoutSubviews
and call your createParticles
methods from there. Make sure to remove any previously created emitter layers by removing them.
One small thing: Swift switch cases start with a lowercase letter by convention.
Code
A complete example to help you get started might then look something like this:
import UIKit
class EmitterView: UIView {
private var emitters = [CAEmitterLayer]()
enum EmitTo {
case up
case down
case left
case right
}
override func layoutSubviews() {
super.layoutSubviews()
emitters.forEach { $0.removeFromSuperlayer() }
emitters.removeAll()
createParticles(direction: .up)
createParticles(direction: .down)
createParticles(direction: .left)
createParticles(direction: .right)
}
func createParticles(direction: EmitTo) {
let particleEmitter = CAEmitterLayer()
emitters.append(particleEmitter)
particleEmitter.position = CGPoint(x: bounds.midX, y: bounds.midY)
particleEmitter.emitterShape = .line
switch direction {
case .up, .down:
particleEmitter.bounds = self.bounds
particleEmitter.emitterPosition = CGPoint(x: bounds.size.width / 2, y: 0)
particleEmitter.emitterSize = CGSize(width: bounds.size.width, height: 0)
if direction == .up {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: -.pi))
}
case .left, .right:
particleEmitter.bounds = CGRect(x: 0, y: 0, width: bounds.size.height, height: bounds.size.width)
particleEmitter.emitterPosition = CGPoint(x: bounds.height / 2, y: 0)
particleEmitter.emitterSize = CGSize(width: bounds.size.height, height: 0)
let rotationAngle: CGFloat = direction == EmitTo.left ? -.pi / 2 : .pi / 2
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: rotationAngle))
}
particleEmitter.emitterCells = [makeEmitterCell()]
layer.addSublayer( particleEmitter)
}
func makeEmitterCell() -> CAEmitterCell {
let cell = CAEmitterCell()
cell.birthRate = 25
cell.lifetime = 3
cell.lifetimeRange = 0
cell.velocity = 10
cell.velocityRange = 5
cell.emissionLongitude = .pi
cell.spin = 2
cell.spinRange = 3
cell.scale = 0.4
cell.scaleRange = 0.6
cell.scaleSpeed = -0.10
cell.contents = UIImage(named: "greenTriangle")?.cgImage
return cell
}
}
Test