CAEmitterBehavior
CAEmitterBehavior is a (mostly?) undocumented class that dramatically increases the power of CAEmitterLayer. It allows for the addition of unique particle behaviors that make for some interesting effects individually — but when combined with other behaviors and carefully-picked animations, can drive amazing effects (like iMessage’s fantastic confetti implementation).
Because it’s an undocumented class, you should not be using it in a production app, and it probably doesn’t deserve that much attention overall.
But because it allows for such powerful effects — and offers an excessive amount of options to help make that happen — let’s dive into it anyways. This post will cover each of the individual behaviors; in the next post we will look at combining multiple behaviors together to replicate iMessages’ confetti for ourselves.
Mostly undocumented?
Let’s get that note out of the way first — if you search for CAEmitterBehavior on Apple’s Developer site, you’ll get no results. Likewise, you will not find any information about CAEmitterBehavior when searching within Xcode’s included Developer Documentation.
Unless you are using Xcode 8.x — then documentation is readily available!
Unfortunately, the documentation is extremely unpolished, and the API makes it clear that this was never really intended for general use; so unless things change in the future, CAEmitterBehavior should be treated as an undocumented, private class for the time being.
CAEmitterBehavior API
Before diving into each of the individual effects shown below, let’s first look at how one uses CAEmitterBehavior in general, per the examples provided in older versions of Xcode:
// Set up behavior
let behavior = CAEmitterBehavior(type: /* ... */)
behavior.setValue(/* ... */, forKey: /* ... */)
behavior.setValue(/* ... */, forKey: /* ... */)
// Add it to an emitter layer
emitterLayer.setValue([behavior], forKeyPath: "emitterBehaviors")
Calling it an API might be a bit of a stretch, with key-value coding being the driving force behind behavior configuration.
Besides the setValue(_:forKey:)
calls, there are only a few other methods to be aware of — namely the behaviorTypes()
class method, which returns an array containing all available behavior names as strings; and the CAEmitterBehavior(type:)
initializer,
which takes in one of these names to create a behavior.
Though the documentation doesn’t call it out, you can also set behaviors directly on CAEmitterCells using the same "emitterBehaviors"
key, which can be quite helpful
when working with emitter layers containing multiple cell types.
Behavior Types
There are eight different types of emitter behaviors available for use:
Later versions of iOS also include Color Over Distance
and Value Over Distance
behaviors;
but these appear to either not be implemented fully, or require some additional
unknown parameters, so we will skip them here accordingly.
Let’s start by going over each type individually, including their available properties and some examples of the functionality they each offer.
A Note on Documentation
Unlike the rest of the content in this post, the tables of available keys are quoted as-is from the documentation available in Xcode 8.x; and given the state of the documentation before it was removed, key descriptions may be less-than-perfect in some instances — either being too short, or missing supplementary information like default values and animatable keypaths, or even omitting available keys entirely. I’ve elaborated on several of these areas in the surrounding text, but some descriptions will likely still fall short.
Following Along Locally
If you’d like to try out the examples localy, consider adding a dumped CAEmitterBehavior header to your project.
If you prefer to stay entirely within Swift, you can do so using a terrible combination of string-based lookups and unsafe bitcasting, which does allow for replicating these examples within Playgrounds!
func createBehavior(type: String) -> NSObject {
let behaviorClass = NSClassFromString("CAEmitterBehavior") as! NSObject.Type
let behaviorWithType = behaviorClass.method(for: NSSelectorFromString("behaviorWithType:"))!
let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self)
return castedBehaviorWithType(behaviorClass, NSSelectorFromString("behaviorWithType:"), type)
}
Wave
The Wave
behavior provides a great starting point, since it creates such a distinctive effect that would not otherwise be possible with CAEmitterLayer. It allows for some fantastic displays, from rising bubbles to fluttering confetti, by applying a given force in a wave-like pattern.
Key | Type | Description |
---|---|---|
force | 3 component vector - pass as array of CGFloat | Force vector [X Y Z] |
frequency | CGFloat | Oscillation frequency. |
Here’s a basic example that combines two waves together at different frequencies:
let horizontalWaveBehavior = CAEmitterBehavior(type: kCAEmitterBehaviorWave)
horizontalWaveBehavior.setValue([100, 0, 0], forKeyPath: "force")
horizontalWaveBehavior.setValue(1, forKeyPath: "frequency")
let verticalWaveBehavior = CAEmitterBehavior(type: kCAEmitterBehaviorWave)
verticalWaveBehavior.setValue([0, 150, 0], forKeyPath: "force")
verticalWaveBehavior.setValue(1, forKeyPath: "frequency")
Drag
The Drag
emitter behavior is among the simplest, featuring only a single key for configuration.
It allows for slowing down particles based on their current speed, making for easy
simulations of air resistance and friction in general.
Key | Type | Description |
---|---|---|
drag | CGFloat | Slowing amount, the force on the particle is calculated as V * -drag where V is the particle’s current velocity. |
Below is an example of a Drag
behavior slowing cells from an initial velocity down to a standstill.
let dragBehavior = CAEmitterBehavior(type: kCAEmitterBehaviorDrag)
dragBehavior.setValue(1.2, forKey: "drag")
Align To Motion
Align To Motion
is another behavior that provides useful results that would be hard to replicate
otherwise, and does so with an extremely simple configuration.
It rotates particles so that they face in the direction of movement.
Key | Type | Description |
---|---|---|
rotation | CGFloat | Angle in rotations to apply in addition to orientation of the velocity vector. |
preservesDepth | Bool | If true will operate in 3D, i.e. by picking an axis orthogonal to the required rotation. If false (the default) operates in 2D by rotating about the z axis. Only plane type particles support a 3D orientation. |
The following demo shows off the most basic version of this behavior, with no extra configuration made. We’ll change the cell style and add a yAccelleration
value to better show the effect of Align To Motion.
let alignToMotionBehavior = CAEmitterBehavior(type: kCAEmitterBehaviorAlignToMotion)
Value Over Life
The Value Over Life
behavior allows for modifying a given value over the lifetime of a cell.
Per the documentation, the supported keyPath
values for this behavior include:
position.x
,position.y
, andposition.z
velocity.x
,velocity.y
, andvelocity.z
mass
,rotation
, andspin
scale
andscaleSpeed
color.red
,color.green
,color.blue
, andcolor.alpha
Key | Type | Description |
---|---|---|
keyPath | String | Name of the property to change. |
values | Array of NSNumber | Array of numeric values. |
locations | Array of NSNumber | Optional array of stop-locations. |
Some basic Value Over Life
constructions can be replicated by relying only on public CAEmitterCell properties — for example,
a very similar demo to the following one can be created using scaleSpeed
to scale a cell
up from 0
to 1
over a fixed lifetime.
That being said, there are plenty of scenarios where Value Over Life
becomes helpful — such as when you need a non-linear change in a value,
or when working with a property that doesn’t have an accompanying “speed” attribute, or when using the lifetimeRange
property
to have a varying lifetime for your cells, as is the case here:
let valueOverLifeBehavior = CAEmitterBehavior(type: kCAEmitterBehaviorValueOverLife)
valueOverLifeBehavior.setValue("scale", forKeyPath: "keyPath")
valueOverLifeBehavior.setValue([0, 1], forKeyPath: "values")
Color Over Life
Color Over Life
acts in the exact same manner as Value Over Life
— in fact, since Value Over Life
already supports color
keyPaths,
it’s already possible to create everything using that behavior that you could with this one. However, when working with whole colors
instead of individual color components, having this separate behavior can make things much nicer.
Key | Type | Description |
---|---|---|
colors | Array of CGColor | Array of gradient color stops. |
locations | Array of NSNumber | Optional array of stop-locations. |
As a quick example, let’s take our standard demo emitter and give the cells some color. Note the use of UIColor.clear
at the end as a convenient way to fade
out the cells at the end of their lifetime:
let colorOverLifeBehavior = CAEmitterBehavior(type: kCAEmitterBehaviorColorOverLife)
colorOverLifeBehavior.setValue([
UIColor.red, UIColor.orange, UIColor.yellow,
UIColor.green, UIColor.blue, UIColor.purple,
UIColor.clear].map { $0.cgColor }, forKeyPath: "colors")
Light
The Light
behavior does not appear to have any uses within iOS currently (at least, none that I can find), but its extensive set of options makes it a particularly powerful behavior regardless.
Key | Type | Description |
---|---|---|
color | CGColor | The light’s color, alpha is ignored. |
position | CGPoint | The light’s 2D position. |
zPosition | CGFloat | The light’s position on z axis. |
falloff | NSNumber | Falloff values. |
spot | Bool | If true , light is a spot-light. |
appliesAlpha | Bool | If true, lit object’s alpha is also affected. |
directionLatitude | NSNumber | The light’s direction (spot-lights only). |
coneAngle | NSNumber | The spot-light’s lighting cone. |
As lacking as the documentation can be for some behaviors, the Light
behavior seems to have gotten hit particularly hard —
default values are not called out (spot
seems to default to false
, appliesAlpha
defaults to true
) and at least
some keys seem to be omitted completely — for example, the Light behavior does seem to respond to directionLongitude
as well.
Without example usages in iOS, finding ideal values to use here can be a bit tricky, but here’s a very basic example of a light focused on the center of the layer, making the emitted cells fade into darkness as they move outward.
let lightBehavior = CAEmitterBehavior(type: kCAEmitterBehaviorLight)
lightBehavior.setValue(UIColor.white.cgColor, forKeyPath: "color")
lightBehavior.setValue(CGPoint(x: emitterLayer.bounds.midX, y: emitterLayer.bounds.midY), forKeyPath: "position")
lightBehavior.setValue(150, forKey: "falloff")
Simple Attractor
The Attractor
behavior described below is the most versatile behavior available — but what if you
don’t need that kind of power? What if you just want some fixed point in the layer’s
plane that pulls in the cells around it?
Then look no further than Simple Attractor
!
Key | Type | Description |
---|---|---|
stiffness | NSNumber | The spring stiffness. |
radius | NSNumber | The attractor’s radius, particles inside the radius are unaffected. |
position | CGPoint | The attractor’s 2D position. |
falloff | NSNumber | Falloff values. |
This example simply pulls nearby cells back to the center of the layer, creating a bouncing, swarm-like effect:
let simpleAttractorBehavior = CAEmitterBehavior(type: kCAEmitterBehaviorSimpleAttractor)
simpleAttractorBehavior.setValue(15, forKey: "stiffness")
simpleAttractorBehavior.setValue(CGPoint(x: emitterLayer.bounds.midX, y: emitterLayer.bounds.midY), forKeyPath: "position")
simpleAttractorBehavior.setValue(500, forKey: "falloff")
Attractor
As mentioned above, the Attractor
behavior is easily the most versatile of the bunch. Let’s start
by looking at all its available keys:
Key | Type | Description |
---|---|---|
attractorType | String | The attractor shape: radial (default), axial or planar . |
stiffness | NSNumber | The spring stiffness. |
radius | NSNumber | The attractor’s radius, particles inside the radius are unaffected. |
position | CGPoint | The attractor’s 2D position. |
zPosition | CGFloat | The attractor’s position on z axis. |
orientationLatitude | NSNumber | The orientation used as the axis of axial attractors or normal of planar attractors. |
falloff | NSNumber | Falloff values. |
While it doesn’t have the most options of all the available behaviors,
those options do combine to create a large number of effects using the Attractor
behavior alone,
and the fact that at least some of its properties are animatable (like stiffness
) allows for even more possibilities.
As opposed to Simple Attractor
above, which always uses a radial attractor type,
Attractor
offers two additional options — attracting cells to a plane, or to an axis. Both have their uses,
with axis attractos being particularly nice for creating spiraling, tornado-like effects.
To accompany these two new types, more configuration options are added: the orientationLatitude
key listed above
lets you change the orientation of the attractor, and similarly to what we saw with the Light
behavior, Attractor
also
allows for the use of an orientationLongitude
key, even though it’s not listed in the documentation. The final advancement over Simple Attractor
is the ability to set the attractor’s position on the z
axis with the zPosition
.
In our Attractor
example, we’ll see one more key feature of the behavior — by using a negative falloff
value,
it’s possible to create a repelling force instead. This opens up a myriad of possibilities, including the popping effect shown below,
or the initial explosion effect seen in iMessage’s confetti implementation.
let attractorBehavior = CAEmitterBehavior(type: kCAEmitterBehaviorAttractor)
attractorBehavior.setValue(20, forKeyPath: "stiffness")
attractorBehavior.setValue(300, forKeyPath: "radius")
attractorBehavior.setValue(CGPoint(x: emitterLayer.bounds.midX, y: emitterLayer.emitterPosition.y + 25), forKeyPath: "position")
attractorBehavior.setValue(70, forKeyPath: "zPosition")
attractorBehavior.setValue(-280, forKeyPath: "falloff")
Up Next
We’ve looked at the individual CAEmitterBehavior
types and seen the effects that can be
created with each; but as mentioned at the start of this post, the real power with these behaviors
comes from combining them together.
For the second half of this series, we’ll look at creating our own version of iMessage’s confetti effect, using multiple emitter behaviors, animations, and at least one or two more undocumented pieces of CAEmitterLayer functionality. Stay tuned!
Say Hello!