Build a slide-down menu in iOS using UIViewControllerAnimatedTransitioning with snapshot views and spring animations. A slide-down menu pushes the main content downward to reveal a menu behind it. You create a custom animator that slides the snapshot of the current view down while the menu view slides in from above. Snapshot views provide smooth performance by rendering the view hierarchy into a lightweight image.
What Is a Slide-Down Menu and How Does It Work?
A slide-down menu reveals navigation by moving the content view down vertically instead of sliding a panel horizontally over it. The content view animates downward a set distance, exposing the menu behind it.
The animation has two phases. Opening slides the content view down to reveal the menu. Closing slides the content view back up to its original position. The content view is not animated directly. A snapshot of the content view is used for the animation, leaving the actual view hierarchy untouched.
How to Create the MenuTransitionManager
Create a class that adopts UIViewControllerAnimatedTransitioning and UIViewControllerTransitioningDelegate. This single class handles both presentation and dismissal.
class MenuTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
let duration: TimeInterval = 0.5
var isPresenting = false
var snapshot: UIView?
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: .from),
let toView = transitionContext.view(forKey: .to)
else { return }
let container = transitionContext.containerView
if isPresenting {
snapshot = fromView.snapshotView(afterScreenUpdates: true)
guard let snapshot = snapshot else { return }
toView.transform = CGAffineTransform(translationX: 0, y: -container.frame.height)
container.addSubview(toView)
container.addSubview(snapshot)
}
guard let snapshot = snapshot else { return }
let moveDown = CGAffineTransform(translationX: 0, y: container.frame.height - 150)
UIView.animate(
withDuration: duration,
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.8,
animations: {
if self.isPresenting {
snapshot.transform = moveDown
toView.transform = .identity
} else {
snapshot.transform = .identity
toView.transform = CGAffineTransform(translationX: 0, y: -container.frame.height)
}
},
completion: { _ in
transitionContext.completeTransition(true)
if !self.isPresenting {
self.snapshot?.removeFromSuperview()
}
}
)
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresenting = true
return self
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresenting = false
return self
}
}
Why Use Snapshot Views for the Slide-Down Animation
snapshotView(afterScreenUpdates:) captures the current visual state of the content view as a lightweight UIView. Animating the snapshot is more performant than animating the actual view hierarchy. The snapshot is a rendered bitmap that does not trigger layout passes or redraws during animation.
The snapshot approach also preserves visual integrity. If the content view had ongoing animations or updates, the snapshot captures the exact frame. When the menu dismisses and the snapshot is removed, the original view reappears in its unchanged state.
How to Set Up the Slide-Down Menu in a Project
Wire the transition manager in prepare(for:sender:). Set the transitioningDelegate and modalPresentationStyle.
let menuTransitionManager = MenuTransitionManager()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showMenu" {
let menuVC = segue.destination as! MenuTableViewController
menuVC.transitioningDelegate = menuTransitionManager
menuVC.modalPresentationStyle = .fullScreen
}
}
Set modalPresentationStyle to .fullScreen. The iOS 13 default .automatic style presents modals as cards with interactive dismissal that interferes with custom transitions.
How to Add Tap-to-Dismiss Gesture on the Snapshot
Users expect to tap the content area behind the menu to dismiss it. Add a UITapGestureRecognizer to the snapshot view. Use a delegate protocol to communicate the dismiss action back to the presenting view controller.
protocol MenuTransitionManagerDelegate: AnyObject {
func dismiss()
}
class MenuTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
weak var delegate: MenuTransitionManagerDelegate?
var snapshot: UIView? {
didSet {
guard let snapshot = snapshot, let delegate = delegate else { return }
let tapGesture = UITapGestureRecognizer(target: delegate, action: #selector(delegate.dismiss))
snapshot.isUserInteractionEnabled = true
snapshot.addGestureRecognizer(tapGesture)
}
}
}
The delegate must use @objc for the dismiss method because UITapGestureRecognizer uses the Objective-C runtime.
extension NewsTableViewController: MenuTransitionManagerDelegate {
@objc func dismiss() {
dismiss(animated: true, completion: nil)
}
}
Set the delegate in prepare(for:sender:).
menuTransitionManager.delegate = self
How to Pass Data Between Menu and Content
Use prepare(for:sender:) to pass the current selection to the menu. Use unwind segues to return the selected menu item back to the content controller.
@IBAction func unwindToHome(segue: UIStoryboardSegue) {
guard let menuVC = segue.source as? MenuTableViewController else { return }
title = menuVC.selectedItem
}
The unwind segue dismisses the menu view controller, which triggers the reverse transition animation.
Practical Tips for Slide-Down Menus
Use snapshotView for any non-interactive content during transitions. Snapshots provide better performance than animating live view hierarchies.
Always call completeTransition(true) in the animation completion block. The system waits for this call and does not finalize the transition without it.
Clean up snapshots in the completion block when dismissing. Remove the snapshot from the container to avoid dead views in the hierarchy.
Set isUserInteractionEnabled to true on the snapshot for gesture recognition. Snapshots have user interaction disabled by default.
Use spring animations with damping 0.8 and velocity 0.8 for natural-feeling motion. The spring effect adds responsive bounce without looking cartoonish.
Test with VoiceOver enabled. Custom transitions can confuse assistive technologies. Ensure proper focus management and accessibility notifications.
Summary
This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.
Frequently Asked Questions
What iOS version is required? This tutorial targets iOS 13. Most of the APIs shown work on earlier versions as well, but some newer APIs may require the version specified in the article.
What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.