CATransaction is a class that is often overlooked by many iOS developers despite offering many useful functions for controlling and responding to animations. The documentation explains things fairly well, but this post’s goal is to explore CATransaction in depth.

What Transactions Are

In Core Animation, transactions are a way to group multiple animation-related changes together. Transactions ensure that the desired animation changes are committed to Core Animation at the same time:

CATransaction.begin()

backingLayer1.opacity = 1.0
backingLayer2.position = CGPoint(x: 50.0, y: 50.0)
backingLayer3.backgroundColor = UIColor.redColor().CGColor

CATransaction.commit()
Creating a transaction

In the trivial example above, no animations will actually occur. The changes made to layers in this way will be reflected immediately.

As the documentation explains, Core Animation has two types of transactions: implicit and explicit. On threads with a run loop (e.g., the main thread), all changes to a layer tree during a run loop cycle will be implicitly placed in a transaction as long as an explicit transaction isn’t already specified. Note that an implicit transaction is not created for changes to backing layers.1

For standalone layers, explicit transactions aren’t needed to make animated changes:

layer1.opacity = 1.0
layer2.position = CGPoint(x: 50.0, y: 50.0)
layer3.backgroundColor = UIColor.redColor().CGColor
Implicit transaction involving standalone layers

At the beginning of the run loop cycle before that code is executed, Core Animation will have created a transaction implicitly. After running that code, those standalone layer changes will automatically be encoded as animations. At the end of the run loop cycle, Core Animation commits the implicit transaction, and any enqueued animations created within that time are executed.

So now that we know how to create transactions, what can they actually do for us?

Changing Animation Duration

Transactions can be used to change the animation duration of every animation involved with that transaction:

layer1.opacity = 1.0	// Default, implicit animation duration

CATransaction.begin()
CATransaction.setAnimationDuration(2.0)

layer2.position = CGPoint(x: 50.0, y: 50.0)             // Duration: 2.0
layer3.backgroundColor = UIColor.redColor().CGColor     // Duration: 2.0

CATransaction.commit()
Changing animation duration using transactions

layer1’s opacity change will occur with whatever the implicit transaction’s animation duration is. layer2’s and layer3’s respective property changes will occur over the course of 2 seconds, thereby overriding the default implicit animation duration.

Changing Animation Timing Function

Transactions can be used to change the animation timing function of every animation involved with that transaction:

layer1.opacity = 1.0    // Uses kCAMediaTimingFunctionDefault

let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

CATransaction.begin()
CATransaction.setAnimationTimingFunction(timingFunction)

layer2.position = CGPoint(x: 50.0, y: 50.0)
layer3.backgroundColor = UIColor.redColor().CGColor

CATransaction.commit()
Changing animation timing function using transactions

CAMediaTimingFunction allows you to specify a cubic Bézier timing function to apply to your animations. You are probably familiar with the standard timing functions used by UIKit, like ease in, ease out, and ease in-ease out. Core Animation supports these same functions via the named media timing function constants.

Ease in-ease out Bézier curve

The power of CAMediaTimingFunction, however, is that you can specify all the points involved in a cubic Bézier curve to create custom animation timing:

Custom Bézier curve

The curve displayed above can be represented as the following media timing function:2

let timingFunction = CAMediaTimingFunction(controlPoints: 0.0, 1.0, 0.76, 0.73)

One of the most useful ways to apply this type of transaction is to a UIView-style animation function:

let timingFunction = CAMediaTimingFunction(controlPoints: 0.0, 1.0, 0.76, 0.73)

CATransaction.begin()
CATransaction.setAnimationTimingFunction(timingFunction)

UIView.animateWithDuration(0.5, animations: {
    view1.alpha = 1.0
})

CATransaction.commit()
Overriding UIView's animation timing function

UIView-style animation functions support the standard timing functions, but they don’t allow you to specify your own cubic Bézier curve. CATransaction can be used instead to force these animations to use the supplied CAMediaTimingFunction to pace animations.3

This is a nice way to leverage the convenience of UIView-style animation functions while still being able to somewhat customize the animation pacing.

Preventing Animations from Occurring

Transactions can be used to prevent every animation involved with that transaction from occurring:

CATransaction.begin()
CATransaction.setDisableActions(true)

UIView.animateWithDuration(0.5, animations: {
    view1.alpha = 1.0
})

layer2.position = CGPoint(x: 50.0, y: 50.0)

CATransaction.commit()
Preventing animations from being committed using transactions

Disabling actions tells Core Animation to simply skip any animated changes to layer properties, so the new values are reflected immediately.4

If you recall, standalone layers placed in a layer tree that exists in a run loop-driven thread (i.e., practically every layer you create yourself) may apply implicit animations when certain properties are changed. This is often a source of confusion for some developers who are working directly with layers when unintended animations occur. In order to facilitate immediate changes to these layer properties,5 actions must be disabled in a transaction that wraps those changes.

Again, backing layers do not need to have their actions disabled explicitly when making model layer property changes, as UIView handles enabling and disabling actions automatically, though doing so doesn’t hurt.

UIView itself has a handful of functions involved with enabling and disabling animations, such as setAnimationsEnabled(_:) and performWithoutAnimation(_:). However, to ensure that both UIView-style and CALayer-style animations are suppressed, you can always just use CATransaction.

Occasionally, I find that deep within UIKit, an animation block was created that I wasn’t expecting. While certainly not an ideal solution, if you find unexpected animations are occurring when none of your code could possibly be creating animations, you can attempt to strategically use CATransaction to disable actions temporarily.

Getting Notified When Animations Finish

Transactions can be used to notify you when every animation involved with that transaction is finished:

CATransaction.begin()
CATransaction.setCompletionBlock({
    // Every animation added to this transaction is now finished
})

UIView.animateWithDuration(0.5, delay: 0.5, options: [], animations: {
    view1.alpha = 1.0
}, completion: nil)

addSeveralLayerAnimations()

CATransaction.commit()
Getting notified when a transaction's animations finish

This is an incredibly useful capability of CATransaction, and besides disabling actions, it is by far what I personally use CATransaction for the most. Regardless of how complex the timings may be for any number of animations enqueued during the transaction, the completion block will be called only after every animation has finished. In the event that animations are canceled, the completion block will be called at that point.

Note that you must set the completion block before creating any animations in that transaction that you want to be tracked for completion.

Regardless of whether the animations involved are CALayer animations or UIView animations,6 CATransaction will capture and consider all of them to determine the last running animation for calling the completion block. Per the documentation, the completion block will always be called on the main thread.

Another important thing to remember is that CATransaction only considers animations committed directly within the scope of that transaction’s lifecycle. This may seem obvious, but consider the following example:

CATransaction.begin()
CATransaction.setCompletionBlock({
    // Every animation added to this transaction is now finished
})

UIView.animateWithDuration(0.5, delay: 0.5, options: [], animations: {
    view1.alpha = 1.0
}, completion: { finished in
    self.addSeveralLayerAnimations()
})

CATransaction.commit()
Transaction completion blocks won't consider animations added later

In this example, the transaction’s completion block will be called immediately after the UIView animation completes. Because the animations in addSeveralLayerAnimations() are only added after the first animation finishes, they are not committed during the lifecycle of the transaction. Thus, they are not considered when determining when to call the completion block.

In order to ensure that every animation is accounted for, use delayed animations that are committed immediately rather than waiting to commit zero-delay animations:

CATransaction.begin()
CATransaction.setCompletionBlock({
    // Every animation added to this transaction is now finished
})

let animationDuration = 0.5
let animationDelay = 0.5

UIView.animateWithDuration(animationDuration, delay: animationDelay, options: [], animations: {
    view1.alpha = 1.0
}, completion: nil)

let firstAnimationCompletionTime = animationDuration + animationDelay

addSeveralLayerAnimations(delay: firstAnimationCompletionTime)

CATransaction.commit()
Committing delayed animations immediately so the transaction captures them

If addSeveralLayerAnimations(delay:) ensures that it creates its actual animations immediately—specifying delays appropriately—, then CATransaction will wait for them to complete as well, calling the completion block only after every animation is finished running. This is likely the desired behavior in most scenarios like this.

Working with Locks

Transactions can be used to safely modify layer properties in a concurrent environment:

CATransaction.lock()

layer1.opacity = 1.0

CATransaction.unlock()
Locking transactions for thread safety

Core Animation is inherently thread safe, so layer animations and changes to layer trees can occur on any thread. However, if shared layer objects are involved across multiple threads, it’s necessary to use a transaction to lock and unlock access to that data to prevent data corruption.

CATransaction locks are recursive, so they’re completely safe to use multiple times in the same thread.

Nesting Transactions

Transactions can be nested:

CATransaction.begin()
CATransaction.setCompletionBlock({
    // layer1, layer2, and layer3 animations are all finished
})

layer1.opacity = 1.0

    CATransaction.begin()
    CATransaction.setCompletionBlock({
        // layer2 and layer3 animations are all finished
    })

    layer2.position = CGPoint(x: 50.0, y: 50.0)
    layer3.backgroundColor = UIColor.redColor().CGColor

    CATransaction.commit()

CATransaction.commit()
Creating nested transactions

In the code above, the outer transaction will consider all three implicit layer animations. The inner transaction will only consider the second and third animations.

In fact, for all iOS applications, an implicit CATransaction is created just before each run loop cycle and committed just after each run loop cycle. So every transaction that we would use in our applications will always be nested inside this run loop transaction.7

There is no way for us to known if a transaction is nested within another transaction using public APIs.

Flushing Transactions

CATransaction.flush() is a mysterious function that has confusing documentation. Someone did a lot of in-depth exploration of what flushing transactions does, and rather than rehash what they discovered, you can read all about it yourself.

The gist is that 99.9999% of the time, you will never need to call this function.

Summary

Core Animation is a complex machine that has a lot of hidden or lesser-known capabilities. CATransaction has a lot of uses, especially if you create complex animations. Being able to override implicit animation durations and timing functions is useful for customizing animation timing. Disabling actions is necessary in some cases, and it guarantees that the changes you make won’t enqueue unexpected animations. Lastly, being able to receive a callback whenever any arbitrary combination of animations is finished is great for controlling your UI’s lifecycle. Of course, any number of these features can be combined into a single transaction, so it’s not necessary to create multiple transactions just to make multiple changes at once.

In later blog posts, we’ll continue to dive more deeply into other useful Core Animation classes.

  1. A backing layer is one that backs a UIView and is created and managed by UIView directly. A standalone layer is one that is created using a CALayer (or subclass) initializer, is added to a layer tree, and is managed by whomever or whatever created it.

  2. One of the strangest APIs in my opinion, CAMediaTimingFunction takes unnamed control point function parameters instead of naming them or using two CGPoints instead. This deviates from practically every other Cocoa Touch API naming convention.

  3. Neither specifying a UIViewAnimationOptions easing curve nor including .OverrideInheritedCurve as an animation option will override the timing function specified by the wrapping CATransaction.

  4. Technically, disabling actions does just that: prevents CAAction-conforming objects from being created in response to layer property changes. More on CAAction at another time.

  5. Actually, even when animations are running, the property changes have already occurred immediately in the model layer. It is the presentation layer that is responsible for displaying what we perceive as the animation.

  6. In fact, all animations created are CALayer animations. UIView ends up creating corresponding layer animations for their animatable properties when changed within an animation block.

  7. Core Animation is able to efficiently render and synchronize animations to the main thread of your application because it maintains this outermost transaction. By coalescing actions into run loop cycles, application content is only potentially rendered and displayed according to the device’s refresh rate, although flushing transactions can interfere with this.

CALayer