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.
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!
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.
Use the iPhone 8 Plus simulator for this tutorial so the starter views make sense.
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
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.
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) {
//1.
guard let maxiCard = storyboard?.instantiateViewController(
withIdentifier: "MaxiSongCardViewController")
as? MaxiSongCardViewController else {
assertionFailure("No view controller ID MaxiSongCardViewController in storyboard")
return
}
//2.
maxiCard.backingImage = view.makeSnapshot()
//3.
maxiCard.currentSong = song
//4.
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:
- Instantiate
MaxiSongCardViewController
from the storyboard. You use anassertionFailure
within theguard
statement to ensure you catch setup errors at design time. - Take a static image of the
SongViewController
and pass it to the new view controller.makeSnapshot
is a helper method provided with the project. - The selected
Song
object is passed to theMaxiSongCardViewController
instance - 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!
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.
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 {
//1.
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
//2.
dimmerLayer.alpha = dimmerAlpha
//3.
backingImageView.layer.cornerRadius = cornerRadius
}
//4.
private func animateBackingImage(presenting: Bool) {
UIView.animate(withDuration: primaryDuration) {
self.configureBackingImageInPosition(presenting: presenting)
self.view.layoutIfNeeded() //IMPORTANT!
}
}
//5.
func animateBackingImageIn() {
animateBackingImage(presenting: true)
}
func animateBackingImageOut() {
animateBackingImage(presenting: false)
}
}
Let’s go over this step-by-step:
- 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.
- 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. - You round off the corners of the image.
- 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 tolayoutIfNeeded()
within the block or the animation will not run. - 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(_:)
:
animateBackingImageIn()
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…
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.
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:
//DELETE THIS LATER
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.
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 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.
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
}
//1.
private var imageLayerInsetForOutPosition: CGFloat {
let imageFrame = view.convert(sourceView.originatingFrameInWindow, to: view)
let inset = imageFrame.minY - backingImageEdgeInset
return inset
}
//2.
func configureImageLayerInStartPosition() {
coverImageContainer.backgroundColor = startColor
let startInset = imageLayerInsetForOutPosition
dismissChevron.alpha = 0
coverImageContainer.layer.cornerRadius = 0
coverImageContainerTopInset.constant = startInset
view.layoutIfNeeded()
}
//3.
func animateImageLayerIn() {
//4.
UIView.animate(withDuration: primaryDuration / 4.0) {
self.coverImageContainer.backgroundColor = self.endColor
}
//5.
UIView.animate(withDuration: primaryDuration, delay: 0, options: [.curveEaseIn], animations: {
self.coverImageContainerTopInset.constant = 0
self.dismissChevron.alpha = 1
self.coverImageContainer.layer.cornerRadius = self.cardCornerRadius
self.view.layoutIfNeeded()
})
}
//6.
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
self.view.layoutIfNeeded()
})
}
}
Let’s go over this step-by-step:
- Get the start position based on the location of the source view, less the vertical offset of the scroll view.
- Place the container in its start position.
- Animate the container to its finished position.
- The first animation fades in the background color to avoid a sharp transition.
- The second animation changes the top inset of the container and fades the dismiss button in.
- 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(_:)
:
animateImageLayerIn()
This adds the animation to the timeline.
Next, add the following to the end of viewWillAppear(_:)
:
configureImageLayerInStartPosition()
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 {
//1.
func configureCoverImageInStartPosition() {
let originatingImageFrame = sourceView.originatingCoverImageView.frame
coverImageHeight.constant = originatingImageFrame.height
coverImageLeading.constant = originatingImageFrame.minX
coverImageTop.constant = originatingImageFrame.minY
coverImageBottom.constant = originatingImageFrame.minY
}
//2.
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
self.view.layoutIfNeeded()
})
}
//3.
func animateCoverImageOut() {
UIView.animate(withDuration: primaryDuration,
delay: 0,
options: [.curveEaseOut], animations: {
self.configureCoverImageInStartPosition()
self.view.layoutIfNeeded()
})
}
}
This code is similar to the image container animation from the previous section. Let’s go over this step-by-step:
- Place the cover image in its start position using information from the source view.
- 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.
- Animate the cover image back to its start position for the dismissal action.
Next, add the following to the end of viewDidAppear(_:)
:
animateCoverImageIn()
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
configureCoverImageInStartPosition()
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.
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) {
animateBackingImageOut()
animateCoverImageOut()
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.
Next, you’re going to setup a child view controller to display the song details by following the instructions below:
- Add a Container View as a subview of Scroll View.
- 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.
- Another view controller will be added with a segue connection. Delete that new view controller.
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.
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.
- Select the Container View.
- Open the Add New Constraints popover.
- Set Height to 400. Tick the height constraint.
- 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.
Close the assistant editor and add the following extension to the end of MaxiSongCardViewController.swift:
//lower module animation
extension MaxiSongCardViewController {
//1.
private var lowerModuleInsetForOutPosition: CGFloat {
let bounds = view.bounds
let inset = bounds.height - bounds.width
return inset
}
//2.
func configureLowerModuleInStartPosition() {
lowerModuleTopConstraint.constant = lowerModuleInsetForOutPosition
}
//3.
func animateLowerModule(isPresenting: Bool) {
let topInset = isPresenting ? 0 : lowerModuleInsetForOutPosition
UIView.animate(withDuration: primaryDuration,
delay:0,
options: [.curveEaseIn],
animations: {
self.lowerModuleTopConstraint.constant = topInset
self.view.layoutIfNeeded()
})
}
//4.
func animateLowerModuleOut() {
animateLowerModule(isPresenting: false)
}
//5.
func animateLowerModuleIn() {
animateLowerModule(isPresenting: true)
}
}
This extension performs a simple animation of the distance between SongPlayControlViewController
‘s view and the Image container as follows:
- Calculates an arbitrary distance to start from. The height of the view less the width is a good spot.
- Places the controller in its start position.
- Performs the animation in either direction.
- A helper method that animates the controller into place.
- Animates the controller out.
Now to add this animation to the timeline. First, add the following to the end of viewDidAppear(_:)
:
animateLowerModuleIn()
Next, add the following to the end of viewWillAppear(_:)
.
stretchySkirt.backgroundColor = .white //from starter project, this hides the gap
configureLowerModuleInStartPosition()
Next, add this line to dismissAction(_:)
before the call to animateImageLayerOut(completion:)
, for the dismissal animation:
animateLowerModuleOut()
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.
- 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 {
//1.
func configureBottomSection() {
if let image = tabBarImage {
bottomSectionHeight.constant = image.size.height
bottomSectionImageView.image = image
} else {
bottomSectionHeight.constant = 0
}
view.layoutIfNeeded()
}
//2.
func animateBottomSectionOut() {
if let image = tabBarImage {
UIView.animate(withDuration: primaryDuration / 2.0) {
self.bottomSectionLowerConstraint.constant = -image.size.height
self.view.layoutIfNeeded()
}
}
}
//3.
func animateBottomSectionIn() {
if tabBarImage != nil {
UIView.animate(withDuration: primaryDuration / 2.0) {
self.bottomSectionLowerConstraint.constant = 0
self.view.layoutIfNeeded()
}
}
}
}
This code is similar to the other animations. You’ll recognize all the sections.
- Set up the image view with the supplied image, or collapse to zero height in the case of no image.
- Drop the image view below the edge of the screen.
- 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(_:)
:
animateBottomSectionOut()
Next, add the following to the end of viewWillAppear(_:)
:
configureBottomSection()
Next, add the following to dismissAction(_:)
before the call to animateImageLayerOut(completion:)
:
animateBottomSectionIn()
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!
The post Recreating the Apple Music Now Playing transition appeared first on Ray Wenderlich.