statics-and-dynamics
Creating Custom Transitions and Animations in Ios Uikit
Table of Contents
Introduction to UIKit Animations and Transitions
User interfaces that respond fluidly to user actions are the hallmark of a polished iOS application. UIKit provides a rich set of APIs for creating custom transitions and animations, enabling developers to guide attention, communicate state changes, and deliver a memorable user experience. This article goes beyond basic UIView.animate calls to explore custom view controller transitions, interactive animations, and best practices that professional iOS teams use daily.
Animations in UIKit are not just cosmetic; they serve functional purposes such as orienting users during navigation, indicating loading states, or providing feedback for gestures. By mastering both simple property animations and the more complex transitioning protocol, you can build apps that feel both intuitive and delightful.
Core Animation APIs in UIKit
At the heart of UIKit animation are two primary approaches: view-based animations using closures and layer-based animations with Core Animation (CABasicAnimation, CAKeyframeAnimation). UIKit’s UIView.animate family is recommended for most simple cases because it handles property changes automatically and integrates with the view’s layout system.
UIView.animate and UIViewPropertyAnimator
The classic UIView.animate(withDuration:delay:options:animations:completion:) is straightforward for single-step animations. However, for more control over timing curves, interruption, and interactive control, the newer UIViewPropertyAnimator is preferred. Introduced in iOS 10, UIViewPropertyAnimator allows you to scrub an animation forward and backward, reverse it, and even add blocks dynamically. Here’s a minimal example that fades and moves a view:
Swift code (UIViewPropertyAnimator):
let animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeInOut) {
view.alpha = 0.0
view.center.x += 100
}
animator.startAnimation()
This API is especially useful when building interactive transitions because you can fractionComplete property to map gesture progress to animation progress.
Spring and Keyframe Animations
UIKit provides built-in spring damping via UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:). A damping value between 0 and 1 creates a bouncy effect, ideal for buttons or appearance animations. For complex multi‑stage animations, UIView.animateKeyframes lets you sequence or overlap changes.
Example of a spring animation on a button press:
Swift code:
UIView.animate(
withDuration: 0.6,
delay: 0,
usingSpringWithDamping: 0.5,
initialSpringVelocity: 0.8,
options: [.allowUserInteraction],
animations: {
button.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
},
completion: { _ in
UIView.animate(withDuration: 0.3) {
button.transform = .identity
}
}
)
Custom View Controller Transitions
Custom transitions between view controllers remain one of the most impactful ways to differentiate your app. UIKit’s transitioning architecture consists of two key components: an animator object conforming to UIViewControllerAnimatedTransitioning, and a delegate object that provides that animator for presentation and dismissal.
Building the Animated Transitioning Object
Create a class that adopts UIViewControllerAnimatedTransitioning. The two required methods are transitionDuration(using:) and animateTransition(using:). Inside animateTransition(using:), you access the UIViewControllerContextTransitioning object to obtain the from‑view, to‑view, and the container view where the animation happens.
Example: A slide‑in animator from the right edge
class SlideRightAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to) else { return }
let container = transitionContext.containerView
let finalFrame = transitionContext.finalFrame(for: transitionContext.viewController(forKey: .to)!)
toView.frame = finalFrame.offsetBy(dx: container.bounds.width, dy: 0)
container.addSubview(toView)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 0.9,
initialSpringVelocity: 0.5,
options: .curveEaseOut,
animations: {
toView.frame = finalFrame
},
completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
}
}
Always call completeTransition(:) after the animation finishes to avoid memory leaks and layout issues.
Setting Up the Transitioning Delegate
The presenting or presented view controller’s transitioningDelegate property must be set to an object that conforms to UIViewControllerTransitioningDelegate. For presentation, implement animationController(forPresented:presenting:source:); for dismissal, implement animationController(forDismissed:). You can also provide separate animators for presentation and dismissal.
Delegate implementation:
class CustomTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideRightAnimator()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideRightAnimator() // or a different dismiss animator
}
}
Assign this delegate to the presented view controller before presenting it:
let detailVC = DetailViewController()
detailVC.transitioningDelegate = customDelegate
detailVC.modalPresentationStyle = .custom
present(detailVC, animated: true, completion: nil)
Interactive Transitions with UIPercentDrivenInteractiveTransition
To make transitions interactive—driven by a gesture, for example—create a subclass of UIPercentDrivenInteractiveTransition. This object manages the synchronization of the animation with user input. You must also implement interactionControllerForPresentation(using:) and interactionControllerForDismissal(using:) in your delegate.
In your gesture recognizer handler, update the transition’s progress:
let interactiveTransition = UIPercentDrivenInteractiveTransition()
@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: view)
let progress = min(max(abs(translation.x) / view.bounds.width, 0), 1)
switch gesture.state {
case .began:
// Start presentation or dismissal
presentedVC?.dismiss(animated: true, completion: nil)
case .changed:
interactiveTransition.update(progress)
case .cancelled, .ended:
if progress > 0.5 {
interactiveTransition.finish()
} else {
interactiveTransition.cancel()
}
default:
break
}
}
Interactive transitions provide the most natural feel for swipe‑to‑dismiss or custom navigation gestures.
Advanced Animation Techniques
Beyond basic presentation, UIKit supports animations that respond to view layout changes (via UIView.animate in viewWillLayoutSubviews), collection view cell animations, and even animating along a path with CAKeyframeAnimation. Here are a few patterns seen in production apps.
View Controller Transition Coordinators
When a view controller transition occurs, the transitionCoordinator property on the view controller returns an object that lets you run additional animations alongside the main transition. This is perfect for animating navigation bar elements, background views, or interactive controls.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let coordinator = transitionCoordinator else { return }
coordinator.animate(alongsideTransition: { context in
// Animations that run parallel to the transition
self.headerView.alpha = 1.0
}, completion: { context in
if context.isCancelled {
self.headerView.alpha = 0.0
}
})
}
Animating with UIViewPropertyAnimator and Custom Timing
For fine‑grained control over animation curves, use UITimingCurveProvider, such as UISpringTimingParameters. You can create a property animator with a custom spring:
let springTiming = UISpringTimingParameters(dampingRatio: 0.7, frequencyResponse: 0.3)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: springTiming)
animator.addAnimations {
view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
}
animator.startAnimation()
The duration parameter is ignored when using springs; the actual duration is derived from the timing parameters.
Layer‑Level Animations with CABasicAnimation
When you need to animate properties not exposed by UIKit (like cornerRadius frame changes, or shadow paths), use Core Animation directly. This is also how many custom path‑based transitions are built.
let cornerAnimation = CABasicAnimation(keyPath: "cornerRadius")
cornerAnimation.fromValue = 20
cornerAnimation.toValue = 0
cornerAnimation.duration = 0.4
view.layer.add(cornerAnimation, forKey: "cornerAnimation")
view.layer.cornerRadius = 0 // set model value to final state
Performance and Debugging
Smooth animations require careful attention to performance. The most common pitfalls include animating layout‑dependent properties that trigger expensive relayouts, excessive off‑screen rendering, and forgetting to set layer.shouldRasterize appropriately.
Using the Debug Gauges and Instruments
Xcode’s Core Animation instrument lets you monitor frame rates (fps) and detect dropped frames. Enable the “Color Blended Layers” and “Color Hits Green and Misses Red” options in the simulator or device debug options to visualize compositing bottlenecks. Reduce overdraw by ensuring views are opaque where possible and by setting layer.opaque = true.
Reducing Layout Passes
Animating frame or center often works fine, but animating transform is cheaper because it operates on the backing layer without triggering Auto Layout. For complex layouts, prefer transform over frame adjustments. Also avoid modifying constraints inside animation blocks; instead, use layoutIfNeeded inside the animation after changing constraints.
Best Practices for Custom Transitions and Animations
- Keep duration between 0.25 and 0.5 seconds for most UI transitions. Longer animations feel sluggish; shorter may be missed.
- Use spring animations for natural bounce effects but avoid over‑doing it. A damping ratio around 0.8 gives subtle bounce.
- Respect user preferences. Check
UIAccessibility.isReduceMotionEnabledand disable or replace animations accordingly. - Test on low‑end devices (e.g., iPhone SE, older iPads). An animation that runs at 60 fps on an iPhone 15 may drop frames on an iPhone 8.
- Always call
completeTransition(_:)in custom animators. Failure to do so can leave the view controller hierarchy in an inconsistent state. - Use
allowUserInteractionoption when animating views that the user may need to tap repeatedly (e.g., button scale animations). - Provide a fallback for interactive transitions if the gesture is cancelled. Use
cancel()with a complimentary animation.
Real‑World Examples
Many popular apps employ custom transitions: Mail uses a push‑slide for reading messages, Photos uses zoom‑in presentation, and many third‑party apps implement swipe‑to‑dismiss with interactive feedback. You can build similar effects by combining UIPercentDrivenInteractiveTransition with a pan gesture and a custom animator that applies scale and opacity changes based on gesture distance.
For further reading, refer to Apple’s official documentation on View Controller Animations and Transitions. Also check out the WWDC sessions on Building Better Animations (WWDC 2016) and Customizing Transition Animations.
Example: Interactive Card‑Style Presentation
Imagine a music app where tapping a track opens a detail card that slides up from the bottom. The card can be dismissed by swiping down. To build this:
- Create a
CardAnimatorconforming toUIViewControllerAnimatedTransitioningthat translates the presented view from below the screen to its final position. - Create a
CardInteractionControlleras a subclass ofUIPercentDrivenInteractiveTransition. - In the presented view controller, add a
UIPanGestureRecognizerthat drives dismissal progress. - Implement
interactionControllerForDismissalin the delegate to return your interactive controller.
This pattern produces a fluid, gesture‑driven experience that feels native.
Summary
Custom transitions and animations are a powerful tool to improve your iOS app’s design and usability. By leveraging UIViewControllerAnimatedTransitioning, UIViewPropertyAnimator, and interactive driving classes, you can create unique, gesture‑driven navigation that delights users. Remember to keep performance in mind, respect accessibility settings, and test across devices. With practice, you’ll be able to implement animations that feel both custom and polished.