Recreating iMessage Confetti


Bryce Bostwick

 •  •  • 

iMessage has easily one of the best-looking confetti implementations to be found on iOS, across first and third-party apps alike.

But how does it work? And why haven’t we seen any accurate clones of its design? (The short answer: undocumented functionality, and undocumented functionality).

Let’s go through how to recreate iMessage’s confetti implementation, step by step, using a few undocumented parts of CAEmitterLayer. This is intended as a follow-up to the previous post (an introduction to the wonderful CAEmitterBehavior class), but that’s not the only piece of undocumented functionality we’ll look at today.

As before, note that it is likely a bad idea to ship this in a production app (although you could maybe get all the way through Step 3 without any trouble, and you’d end up with very nice confetti if you did; I would still not recommend it though!).

(Update: this post is now available as an Xcode Playground!)


Step 1: Creating Confetti Images

(Note that this section mostly exists for the sake of including all necessary code; feel free to skip to Step 2 to jump to the emitter layer setup)

There’s a lot of variety in a good confetti implementation; different pieces of confetti will have different shapes, colors, and more.

To best match iMessage’s confetti effect, we’ll want to account for not only shape and color, but also if the confetti is in the foreground or background — iMessage uses blurred images for background confetti to help give the impression of distance.

Shape, color, and position; let’s start by representing these in code.

/**
 Represents a single type of confetti piece.
 */
class ConfettiType {
    let color: UIColor
    let shape: ConfettiShape
    let position: ConfettiPosition

    init(color: UIColor, shape: ConfettiShape, position: ConfettiPosition) {
        self.color = color
        self.shape = shape
        self.position = position
    }
}

enum ConfettiShape {
    case rectangle
    case circle
}

enum ConfettiPosition {
    case foreground
    case background
}

Now let’s add the ability to actually get an image representation of a given ConfettiType. There are a lot of options for how to proceed here, depending on what level of customization you need for your confetti — like reading a set of images from a disk, or drawing custom shapes based on a given path.

Since we’re trying to mirror iMessage’s design, and only need to create circles and rectangles of different colors, we’ll use some simple drawing code to do so.

class ConfettiType {

    // ...

    lazy var image: UIImage = {
        let imageRect: CGRect = {
            switch shape {
            case .rectangle:
                return CGRect(x: 0, y: 0, width: 20, height: 13)
            case .circle:
                return CGRect(x: 0, y: 0, width: 10, height: 10)
            }
        }()

        UIGraphicsBeginImageContext(imageRect.size)
        let context = UIGraphicsGetCurrentContext()!
        context.setFillColor(color.cgColor)

        switch shape {
        case .rectangle:
            context.fill(imageRect)
        case .circle:
            context.fillEllipse(in: imageRect)
        }

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image!
    }()
}

Finally, using some truly ugly code for the sake of not having to include a hex-color-decoding method, let’s create an array containing all of the confetti types we want to see.

lazy var confettiTypes: [ConfettiType] = {
    let confettiColors = [
        (r:149,g:58,b:255), (r:255,g:195,b:41), (r:255,g:101,b:26),
        (r:123,g:92,b:255), (r:76,g:126,b:255), (r:71,g:192,b:255),
        (r:255,g:47,b:39), (r:255,g:91,b:134), (r:233,g:122,b:208)
        ].map { UIColor(red: $0.r / 255.0, green: $0.g / 255.0, blue: $0.b / 255.0, alpha: 1) }

    // For each position x shape x color, construct an image
    return [ConfettiPosition.foreground, ConfettiPosition.background].flatMap { position in
        return [ConfettiShape.rectangle, ConfettiShape.circle].flatMap { shape in
            return confettiColors.map { color in
                return ConfettiType(color: color, shape: shape, position: position)
            }
        }
    }
}()

Step 2: Basic Emitter Layer Setup

Now that we can dynamically create images for our individual confetti pieces, let’s set up a basic CAEmitterLayer to emit them.

We’ll start with cells being emitted within a rectangle just above the screen; this will change later, but it’ll give us a nice falling confetti effect to work with now.

lazy var confettiLayer: CAEmitterLayer = {
    let emitterLayer = CAEmitterLayer()

    emitterLayer.emitterCells = confettiCells
    emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.minY - 500)
    emitterLayer.emitterSize = CGSize(width: view.bounds.size.width, height: 500)
    emitterLayer.emitterShape = .rectangle
    emitterLayer.frame = view.bounds

    emitterLayer.beginTime = CACurrentMediaTime()
    return emitterLayer
}()

lazy var confettiCells: [CAEmitterCell] = {
    return confettiTypes.map { confettiType in
        let cell = CAEmitterCell()

        cell.beginTime = 0.1
        cell.birthRate = 10
        cell.contents = confettiType.image.cgImage
        cell.emissionRange = CGFloat(Double.pi)
        cell.lifetime = 10
        cell.spin = 4
        cell.spinRange = 8
        cell.velocityRange = 100
        cell.yAcceleration = 150

        return cell
    }
}()

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    view.layer.addSublayer(confettiLayer)
}

Step 3: A New Spin On Things

We’re only on Step 3 and already running into our first use of undocumented functionality — did you know that CAEmitterCell has multiple types of particles that each allow for a different appearance?

By default, emitter cells are created with a particle type of "sprite" — but as alluded to in Align To Motion’s documentation, there is also a "plane" type available, and using it opens up some interesting opportunities.

Let’s switch to using this particle type in our emitter cell setup, along with a few properties related to cell orientation that will now be able to take effect.

lazy var confettiCells: [CAEmitterCell] = {
    return confettiImages.map { confettiImage in
        // ...
        cell.setValue("plane", forKey: "particleType")
        cell.setValue(Double.pi, forKey: "orientationRange")
        cell.setValue(Double.pi / 2, forKey: "orientationLongitude")
        cell.setValue(Double.pi / 2, forKey: "orientationLatitude")
        // ...
    }
}()

The difference between this and our previous implementation is night and day. With the "plane" particle type, our confetti can now rotate on more than one axis, giving it an immediate three-dimensional appearance.

This alone makes for a pretty good confetti implementation, and only uses a bit of undocumented behavior! But it doesn’t quite look like iMessage yet. Let’s keep going.

Step 4: Wave Hello to CAEmitterBehavior

Let’s add our first CAEmitterBehavior to the mix. We’ll use two Wave behaviors to give a slight flutter to the confetti pieces as they fall.

We’ll first define two methods for creating the behaviors.

func horizontalWaveBehavior() -> CAEmitterBehavior {
    let behavior = CAEmitterBehavior(type: kCAEmitterBehaviorWave)
    behavior.setValue([100, 0, 0], forKeyPath: "force")
    behavior.setValue(0.5, forKeyPath: "frequency")
    return behavior
}

func verticalWaveBehavior() -> CAEmitterBehavior {
    let behavior = CAEmitterBehavior(type: kCAEmitterBehaviorWave)
    behavior.setValue([0, 500, 0], forKeyPath: "force")
    behavior.setValue(3, forKeyPath: "frequency")
    return behavior
}

Next, let’s create one method that we’ll use to add all behaviors to our emitter layer, and then invoke it after that layer has been set up.

func addBehaviors() {
    confettiLayer.setValue([
        horizontalWaveBehavior(),
        verticalWaveBehavior()
    ], forKey: "emitterBehaviors")
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    view.layer.addSublayer(confettiLayer)
    addBehaviors() // new!
}

The wave effect makes the confetti’s path a little less linear, but it’s very subtle — with the values we’ve chosen above, the effect is really only noticeable if you pick out a particular piece and follow its descent.

It’s a welcome addition, but let’s move on to a CAEmitterBehavior that’s a little more noticeable.

Step 5: More Attractive Confetti

So far, we’ve only had confetti falling down uniformly from the top of the screen, but this is notably different from what we find in iMessage, where confetti seems to explode out from the top & center of the device.

As mentioned previously, this explosive effect is powered by an Attractor behavior — specifically using a negative falloff value.

func attractorBehavior(for emitterLayer: CAEmitterLayer) -> CAEmitterBehavior {
    let behavior = CAEmitterBehavior(type: kCAEmitterBehaviorAttractor)

    // Attractiveness
    behavior.setValue(-290, forKeyPath: "falloff")
    behavior.setValue(300, forKeyPath: "radius")
    behavior.setValue(10, forKeyPath: "stiffness")

    // Position
    behavior.setValue(CGPoint(x: emitterLayer.emitterPosition.x,
                              y: emitterLayer.emitterPosition.y + 20),
                      forKeyPath: "position")
    behavior.setValue(-70, forKeyPath: "zPosition")

    return behavior
}

The setup of this behavior is a bit more involved, but still fairly straightforward — first we set up some basic (and slightly arbitrary) values that affect the strength of the behavior; the key note here is the negative falloff value, causing this attractor to provide a repulsive force.

From there we position the attractor a bit below & a bit behind the emitter, causing the pieces to pop up & out (“out” being a loose description here, since our cells don’t scale up or down based on z-position; but the value does still affect their direction & movement, just not their appearance).

There are a few other changes we need to make to support this new behavior; we’ll change the emitter to be a sphere in the center of the screen to help see the effect of the attractor behavior; and we’ll remove the cell-defined velocity and y-acceleration for the same reason. Finally, we’ll add our new behavior alongside the existing ones.

lazy var confettiLayer: CAEmitterLayer = {
    // ...
    emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
    emitterLayer.emitterSize = CGSize(width: 100, height: 100)
    emitterLayer.emitterShape = .sphere
    // ...
}()

lazy var confettiCells: [CAEmitterCell] = {
    return confettiImages.map { confettiImage in
        // ...
        cell.velocityRange = 0
        cell.yAcceleration = 0
        // ...
    }
}()

func addBehaviors() {
    confettiLayer.setValue([
        horizontalWaveBehavior(),
        verticalWaveBehavior(),
        attractorBehavior(for: confettiLayer) // new!
    ], forKey: "emitterBehaviors")
}

Step 6: Animations & Explosions

and the point when I stop making puns in section titles

The above animation is a good start, but it’s not really an explosion yet; the main issue is the constant-rate emission of cells.

As mentioned in the last post, some CAEmitterBehavior properties are animatable — now we’ll get to see that ability in action. We’ll animate our attractor behavior’s stiffness property, so that we initially have a strong repulsive force, then head towards almost no force at all. To do so, we’ll have to first modify our behavior to have a name, so that we can reference it via key path from our emitter layer.

func attractorBehavior(for emitterLayer: CAEmitterLayer) -> CAEmitterBehavior {
    // ...
    behavior.setValue("attractor", forKeyPath: "name")
    // ...
}

func addAttractorAnimation(to layer: CALayer) {
    let animation = CAKeyframeAnimation()
    animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
    animation.duration = 3
    animation.keyTimes = [0, 0.4]
    animation.values = [80, 5]

    layer.add(animation, forKey: "emitterBehaviors.attractor.stiffness")
}

We’ll also bump up our cells’ birthrates and make our emitter layer’s overall birthrate animate from 1 to 0; this will give us a lot of confetti pieces at the start of our animation, with fewer pieces afterward to be affected by the now-weaker attractor.

var confettiLayer: CAEmitterLayer = {
    // ...
    emitterLayer.birthRate = 0
    // ...
}

lazy var confettiCells: [CAEmitterCell] = {
    return confettiImages.map { confettiImage in
        // ...
        cell.birthRate = 100
        // ...
    }
}()

func addBirthrateAnimation(to layer: CALayer) {
    let animation = CABasicAnimation()
    animation.duration = 1
    animation.fromValue = 1
    animation.toValue = 0

    layer.add(animation, forKey: "birthRate")
}

Finally, we simply add these animations to the layer:

func addAnimations() {
    addAttractorAnimation(to: confettiLayer)
    addBirthrateAnimation(to: confettiLayer)
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    view.layer.addSublayer(confettiLayer)
    addBehaviors()
    addAnimations() // new!
}

Now we have a proper confetti explosion!

We’ll improve on this in a moment, but as a quick aside, the last few frames here make it easy to see the wave effect if you missed it before. It looks a bit nonsensical without any gravity, so let’s add that back now.

Step 7: Air Resistance & Gravity

Let’s attempt to add some realism back to our confetti, starting with air resistance — or at least a close proxy to it. We can use the Drag behavior to slow particles down when they’re moving quickly, which will be perfect for countering our initial explosion; confetti particles should start with a high velocity, but slow quickly as they encounter air resistance.

The Drag behavior is also animatable; we can leverage that to make sure it doesn’t get in the way of our initial explosion, by starting with no drag and quickly animating the value up.

func dragBehavior() -> CAEmitterBehavior {
    let behavior = CAEmitterBehavior(type: kCAEmitterBehaviorDrag)
    behavior.setValue("drag", forKey: "name")
    behavior.setValue(2, forKey: "drag")

    return behavior
}

func addDragAnimation(to layer: CALayer) {
    let animation = CABasicAnimation()
    animation.duration = 0.35
    animation.fromValue = 0
    animation.toValue = 2

    layer.add(animation, forKey:  "emitterBehaviors.drag.drag")
}

This would also be a good time to add gravity back into our emitter — but crucially, we still want the “floating” effect we see right after the initial explosion in iMessage’s implementation. We can manage this with animations as well, but unfortunately CAEmitterLayer doesn’t have its own y-acceleration property; we’ll have to name each of our cells so we can animate them individually.

class ConfettiType {
    // ...

    // Certainly not the _best_ name, but it works for our purposes
    lazy var name = UUID().uuidString

    // ...
}

lazy var confettiCells: [CAEmitterCell] = {
    return confettiTypes.map { confettiType in
        // ...
        cell.name = confettiType.name
        // ...
    }
}()

func addGravityAnimation(to layer: CALayer) {
    let animation = CAKeyframeAnimation()
    animation.duration = 6
    animation.keyTimes = [0.05, 0.1, 0.5, 1]
    animation.values = [0, 100, 2000, 4000]

    for image in confettiTypes {
        layer.add(animation, forKey: "emitterCells.\(image.name).yAcceleration")
    }
}

For each new animation and behavior, remember to add the appropriate function calls alongside the existing ones for setup, at which point you should have a near-complete confetti implementation:

Step 8: Background & Foreground

There’s one final change we need in order to mirror iMessage’s confetti as closely as possible; two separate confetti layers. This is another detail that’s pretty subtle if you’re not looking for it — but not hard to notice once you know about it.

A second layer with smaller, slower confetti pieces allows for an even more three-dimensional end result. iMessage handles this by using a different set of blurred images for the background layer, along with different values for certain operations — you can imagine a slightly lower gravitational force as making an object appear further away, for instance.

We’ll keep our implementation simpler than that, with the only difference in the background layer essentially being:

for emitterCell in emitterLayer.emitterCells ?? [] {
    emitterCell.scale = 0.5
}

emitterLayer.opacity = 0.5
emitterLayer.speed = 0.95

And that’s it! A very similar effect with only a few lines of code. Some of our previous methods will need to be updated to support the two layers, but we’ll leave the exact details out of this post — most of the methods have already been set up to make such a change easier, like taking in layers as a parameter where needed.

Once animations & behaviors are added to both layers appropriately, we finally end up with the implementation that appeared at the top of this post:


Wrapping Up

In contrast to the previous high-level overview of CAEmitterBehavior, the goal for this post was to dive into a specific example and see just how nice of an effect we can make with it — and the end result is pretty amazing for a relatively small amount of code.

CAEmitterBehavior’s existence as an undocumented class is a bit sad, but hopefully there is some value to be found for this it out in the wild — a bit of confetti within an #if DEBUG statement makes for a much better development experience if you ask me!


Say Hello!