Core Animation on macOS: Transactions, Model State, and Presentation
Reason about Core Animation transactions, implicit actions, model and presentation layers, completion timing, and reliable animation diagnostics on macOS.
Core Animation can make a layer property look deceptively synchronous. Assigning a new value to position, opacity, or another animatable property updates the layer tree that your application owns, while the compositor may still be displaying an interpolation toward that value. A reliable animation design distinguishes the model layer, the presentation layer, and the transaction that batches changes between them. Without that distinction, hit testing, cancellation, completion handling, and tests can read a state that is valid but not the state currently visible on screen.
This article describes the classic CALayer and CATransaction model used by AppKit applications. Core Animation also has higher-level animation APIs and specialized layer types; choose the API matching the behavior you need and verify availability against the deployment target. A transaction is a grouping and commit mechanism, not a promise that pixels have already reached the display.
Three views of an animated property
The model layer is the application-facing object. It stores the target values your code sets and participates in the layer hierarchy. During an animation, the presentation layer provides a close approximation of the values currently being displayed. It is a read-only, transient representation; it can be absent when no corresponding presentation state exists. The render server and display pipeline then consume committed changes asynchronously.
That separation explains a common observation: after setting layer.position, reading the model layer returns the destination immediately, although the visible layer is halfway through an animation. If code needs the current visual position for an interaction, it can inspect presentation() when available. If code needs the intended destination, it should read the model layer. Do not write to the presentation layer or retain it as durable application state; it is a view of in-progress rendering, not the source of truth.
The presentation layer’s hierarchy corresponds to the presentation tree. Its sublayers, superlayer, and hitTest behavior refer to presentation objects. Mixing a model-tree coordinate conversion with a presentation-tree hit test can therefore produce confusing results while transforms animate. Choose one tree for a given calculation, convert coordinates deliberately, and handle the case where a presentation object is unavailable because no animation is active.
Implicit transactions and explicit batches
Core Animation wraps layer-tree mutations in transactions. When a thread changes layer properties without an active transaction, Core Animation creates an implicit transaction and commits it when the run loop next iterates. This makes ordinary property changes convenient, but it also means a series of assignments is not necessarily visible at the instant each line executes.
Use an explicit transaction when a group of changes needs shared animation properties or a completion callback. Always balance begin() and commit() on every control-flow path. Transactions can nest; inner settings apply within the nested scope, and the outer transaction remains active afterward. Keep the scope narrow so unrelated layer updates do not accidentally inherit a duration, timing function, or disabled-action setting.
import QuartzCore
func move(_ layer: CALayer, to destination: CGPoint) {
CATransaction.begin()
CATransaction.setAnimationDuration(0.24)
CATransaction.setAnimationTimingFunction(
CAMediaTimingFunction(name: .easeInEaseOut)
)
CATransaction.setCompletionBlock {
// This runs after animations grouped by this transaction complete.
// Update nonvisual state here only if it still belongs to this request.
}
layer.position = destination
CATransaction.commit()
}
The callback is not a substitute for a state machine. If a second request supersedes the first while animation is in flight, the first completion can still arrive. Associate each transition with an identifier or cancellation policy and ignore stale completions. CATransaction documents its completion block as running on the main thread; also account for the case where no animation was added, in which case the block may run immediately. Keep other callbacks on their documented executor and isolate UI-owned state according to the framework’s threading rules.
Animation actions and intentionally immediate updates
An animatable property may resolve an action when it changes. The action can create an implicit animation, be supplied by the layer’s delegate or action dictionary, come from a class default, or resolve to no action. This lookup explains why changing a property can animate in one layer hierarchy but appear immediate in another. Inspect the action resolution path before adding a manual animation that may duplicate the implicit one.
For layout or state synchronization where an animation is undesirable, temporarily disable actions in a narrowly scoped transaction. Restore or commit that setting before unrelated work. Avoid disabling actions globally as a debugging shortcut: it can hide a missing animation policy and change behavior in code that happens to share the transaction scope. Conversely, explicit CABasicAnimation or keyframe animations should be coordinated with the model value. An explicit animation does not necessarily update the model layer’s target property; when it is removed, the layer can snap back to the model value if that value was never advanced.
An animation’s key, fill mode, removal behavior, and model-layer target form one lifecycle. Prefer setting the final model value and using a suitable explicit animation when continuity is required. Test interruption, replacement, removal, and reparenting, not only the successful completion path. Animation delegates and transaction completion blocks observe different scopes: an animation delegate reports an individual animation, while a transaction completion block relates to the transaction group.
Time, hierarchy, and geometry
Layer timing is hierarchical. A layer’s local time can be affected by its timing properties and its superlayer, so a timestamp meaningful in one layer’s clock may not be directly comparable in another. Core Animation provides conversion methods for moving time between layers. When synchronizing a custom animation to another layer, convert the time rather than assuming that a process-wide timestamp is already in the destination clock.
Geometry also has multiple representations. bounds describes a layer’s local rectangle, position locates its anchor point in its parent’s coordinate system, and transforms affect the mapping between spaces. An animated transform changes presentation geometry over time even when the model tree already has the final transform. Use layer coordinate-conversion APIs when moving points or rectangles between hierarchies, and explicitly decide whether the calculation is for model geometry or on-screen presentation geometry.
Completion is not display synchronization
A transaction completion block is useful for work that depends on the grouped animation finishing according to Core Animation’s animation lifecycle. If the group contains no animations, the block can run immediately; even when it follows an animation, it does not mean a screenshot captured on an arbitrary thread has necessarily observed a particular scan-out frame. Avoid calling a blocking flush or sleeping to force a visual result. Such work couples application responsiveness to the render pipeline and can still be unreliable under occlusion, app suspension, or display refresh changes.
If the task is to update application state, update the model state at the point the state transition is accepted, then animate the visual difference. Completion may release a temporary resource or start a dependent effect, but it should not be the only place where the model becomes correct. If the animation is interrupted, the model should still represent the intended state. For coordinated Metal presentation, use the documented CAMetalLayer transaction behavior; Metal drawable presentation and ordinary layer transactions are not automatically synchronized by default.
Diagnostics and tests
When an animation appears stale or jumps, record the property value, layer identity, transaction boundary, animation key, and whether the sample came from a model or presentation object. Capture screenshots at controlled times only as a visual check, not as the sole correctness assertion. A deterministic unit-level test can assert target model values and action configuration; a UI-level test can exercise interruption and visible continuity on a supported macOS version.
Test rapid successive changes, nested transactions, a missing presentation layer, layer removal before completion, window closure during animation, and a second transition replacing the first. Verify that callbacks cannot mutate a closed document or a newly selected model object. Test with reduce-motion preferences and high-refresh displays when the application adapts its motion policy. If a layer is hosted or rendered offscreen, validate that rendering mode’s timing and lifecycle separately from an onscreen window.
An acceptance review should answer four questions: which tree is authoritative for this read, who owns the transaction, what cancels or supersedes the animation, and what event truly signals that downstream work may proceed? Treat the presentation tree as an observation, the model tree as the target, and the transaction as a commit boundary. This keeps interaction logic stable even when the visual renderer is asynchronous.
Related:
- Metal on macOS: Command Buffers, Resource Hazards, and GPU Completion
- AUv3 on macOS: Extension Discovery and Real-Time Audio Rendering
Sources: