CATransaction in Depth
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
- Changing Animation Duration
- Changing Animation Timing Function
- Preventing Animations from Occurring
- Getting Notified When Animations Finish
- Working with Locks
- Nesting Transactions
- Flushing Transactions
- Summary
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.red.cgColor
CATransaction.commit()
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.red.cgColor
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.red.cgColor // Duration: 2.0
CATransaction.commit()
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.red.cgColor
CATransaction.commit()
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.
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:
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.animate(withDuration: 0.5, animations: {
view1.alpha = 1.0
})
CATransaction.commit()
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.animate(withDuration: 0.5, animations: {
view1.alpha = 1.0
})
layer2.position = CGPoint(x: 50.0, y: 50.0)
CATransaction.commit()
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 automatically6, 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.animate(withDuration: 0.5, delay: 0.5, options: [], animations: {
view1.alpha = 1.0
}, completion: nil)
addSeveralLayerAnimations()
CATransaction.commit()
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,7 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.animate(withDuration: 0.5, delay: 0.5, options: [], animations: {
view1.alpha = 1.0
}, completion: { finished in
self.addSeveralLayerAnimations()
})
CATransaction.commit()
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.animate(withDuration: animationDuration, delay: animationDelay, options: [], animations: {
view1.alpha = 1.0
}, completion: nil)
let firstAnimationCompletionTime = animationDuration + animationDelay
addSeveralLayerAnimations(delay: firstAnimationCompletionTime)
CATransaction.commit()
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()
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.
In practice, using transaction locks is almost never necessary. Just keep in mind that if, for some reason, you’re passing layers around to multiple threads and modifying their properties, you should use transaction locks when doing so to ensure data validity.
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.red.cgColor
CATransaction.commit()
CATransaction.commit()
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.8
There is no way for us to know 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 in your working code.
There is a neat trick you can use while breaking in the debugger, though. Because the render server exists outside of your application’s process, being stopped at a break point in your own code doesn’t affect Core Animation rendering at all.9 However, if you were to change a UIView
or CALayer
property while stopped in the debugger, you wouldn’t see any visual changes in your app until you resumed execution. Calling CATransaction.flush()
, though, would immediately refresh the display in many cases, without requiring you to resume your application.
Per the description, CATransaction.flush()
“flushes any extant implicit transaction.” And, in fact, for every iOS application, there is always an extant—i.e., “still existing”—implicit transaction. This is because the implicit, top-level, run-loop transaction is still waiting to be committed.
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.
-
A backing layer is one that backs a
UIView
and is created and managed byUIView
directly. A standalone layer is one that is created using aCALayer
(or subclass) initializer, is added to a layer tree, and is managed by whomever or whatever created it. ↩ -
One of the strangest APIs in my opinion,
CAMediaTimingFunction
takes unnamed control point function parameters instead of naming them or using twoCGPoint
s instead. This deviates from practically every other Cocoa Touch API naming convention. ↩ -
Neither specifying a
UIViewAnimationOptions
easing curve nor including.OverrideInheritedCurve
as an animation option will override the timing function specified by the wrappingCATransaction
. ↩ -
Technically, disabling actions does just that: prevents
CAAction
-conforming objects from being created in response to layer property changes. More onCAAction
at another time. ↩ -
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. ↩
-
Specifically,
UIView
creates noCAAction
s for animatableCALayer
properties unless those properties are being modified within a UIKit-style animation API. ↩ -
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. ↩ -
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. ↩
-
To easily see this in practice, add and stop at a break point just after creating a long animation. When running that code, you’ll notice that even though Xcode will stop your application’s main thread, the animation continues to run unimpeded by any of your process’s code. This, again, emphasizes how Core Animation’s architecture does not allow blocking application code to hamper much of the render server’s activities. ↩