CAEmitterBehavior


Bryce Pauken

 •  •  • 

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.

KeyTypeDescription
force3 component vector - pass as array of CGFloatForce vector [X Y Z]
frequencyCGFloatOscillation 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.

KeyTypeDescription
dragCGFloatSlowing 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.

KeyTypeDescription
rotationCGFloatAngle in rotations to apply in addition to orientation of the velocity vector.
preservesDepthBoolIf 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:

KeyTypeDescription
keyPathStringName of the property to change.
valuesArray of NSNumberArray of numeric values.
locationsArray of NSNumberOptional 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.

KeyTypeDescription
colorsArray of CGColorArray of gradient color stops.
locationsArray of NSNumberOptional 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.

KeyTypeDescription
colorCGColorThe light’s color, alpha is ignored.
positionCGPointThe light’s 2D position.
zPositionCGFloatThe light’s position on z axis.
falloffNSNumberFalloff values.
spotBoolIf true, light is a spot-light.
appliesAlphaBoolIf true, lit object’s alpha is also affected.
directionLatitudeNSNumberThe light’s direction (spot-lights only).
coneAngleNSNumberThe 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!

KeyTypeDescription
stiffnessNSNumberThe spring stiffness.
radiusNSNumberThe attractor’s radius, particles inside the radius are unaffected.
positionCGPointThe attractor’s 2D position.
falloffNSNumberFalloff 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:

KeyTypeDescription
attractorTypeStringThe attractor shape: radial (default), axial or planar.
stiffnessNSNumberThe spring stiffness.
radiusNSNumberThe attractor’s radius, particles inside the radius are unaffected.
positionCGPointThe attractor’s 2D position.
zPositionCGFloatThe attractor’s position on z axis.
orientationLatitudeNSNumberThe orientation used as the axis of axial attractors or normal of planar attractors.
falloffNSNumberFalloff 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!