Implementing custom animations in iOS is one of the most effective ways to elevate the user experience, making applications feel more polished, responsive, and engaging. Apple’s Core Animation framework provides a powerful, hardware-accelerated infrastructure for rendering and animating visual content with remarkable efficiency. This article explores the fundamentals of Core Animation and demonstrates how to build custom animations that go beyond the built-in UIKit transitions. From basic property animations to complex keyframe sequences and group animations, you’ll gain a thorough understanding of the tools needed to craft fluid, production‑ready motion in your iOS apps.

Understanding Core Animation

Core Animation is not simply a set of animation convenience methods; it is a deep graphics compositing and rendering engine that sits beneath UIKit, AppKit, and SpriteKit. At its heart, Core Animation operates on layers (instances of CALayer). Every UIView is backed by a layer, and you can also create standalone layers for lightweight or off‑screen content.

Layers form a hierarchical layer tree. When you animate a layer property, Core Animation actually creates two copies of the layer: the model layer (which stores the current value of the property) and the presentation layer (which reflects the current visual state during an animation). This distinction is crucial for hit testing during animations and for understanding why some property changes are instant while others are interpolated.

Core Animation provides two animation styles:

  • Implicit animations – Automatic, system‑provided transitions when many layer properties change inside a transaction. For example, changing a layer’s position or opacity outside of an explicit animation block may still animate with default timing.
  • Explicit animations – Custom animations you create using classes such as CABasicAnimation, CAKeyframeAnimation, or CAAnimationGroup. These give you precise control over timing, key paths, and repeat behavior.

Mastering both styles allows you to choose the most appropriate approach for each scenario, balancing ease of use with granular control.

Getting Started with Basic Animations

The simplest way to add motion to a view is to use the UIKit animation block API, which internally leverages Core Animation. The following example changes a view’s center point over two seconds:

UIView.animate(withDuration: 2.0) {
    myView.layer.position = CGPoint(x: 200, y: 200)
}

Under the hood, UIKit wraps the property change in a CATransaction and creates an implicit animation with default timing. However, for custom animations that require fine‑grained control over key paths, repeat counts, or timing curves, you should work directly with CABasicAnimation.

Here’s the same position change animated explicitly:

let posAnimation = CABasicAnimation(keyPath: "position")
posAnimation.fromValue = myView.layer.position
posAnimation.toValue = CGPoint(x: 200, y: 200)
posAnimation.duration = 2.0
myView.layer.add(posAnimation, forKey: "positionAnimation")
myView.layer.position = CGPoint(x: 200, y: 200)

Notice that you must update the model value (the second line from the bottom) to the final position. Core Animation only displays the animation; the model layer retains its original value until you explicitly change it. This is a common pitfall – always remember to set the model property to the end state after adding the animation.

Creating Custom Animations with CABasicAnimation

CABasicAnimation animates a single property from one value to another over a specified duration. The key path can be any animatable property of CALayer, including opacity, cornerRadius, shadowRadius, transform.scale, and many more. You can also animate custom properties if you subclass CALayer and make them @NSManaged.

Animating Transform Properties

Transform animations are especially powerful. To rotate a view continuously:

let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.toValue = NSNumber(value: Double.pi * 2)
rotate.duration = 3.0
rotate.repeatCount = .infinity
myView.layer.add(rotate, forKey: "rotationAnimation")

To scale a layer:

let scale = CABasicAnimation(keyPath: "transform.scale")
scale.fromValue = 1.0
scale.toValue = 1.5
scale.duration = 1.0
scale.autoreverses = true
myView.layer.add(scale, forKey: "scaleAnimation")

Using autoreverses together with repeatCount creates a smooth pulse effect that returns to the original size.

Using CAKeyframeAnimation for Complex Paths

When you need a motion that passes through multiple intermediate states (not just a linear A→B), use CAKeyframeAnimation. This is ideal for animating a layer’s position along a curved path, or for changing opacity in a custom sequence.

let keyframe = CAKeyframeAnimation(keyPath: "position")
keyframe.values = [
    NSValue(cgPoint: CGPoint(x: 50, y: 50)),
    NSValue(cgPoint: CGPoint(x: 150, y: 100)),
    NSValue(cgPoint: CGPoint(x: 250, y: 50))
]
keyframe.keyTimes = [0.0, 0.5, 1.0]
keyframe.duration = 2.0
myView.layer.add(keyframe, forKey: "keyframeAnimation")

You can also use path property of CAKeyframeAnimation with a CGPath to move a layer along a bezier curve:

let path = UIBezierPath(roundedRect: CGRect(x: 20, y: 20, width: 300, height: 300),
                        cornerRadius: 50).cgPath
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.path = path
pathAnimation.duration = 5.0
pathAnimation.repeatCount = .infinity
myView.layer.add(pathAnimation, forKey: "pathAnimation")

Grouping Animations with CAAnimationGroup

Often you need to run multiple animations simultaneously, such as scaling a view while also moving it. CAAnimationGroup allows you to synchronize several animations under one duration and timing function.

let move = CABasicAnimation(keyPath: "position")
move.toValue = NSValue(cgPoint: CGPoint(x: 300, y: 300))

let fade = CABasicAnimation(keyPath: "opacity")
fade.toValue = 0.0

let scale = CABasicAnimation(keyPath: "transform.scale")
scale.toValue = 0.5

let group = CAAnimationGroup()
group.animations = [move, fade, scale]
group.duration = 2.0
myView.layer.add(group, forKey: "groupAnimation")

All animations in the group start at the same time (unless you set their individual beginTime relative to the group’s duration). The group’s duration overrides individual durations if they are shorter.

Timing Functions and Easing

Animation timing functions control the pace of the interpolation. Core Animation provides the CAMediaTimingFunction class with built‑in presets:

  • linear – Constant speed throughout.
  • easeIn – Starts slow, then speeds up.
  • easeOut – Starts fast, then slows down.
  • easeInEaseOut – Slow at both ends, fast in the middle.
  • default – Similar to easeInEaseOut.

You can also create a custom cubic bezier timing function using CAMediaTimingFunction(controlPoints:...). For example, a bounce‑like effect:

let bounceTiming = CAMediaTimingFunction(controlPoints: 0.25, 0.1, 0.25, 1.0)
animation.timingFunction = bounceTiming

Setting the right timing function is often the difference between a jerky motion and one that feels natural. For the highest degree of realism, consider using UIView’s spring animations (UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)) for bouncy effects, or implement custom physics with UIDynamicAnimator for interactive animations.

Animating Custom Properties

If your animation needs involve data not already represented by a standard CALayer property, you can extend CALayer with custom animatable properties. This requires overriding needsDisplay(forKey:) and action(forKey:) indicators, and marking the property with @NSManaged in Swift.

class CustomLayer: CALayer {
    @NSManaged var progress: CGFloat

    override class func needsDisplay(forKey key: String) -> Bool {
        if key == "progress" {
            return true
        }
        return super.needsDisplay(forKey: key)
    }

    override func draw(in ctx: CGContext) {
        // Draw using self.progress
    }
}

Then you can animate the progress property just like any other layer property:

let anim = CABasicAnimation(keyPath: "progress")
anim.fromValue = 0.0
anim.toValue = 1.0
anim.duration = 2.0
customLayer.add(anim, forKey: "progressAnim")
customLayer.progress = 1.0

This approach is especially useful for custom progress indicators, circular counters, and morphing shapes.

Advanced Techniques

CATransaction and Animation Coordination

Core Animation groups all animation changes within a transaction. You can create nested transactions to override animation duration, timing function, or even disable animations temporarily. CATransaction is also the mechanism by which you set completion blocks (though deprecated in favor of CAAnimation delegates or UIKit completion handlers).

CATransaction.begin()
CATransaction.setAnimationDuration(5.0)
myView.layer.opacity = 0.0
myView.layer.position = CGPoint(x: 400, y: 400)
CATransaction.commit()

Using CATransaction is especially handy when you need to apply the same timing changes to multiple implicit animations without repeating yourself.

The Presentation Layer

During an animation, the presentation() layer reflects the current visual state. You can query it for hit testing or to retrieve interpolated values at a given moment. Because the presentation layer is a temporary copy, always access it during a running animation and never store a strong reference beyond the animation’s lifetime.

let presentationLayer = myView.layer.presentation()
let currentX = presentationLayer?.position.x

CATransform3D for Perspective Effects

To create 3D‑inspired transformations, use CATransform3D with a perspective component. Setting the m34 subfield to a small negative value (e.g., -1.0 / 500.0) gives the illusion of depth when rotating a layer.

var transform = CATransform3DIdentity
transform.m34 = -1.0 / 500.0
let rotate = CABasicAnimation(keyPath: "transform")
rotate.toValue = CATransform3DRotate(transform, .pi, 0, 1, 0)
rotate.duration = 3.0
myView.layer.add(rotate, forKey: "3dRotate")

Combine this with CAKeyframeAnimation for card‑flip or page‑curl effects.

Best Practices and Performance Considerations

Core Animation is heavily optimized, but misusing it can lead to dropped frames and battery drain. Follow these guidelines to keep animations smooth:

  • Use hardware‑accelerated properties. Animating opacity, position, transform, and backgroundColor is always efficient because they are handled on the GPU. Avoid animating frame or bounds if possible; prefer transform for scaling and position for movement.
  • Avoid overloading the main thread. Core Animation dispatches rendering work to a separate render server process. However, creating many CPU‑intensive animations (e.g., drawing custom layers on every frame) can still block the main thread. Profile with Instruments and use the “Core Animation” template to detect off‑screen rendering and excessive blending.
  • Set shouldRasterize sparingly. Rasterizing complex layer trees can improve performance if the layer is static, but animating a rasterized layer forces it to be re‑rasterized each frame, defeating the purpose.
  • Use completion blocks or delegates to clean up. After an animation finishes, the added animation object is automatically removed. However, if you stop an animation early, ensure you remove it with layer.removeAnimation(forKey:) to prevent stale references.
  • Test on multiple devices. Animation performance varies significantly between older and newer iPhones. Use physical devices running iOS 16 or later to evaluate real‑world smoothness.

External Resources

To deepen your understanding, refer to these authoritative sources:

Conclusion

Core Animation is an indispensable tool for iOS developers who want to create custom, high‑performance animations. By understanding the layer model, mastering explicit animation classes, and adhering to performance best practices, you can bring your app’s interface to life with fluid motion that feels both intentional and delightful. Start with CABasicAnimation for simple transitions, explore CAKeyframeAnimation for complex paths, and group multiple animations when you need coordinated effects. With practice, you’ll be able to craft animations that are not only visually impressive but also maintain the responsiveness that users expect from modern iOS applications.