Recreating the Apple Music Now Playing Transition

A common visual pattern in many iPhone apps are stacks of cards sliding in from the screen edge. You’ll recreate the Apple Music Now Playing Transition By Warren Burton.

Leave a rating/review
Save for later

A common visual pattern in many iPhone apps is stacks of cards that slide in from the edge of the screen. You can see this in apps like Reminders, where the lists are represented by a stack of cards that spring up from the bottom. The Music app does this as well, where the current song expands from a mini player to a full screen card.

These animations can seem simple when examined in a casual fashion. But if you look closer, you’ll see there’s actually many things happening that make up the animation. Good animations are like good special effects in movies: they should go almost unnoticed.

In this tutorial, you are going to reproduce the Music app’s transition from mini-player to full-screen card. To keep things clean, you’ll use ordinary UIKit APIs.

Apple Music Now Playing transition

To follow along with this tutorial, you’ll need the following:

  • Xcode 9.2 or later.
  • Familiarity with Auto Layout concepts.
  • Experience with creating and modifying UI and Auto Layout constraints within Interface Builder.
  • Experience with connecting IBOutlets in code to Interface Builder entities.
  • Experience with UIView animation APIs.

Getting Started

Download the starter project for this tutorial here.

Build and run the app. This app is RazePlayer, which provides a simple music catalog UI. Touch any song in the collection view to load the mini player at the bottom with that song. The mini player won’t actually play the song, which might be a good thing judging by the playlist!

start point

Introducing the Storyboard

The starter project includes a full set of semi-complete view controllers so you can spend your time concentrating on creating the animation. Open Main.storyboard in the Project navigator to see them.

prebuilt view controllers

Use the iPhone 8 Plus simulator for this tutorial so the starter views make sense.

use 8 plus simulator

Have a look at the storyboard from left to right:

  • Tab Bar Controller with SongViewController: This is the collection view you see when you launch the app. It has a repeating collection of fake songs.
  • Mini Player View Controller: This view controller is embedded as a child of SongViewController. This is the view you’ll be animating from.
  • Maxi Song Card View Controller: This view will display the final state of the animation. Along with the storyboard, it’s the class you’ll be working with most.
  • Song Play Control View Controller: You’ll use this as part of the animation.

Expand the project in the project navigator. The project uses a normal Model-View-Controller pattern to keep data logic outside of the view controllers. The file you’ll be using most frequently is Song.swift, which represents a single song from the catalog.

You can explore these files later if you’re curious, but you don’t need to know what’s inside for this tutorial. Instead, you’ll be working with the following files in the View Layer folder:

  • Main.storyboard: Contains all the UI for the project.
  • SongViewController.swift: The main view controller.
  • MiniPlayerViewController.swift: Shows the currently selected song.
  • MaxiSongCardViewController.swift: Displays the card animation from mini player to maxi player.
  • SongPlayControlViewController.swift: Provides extra UI for the animation.

Take a moment to examine the transition in Apple’s Music app from the mini player to the large card. The album art thumbnail animates continuously into a large image, and the tab bar animates down and away. It might be hard to spot all the effects that contribute to this animation in real time. Fortunately, you’ll animate things in slow motion as you recreate this animation.

Your first task will be to jump from the mini player to the full-screen card.

Animating the Background Card

iOS animations often involve smoke and mirrors that fool users’ eyes into thinking what they are seeing is real. Your first task will be to make it appear the underlying content shrinks.

Creating a Fake Background

Open Main.storyboard and expand Maxi Song Card View Controller. The two views you’re going to work with are Backing Image View and Dimmer Layer

layers to work with in this section

Open MaxiSongCardViewController.swift and add the following properties to the class, below the dimmerLayer outlet:

//add backing image constraints here
@IBOutlet weak var backingImageTopInset: NSLayoutConstraint!
@IBOutlet weak var backingImageLeadingInset: NSLayoutConstraint!
@IBOutlet weak var backingImageTrailingInset: NSLayoutConstraint!
@IBOutlet weak var backingImageBottomInset: NSLayoutConstraint!

Next, open Main.storyboard in the assistant editor by holding down the Option key and clicking Main.storyboard in the project navigator. You should now have MaxiSongCardViewController.swift open on the left and Main.storyboard on the right. The other way ’round is OK too if you’re in the southern hemisphere.

connect outlets in code to interface builder

Next, connect the backing image IBOutlet's to the storyboard objects as shown below:

  • Expand the top level view of MaxiSongCardViewController and its top level constraints.
  • Connect backingImageTopInset to the top constraint of the Backing Image View.
  • Connect backingImageBottomInset to the bottom constraint of the Backing Image View.
  • Connect backingImageLeadingInset to the leading constraint of the Backing Image View.
  • Connect backingImageTrailingInset to the trailing constraint of the Backing Image View.

You’re now ready to present MaxiSongCardViewController. Close the assistant editor by pressing Cmd + Return or, alternately, View ▸ Standard Editor ▸ Show Standard Editor.

Open SongViewController.swift. First, add the following extension to the bottom of the file:

extension SongViewController: MiniPlayerDelegate {
  func expandSong(song: Song) {
    guard let maxiCard = storyboard?.instantiateViewController(
              withIdentifier: "MaxiSongCardViewController") 
              as? MaxiSongCardViewController else {
      assertionFailure("No view controller ID MaxiSongCardViewController in storyboard")
    maxiCard.backingImage = view.makeSnapshot()
    maxiCard.currentSong = song
    present(maxiCard, animated: false)

When you tap the mini player, it delegates that action back up to the SongViewController. The mini player should neither know nor care what happens to that action.

Let’s go over this step-by-step:

  1. Instantiate MaxiSongCardViewController from the storyboard. You use an assertionFailure within the guard statement to ensure you catch setup errors at design time.
  2. Take a static image of the SongViewController and pass it to the new view controller. makeSnapshot is a helper method provided with the project.
  3. The selected Song object is passed to the MaxiSongCardViewController instance
  4. Present the controller modally with no animation. The presented controller will own its animation sequence.

Next, find the function prepare(for:sender:) and add the following line after miniPlayer = destination:

miniPlayer?.delegate = self

Build and run app, select a song from the catalog, then touch the mini player. You should get an instant blackout. Success!

maxi player has been presented

You can see the status bar has vanished. You’ll fix that now.

Changing the Status Bar’s Appearance

The presented controller has a dark background, so you’re going to use a light style for the status bar instead. Open MaxiSongCardViewController.swift and add the following code to the MaxiSongCardViewController class;

override var preferredStatusBarStyle: UIStatusBarStyle {
  return .lightContent

Build and run app, tap a song then tap the mini player to present the MaxiSongCardViewController. The status bar will now be white-on-black.

status bar now appears correctly

The last task in this section is to create the illusion of the controller falling away to the background.

Shrinking the View Controller

Open MaxiSongCardViewController.swift and add the following properties to the top of the class:

let primaryDuration = 4.0 //set to 0.5 when ready
let backingImageEdgeInset: CGFloat = 15.0

This provides the duration for the animation as well as the inset for the backing image. You can speed up the animation later, but for now it will run quite slowly so you can see what’s happening.

Next, add the following extension to the end of the file:

//background image animation
extension MaxiSongCardViewController { 
  private func configureBackingImageInPosition(presenting: Bool) {
    let edgeInset: CGFloat = presenting ? backingImageEdgeInset : 0
    let dimmerAlpha: CGFloat = presenting ? 0.3 : 0
    let cornerRadius: CGFloat = presenting ? cardCornerRadius : 0
    backingImageLeadingInset.constant = edgeInset
    backingImageTrailingInset.constant = edgeInset
    let aspectRatio = backingImageView.frame.height / backingImageView.frame.width
    backingImageTopInset.constant = edgeInset * aspectRatio
    backingImageBottomInset.constant = edgeInset * aspectRatio
    dimmerLayer.alpha = dimmerAlpha
    backingImageView.layer.cornerRadius = cornerRadius
  private func animateBackingImage(presenting: Bool) {
    UIView.animate(withDuration: primaryDuration) {
      self.configureBackingImageInPosition(presenting: presenting)
      self.view.layoutIfNeeded() //IMPORTANT!
  func animateBackingImageIn() {
    animateBackingImage(presenting: true)
  func animateBackingImageOut() {
    animateBackingImage(presenting: false)

Let’s go over this step-by-step:

  1. Set the desired end position of the image frame. You correct the vertical insets with the aspect ratio of the image so the image doesn’t look squashed.
  2. The dimmer layer is a UIView above the Image View with a black background color. You set the alpha on this to dim the image slightly.
  3. You round off the corners of the image.
  4. Using the simplest UIView animation API, you tell the image view to animate into its new layout. When animating Auto Layout constraints you must make a call to layoutIfNeeded() within the block or the animation will not run.
  5. Provide public accessors to keep your code clean.

Next, add the following to viewDidLoad() after the call to super:

backingImageView.image = backingImage

Here you install the snapshot you passed through from SongViewController previously.

Finally add the following to the end of viewDidAppear(_:):


Once the view appears, you tell the animation to start.

Build and run the app, select a song and then touch the mini player. You should see the current view controller receding into the background…very…slowly…

backing image recedes and dims

Awesome stuff! That takes care of one part of the sequence. The next significant part of the animation is growing the thumbnail image in the mini player into the large top image of the card.

Growing the Song Image

Open Main.storyboard and expand its view hierarchy again.

views to work with in this section

You’re going to be focusing on the following views:

  • Cover Image Container: This is a UIView with a white background. You’ll be animating its position in the scroll view.
  • Cover Art Image: This is the UIImageView you’re going to transition. It has a yellow background so it’s easier to see and grab in Xcode. Note the following two things about this view:
    • The Aspect Ratio is set to 1:1. This means it’s always a square.
    • The height is constrained to a fixed value. You’ll learn why this is in just a bit.

Open MaxiSongCardViewController.swift. You can see the outlets for the two views and dismiss button are already connected:

//cover image
@IBOutlet weak var coverImageContainer: UIView!
@IBOutlet weak var coverArtImage: UIImageView!
@IBOutlet weak var dismissChevron: UIButton!

Next, find viewDidLoad(), and delete the following lines:

scrollView.isHidden = true

This makes the UIScrollView visible. It was hidden previously so you could see what was going on with the background image.

Next, add the following lines to the end of viewDidLoad():

coverImageContainer.layer.cornerRadius = cardCornerRadius
coverImageContainer.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]

This sets corner radii for the top two corners only.

Build and run the app, tap the mini player and you’ll see you now see the container view and image view displayed above the background image snapshot.

Also notice that the image view has rounded corners. This was accomplished without code; instead, it was done via the User Defined Runtime Attributes panel.

you can use user defined attributes within Interface Builder

Configuring the Cover Image Constraints

In this part you are going to add the constraints needed to animate the cover image display.

Open MaxiSongCardViewController.swift. Next, add the following constraints:

//cover image constraints
@IBOutlet weak var coverImageLeading: NSLayoutConstraint!
@IBOutlet weak var coverImageTop: NSLayoutConstraint!
@IBOutlet weak var coverImageBottom: NSLayoutConstraint!
@IBOutlet weak var coverImageHeight: NSLayoutConstraint!

Next, open Main.storyboard in the assistant editor and connect the outlets as follows:

Connect cover image constraint outlets

  • Connect coverImageLeading, coverImageTop and coverImageBottom to the leading, top and bottom constraints of the Image View.
  • Connect coverImageHeight to the height constraint of the Image View.

The last constraint to add is the distance from the top of the cover image container to the content view of the scroll view.

Open MaxiSongCardViewController.swift. Next, add the following property to the class declaration:

//cover image constraints
@IBOutlet weak var coverImageContainerTopInset: NSLayoutConstraint!

Finally, connect coverImageContainerTopInset to the top inset of the cover image container; this is the constraint with the constant parameter of 57, visible in Interface Builder.

connect top inset outlet for container

Now all the constraints are set up to perform the animation.

Build and run the app; tap a song then tap the mini player to make sure everything is working fine.

Creating a Source Protocol

You need to know the starting point for the animation of the cover image. You could pass a reference of the mini player to the maxi player to derive all the necessary information to perform this information, but that would create a hard dependency between MiniPlayerViewController and MaxiSongCardViewController. Instead, you’ll add a protocol to pass the information.

Close the assistant editor and add the following protocol to the top of MaxiSongCardViewController.swift:

protocol MaxiPlayerSourceProtocol: class {
  var originatingFrameInWindow: CGRect { get }
  var originatingCoverImageView: UIImageView { get }

Next, open MiniPlayerViewController.swift and add the following code at the end of the file:

extension MiniPlayerViewController: MaxiPlayerSourceProtocol {
  var originatingFrameInWindow: CGRect {
    let windowRect = view.convert(view.frame, to: nil)
    return windowRect
  var originatingCoverImageView: UIImageView {
    return thumbImage

This defines a protocol to express the information the maxi player needs to animate. You then made MiniPlayerViewController conform to that protocol by supplying that information. UIView has built in conversion methods for rectangles and points that you’ll use a lot.

Next, open MaxiSongCardViewController.swift and add the following property to the main class:

weak var sourceView: MaxiPlayerSourceProtocol!

The reference here is weak to avoid retain cycles.

Open SongViewController.swift and add the following line to expandSong before the call to present(_, animated:):

maxiCard.sourceView = miniPlayer

Here you pass the source view reference to the maxi player at instantiation.

Animating in From the Source

In this section, you’re going to glue all your hard work together and animate the image view into place.

Open MaxiSongCardViewController.swift. Add the following extension to the file:

//Image Container animation.
extension MaxiSongCardViewController {
  private var startColor: UIColor {
    return UIColor.white.withAlphaComponent(0.3)
  private var endColor: UIColor {
    return .white
  private var imageLayerInsetForOutPosition: CGFloat {
    let imageFrame = view.convert(sourceView.originatingFrameInWindow, to: view)
    let inset = imageFrame.minY - backingImageEdgeInset
    return inset
  func configureImageLayerInStartPosition() {
    coverImageContainer.backgroundColor = startColor
    let startInset = imageLayerInsetForOutPosition
    dismissChevron.alpha = 0
    coverImageContainer.layer.cornerRadius = 0
    coverImageContainerTopInset.constant = startInset
  func animateImageLayerIn() {
    UIView.animate(withDuration: primaryDuration / 4.0) {
      self.coverImageContainer.backgroundColor = self.endColor
    UIView.animate(withDuration: primaryDuration, delay: 0, options: [.curveEaseIn], animations: {
      self.coverImageContainerTopInset.constant = 0
      self.dismissChevron.alpha = 1
      self.coverImageContainer.layer.cornerRadius = self.cardCornerRadius
  func animateImageLayerOut(completion: @escaping ((Bool) -> Void)) {
    let endInset = imageLayerInsetForOutPosition
    UIView.animate(withDuration: primaryDuration / 4.0,
                   delay: primaryDuration,
                   options: [.curveEaseOut], animations: {
      self.coverImageContainer.backgroundColor = self.startColor
    }, completion: { finished in
      completion(finished) //fire complete here , because this is the end of the animation
    UIView.animate(withDuration: primaryDuration, delay: 0, options: [.curveEaseOut], animations: {
      self.coverImageContainerTopInset.constant = endInset
      self.dismissChevron.alpha = 0
      self.coverImageContainer.layer.cornerRadius = 0

Let’s go over this step-by-step:

  1. Get the start position based on the location of the source view, less the vertical offset of the scroll view.
  2. Place the container in its start position.
  3. Animate the container to its finished position.
  4. The first animation fades in the background color to avoid a sharp transition.
  5. The second animation changes the top inset of the container and fades the dismiss button in.
  6. Animate the container back to its start position. You’ll use this later. It reverses the animateImageLayerIn sequence.

Next, add the following to the end of viewDidAppear(_:):


This adds the animation to the timeline.

Next, add the following to the end of viewWillAppear(_:):


Here you set up the start position before the view appears. This lives in viewWillAppear so the change in start position of the image layer isn’t seen by the user.

Build and run the app, and tap the mini player to present the maxi player. You’ll see the container rise into place. It won’t change shape just yet because the container depends on the height of the image view.


Your next task is to add the shape change and animate the image view into place.

Animating From the Source Image

Open MaxiSongCardViewController.swift and add the following extension to the end of the file:

//cover image animation
extension MaxiSongCardViewController {
  func configureCoverImageInStartPosition() {
    let originatingImageFrame = sourceView.originatingCoverImageView.frame
    coverImageHeight.constant = originatingImageFrame.height
    coverImageLeading.constant = originatingImageFrame.minX
    coverImageTop.constant = originatingImageFrame.minY
    coverImageBottom.constant = originatingImageFrame.minY
  func animateCoverImageIn() {
    let coverImageEdgeContraint: CGFloat = 30
    let endHeight = coverImageContainer.bounds.width - coverImageEdgeContraint * 2
    UIView.animate(withDuration: primaryDuration, delay: 0, options: [.curveEaseIn], animations:  {
      self.coverImageHeight.constant = endHeight
      self.coverImageLeading.constant = coverImageEdgeContraint
      self.coverImageTop.constant = coverImageEdgeContraint
      self.coverImageBottom.constant = coverImageEdgeContraint
  func animateCoverImageOut() {
    UIView.animate(withDuration: primaryDuration,
                   delay: 0,
                   options: [.curveEaseOut], animations:  {

This code is similar to the image container animation from the previous section. Let’s go over this step-by-step:

  1. Place the cover image in its start position using information from the source view.
  2. Animate the cover image into its end position. The end height is the container width less its insets. Since the aspect ratio is 1:1, that will be its width as well.
  3. Animate the cover image back to its start position for the dismissal action.

Next, add the following to the end of viewDidAppear(_:):


This fires off the animation once the view is on screen.

Next, add the following lines to the end of viewWillAppear(_:):

coverArtImage.image = sourceView.originatingCoverImageView.image

This uses the UIImage from the source to populate the image view. It works in this particular case, because the UIImage has sufficient resolution so the image will not appear pixelated or stretched.

Build and run the app, the image view now grows from the source thumbnail and changes the frame of the container view at the same time.

image view controls the height of the container

Adding the Dismissal Animations

The button at the top of the card is connected to dismissAction(_:). Currently, it simply performs a modal dismiss action with no animation.

Just like you did when presenting the view controller, you want MaxiSongCardViewController to handle its own dismiss animation.

Open MaxiSongCardViewController.swift and replace dismissAction(_:) with the following:

@IBAction func dismissAction(_ sender: Any) {
  animateImageLayerOut() { _ in
    self.dismiss(animated: false)

This plays out the reverse animations that you set up previously in animating from source image. Once the animations have completed, you dismiss the MaxiSongCardViewController.

Build and run the app, bring up the maxi player and touch the dismiss control. The cover image and container view reverse back into the mini player. The only visible evidence of the dismissal is the Tab bar flickering in. You’ll fix this soon.

Displaying Song Information

Have a look at the Music app again and you’ll notice the expanded card contains a scrubber and volume control, information about the song, artist, album and upcoming tracks. This isn’t all contained in one single view controller — it’s built from components.

Your next task will be to embed a view controller in the scroll view. To save you time, there’s a controller all ready for you: SongPlayControlViewController.

Embedding the Child Controller

The first task is to detach the bottom of the image container from the scroll view.

Open Main.storyboard. Delete the constraint which binds the bottom of the cover image container to the bottom of the superview. You’ll get some red layout errors that the scroll view needs constraints for Y position or height. That’s OK.

detach the bottom of the image container from the scroll view content

Next, you’re going to setup a child view controller to display the song details by following the instructions below:

  1. Add a Container View as a subview of Scroll View.
  2. Ensure the Container View is above Stretchy Skirt in the view hierarchy (which requires it be below the Stretchy Skirt view in the Interface Builder Document Outline.
  3. Another view controller will be added with a segue connection. Delete that new view controller.

drag Container View from Object library to object hierarchy

Now add the following constraints to the new container view:

  • Leading, trailing and bottom. Pin to the scroll view and make them equal to 0.
  • Top to Cover Image Container bottom = 30

You may find it helpful to first adjust the view’s Y position, so that it is positioned below the image container view where it will be easier to define the constraints.

add edge constraints to Container View

Lastly, bind the Container View embed segue to the SongPlayControlViewController. Hold down Control and drag from the container view to SongPlayControlViewController.

Release the mouse, and choose Embed from the menu that appears.

Finally, constrain the height of the Container view within the scroll view to unambiguously define the height of the scroll view’s content.

  1. Select the Container View.
  2. Open the Add New Constraints popover.
  3. Set Height to 400. Tick the height constraint.
  4. Press Add 1 Constraint.

At this stage, all the Auto Layout errors should be gone.

Animating the Controls

The next effect will raise the controls from the bottom of the screen to join the cover image at the end of the animation.

Open MaxiSongCardViewController.swift in the standard editor and Main.storyboard in the assistant editor.

Add the following property to the main class of MaxiSongCardViewController:

//lower module constraints
@IBOutlet weak var lowerModuleTopConstraint: NSLayoutConstraint!

Attach the outlet to the constraint separating the image container and the Container View.

connect top constraint outlet

Close the assistant editor and add the following extension to the end of MaxiSongCardViewController.swift:

//lower module animation
extension MaxiSongCardViewController {
  private var lowerModuleInsetForOutPosition: CGFloat {
    let bounds = view.bounds
    let inset = bounds.height - bounds.width
    return inset
  func configureLowerModuleInStartPosition() {
    lowerModuleTopConstraint.constant = lowerModuleInsetForOutPosition
  func animateLowerModule(isPresenting: Bool) {
    let topInset = isPresenting ? 0 : lowerModuleInsetForOutPosition
    UIView.animate(withDuration: primaryDuration,
                   options: [.curveEaseIn],
                   animations: {
      self.lowerModuleTopConstraint.constant = topInset
  func animateLowerModuleOut() {
    animateLowerModule(isPresenting: false)
  func animateLowerModuleIn() {
    animateLowerModule(isPresenting: true)

This extension performs a simple animation of the distance between SongPlayControlViewController‘s view and the Image container as follows:

  1. Calculates an arbitrary distance to start from. The height of the view less the width is a good spot.
  2. Places the controller in its start position.
  3. Performs the animation in either direction.
  4. A helper method that animates the controller into place.
  5. Animates the controller out.

Now to add this animation to the timeline. First, add the following to the end of viewDidAppear(_:):


Next, add the following to the end of viewWillAppear(_:).

stretchySkirt.backgroundColor = .white //from starter project, this hides the gap 

Next, add this line to dismissAction(_:) before the call to animateImageLayerOut(completion:), for the dismissal animation:


Finally, add the following to MaxiSongCardViewController.swift to pass the current song across to the new controller.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if let destination = segue.destination as? SongSubscriber {
    destination.currentSong = currentSong

This checks if the destination conforms to SongSubscriber then passes the song across. This is a simple demonstration of dependency injection.

Build and run the app. Present the maxi player and you’ll see the SongPlayControl’s view rise into place.

Hiding the Tab Bar

The last thing to do before you finish is to deal with the Tab bar. You could possibly hack the frame of the tab bar, but that would create some messy interactions with the active view controller frame. Instead, you’ll need a bit more smoke and a few more mirrors:

  • Take a snapshot image of the Tab bar.
  • Pass it through to the MaxiSongCardViewController.
  • Animate the tab bar snapshot image.

First, add the following to MaxiSongCardViewController:

//fake tabbar contraints
var tabBarImage: UIImage?
@IBOutlet weak var bottomSectionHeight: NSLayoutConstraint!
@IBOutlet weak var bottomSectionLowerConstraint: NSLayoutConstraint!
@IBOutlet weak var bottomSectionImageView: UIImageView!

Next, open Main.storyboard and drag an Image View into the MaxiSongCardViewController view hierarchy. You want it to be above the scroll view in the view hierarchy (which means below it, in Interface Builder’s navigator).

Using the Add Constraints popover, Untick Constrain to margins. Pin its leading, trailing and bottom edges to the superview with size 0. This will, in fact, pin to the safe area. Add a height constraint of 128, and press Add 4 Constraints to commit the changes.

Next, open MaxiSongCardViewController.swift in the assistant editor and connect the three properties you added to the Image view.

connect tab bar image view to outlets

  • bottomSectionImageView connects to the Image View.
  • bottomSectionLowerConstraint connects to the Bottom constraint.
  • bottomSectionHeight connects to the height constraint.

Finally, close the assistant editor, and add the following extension to the end of MaxiSongCardViewController.swift:

//fake tab bar animation
extension MaxiSongCardViewController {
  func configureBottomSection() {
    if let image = tabBarImage {
      bottomSectionHeight.constant = image.size.height
      bottomSectionImageView.image = image
    } else {
      bottomSectionHeight.constant = 0
  func animateBottomSectionOut() {
    if let image = tabBarImage {
      UIView.animate(withDuration: primaryDuration / 2.0) {
        self.bottomSectionLowerConstraint.constant = -image.size.height
  func animateBottomSectionIn() {
    if tabBarImage != nil {
      UIView.animate(withDuration: primaryDuration / 2.0) {
        self.bottomSectionLowerConstraint.constant = 0

This code is similar to the other animations. You’ll recognize all the sections.

  1. Set up the image view with the supplied image, or collapse to zero height in the case of no image.
  2. Drop the image view below the edge of the screen.
  3. Lift the image view back into the normal position.

The last thing to do in this file is add the animations to the timeline.

First, add the following to the end of viewDidAppear(_:):


Next, add the following to the end of viewWillAppear(_:):


Next, add the following to dismissAction(_:) before the call to animateImageLayerOut(completion:):


Next, open SongViewController.swift and add the following code before the call to present(animated:) in expandSong(song:):

if let tabBar = tabBarController?.tabBar {
  maxiCard.tabBarImage = tabBar.makeSnapshot()

Here you take a snapshot of the Tab bar, if it exists, and then pass it through to MaxiSongCardViewController.

Finally, open MaxiSongCardViewController.swift and change the primaryDuration property to 0.5 so you don’t have to be tortured by the slow animations anymore!

Build and run the app, present the maxi player, and the tab bar will rise and fall into place naturally.

Congratulations! You’ve just completed a recreation of the card animation that closely resembles the one in the Music app.

Where to Go From Here

You can download the finished version of the project here.

In this tutorial, you learned all about the following:

  • Animating Auto Layout constraints.
  • Placing multiple animations into a timeline to composite a complex sequence.
  • Using static snapshots of views to create the illusion of change.
  • Using the delegate pattern to create weak bindings between objects.

Note that the method of using a static snapshot would not work where the underlying view changes while the card is being presented, such as in the case where an asynchronous event causes a reload.

Animations are costly in terms of development time, and they’re hard to get just right. However, it’s usually worth the effort, as they add an extra element of delight and can turn an ordinary app into an extraordinary one.

Hopefully this tutorial has triggered some ideas for your own animations. If you have any comments or questions, or want to share your own creations, come join the discussion below!