9.
Animating Constraints
Written by Marin Todorov
In the previous chapter, you learned how to use Auto Layout to create a responsive user interface for your Packing List project. You’ll take it up a notch in this chapter and add a number of bouncy animations to your app. You’ve learned that in order for Auto Layout to work properly, you can’t fiddle directly with the view’s frame or center properties. Instead, you have to work with the layout constraints to create your desired animations.
So far you’ve seen how to animate view properties: you can animate numeric properties, such as alpha, from one float value to another. Instances of CGPoint, like in the case of the center property, can be modified progressively until the center value reaches the target position. Naturally, your next question would be “That’s great — but how do I animate a constraint?”
Animating constraints is no more difficult than animating properties; it’s just a little different. Usually you simply replace an existing constraint with a new one and let Auto Layout animate the UI between the two states.
The sole exception is when you only need to change a single property of the constraint, such as constant, in the constraint’s equation. In that case, you simply modify the constraint directly in code and animate the change.
In this chapter, you’ll add an animation to expand the Packing List menu bar and reveal a list of items; the user can then tap an item to add it to their packing list as shown below:
This interaction — as well as a few more visual treats — will be driven by fluid and eye-catching animations.
What are you waiting for? Time to get packing!
Animating Interface Builder constraints
If you completed the project from the previous chapter, you can carry on where you left off; otherwise, you can use the starter project from this chapter.
Your first task is to expand the menu when the user taps the + button. In order to do that, you’ll need to change the height of the menu bar by animating its height constraint.
Making the menu expand
Open ViewController.swift and scroll to the top of the class. Under the rest of the outlets add the following line of code:
@IBOutlet weak var menuHeightConstraint: NSLayoutConstraint!
NSLayoutConstraint is the class that represents the constraints you create in Interface Builder. Just like any other button, image view, or label, you can also create an outlet to a constraint.
Open Main.storyboard and select the menu view:
Open the Size Inspector tab and double click on the Constraint that says Height Equals: 44. This will open the familiar view of the constraint’s equation:
Switch to the last tab on the right — the Connections Inspector:
From here you can connect the constraint to its outlet in ViewController.
Drag from the small circle next to New Referencing Outlet to the view controller object:
From the popup menu, select the only available option: menuHeightConstraint. You’ve now created your outlet, as shown in the image below:
Open ViewController.swift and add the following three lines to actionToggleMenu():
isMenuOpen = !isMenuOpen
menuHeightConstraint.constant = isMenuOpen ? 184.0 : 44.0
titleLabel.text = isMenuOpen ? "Select Item" : "Packing List"
In the code above, you first toggle the Boolean variable isMenuOpen, which tracks whether the menu is currently expanded or collapsed.
Next, you modify the constraint’s constant property to either 184.0 pt or 44.0 pt, depending on the state of isMenuOpen, to make the menu expand or collapse as required.
In the final line of code you toggle the menu title between Packing List and Select Item as appropriate.
Build and run your project; tap the + button a few times and the menu should expand and contract as you had planned:
To animate the layout changes, you’ll need your old friend animate(withDuration:animations:) or any of the similar APIs.
Animating layout changes
Add the following code to the bottom of actionToggleMenu:
UIView.animate(withDuration: 1.0, delay: 0.0,
usingSpringWithDamping: 0.4, initialSpringVelocity: 10.0,
options: .curveEaseIn,
animations: {
self.view.layoutIfNeeded()
},
completion: nil
)
In the code above, you create a spring animation (just as you learned about in Chapter 4, “Springs”) and force an update of the layout from within the animations closure. This is all it takes to animate your constraint modifications.
Still a little fuzzy on what’s happening above? Here’s a bit more background on how the animation works.
When you modify pertinent view properties inside an animation closure, they’ll be animated as you would expect, and Auto Layout will still set the bounds and center of your views once it finishes its calculations.
In this case, you’ve already updated the constraint value, but iOS hasn’t had a chance to update the layout yet. By calling layoutIfNeeded() from within the animation closure, you set the center and bounds of every view involved in the layout. That’s it — there’s no magic happening in the background!
If you hadn’t called layoutIfNeeded(), UIKit would have performed a layout anyway since you changed a constraint, which marked the layout as dirty.
Build and run your project again; tap the + button and you’ll see the menu expand and contract with a bouncy animation.
You can even see that the menu title temporarily overlays the system menu on its way back up:
Of course, once the animation settles down it all looks good again.
Did you notice that the table view also shrank and grew along with the menu? Instead of covering up the table, the menu actually pushed the table view away as it expanded.
This is because of the existing constraint you added in the previous chapter that attaches the top of the table to the bottom of the menu. When the menu grows, the table shrinks to satisfy the constraint. Two animations for the price of one!
To spice things up a bit, you’ll now mix constraint animations with some non-constraint animations to see what new effects you can create.
Your next task is to rotate the + button by 45 degrees when the menu expands so it resembles an x — i.e. a close button.
Rotating view animations
Since you already know how to rotate views by adjusting their transform, add the following code to the final animations closure:
let angle: CGFloat = self.isMenuOpen ? .pi / 4 : 0.0
self.buttonMenu.transform = CGAffineTransform(rotationAngle: angle)
When the menu expands, you set the angle of the rotation to 45 degrees (or π/4 radians); when it contracts, you simply set the rotation back to 0. Then you update the transform on the button to set the view in motion.
Build and run your project; tap the + button to see how the rotation animation looks alongside the call to layoutIfNeeded():
The animation looks gorgeous; you can temporarily set the animation duration to 5–6 seconds to see exactly how the + sign rotates to become an x. You can also see the button bounce around its center thanks to the spring animation that drives both the constraint and rotation animations.
Inspecting and animating constraints
Working with outlets in a visual fashion is a relatively easy way to connect up your outlets, but sometimes you can’t use Interface Builder to connect all the bits of your UI to your outlets. You might add constraints from code, or maybe you just don’t want to Control-drag and create a massive number of outlets!
In these cases, you need to inspect the existing constraints at runtime and modify in code the ones you want to animate.
Luckily, the UIView class has a property named constraints, which gives you a list of all constraints that affect the given view. How convenient is that?
Add the following code to the top of actionToggleMenu():
titleLabel.superview?.constraints.forEach { constraint in
print(" -> \(constraint.description)\n")
}
This one-liner loops over all constraints affecting the menu bar view and prints them one by one to Xcode’s output console.
Build and run your project; tap the + button to see all constraints neatly listed like so:
It looks a bit messy, but read through the output carefully and you’ll be able to figure out what each constraint does. Take the following constraint as an example:
UILabel:...'Select Item'.centerX == UIView:...centerX
It’s clear that this is a constraint between a UIView and a UILabel; the description also includes the current text of the label. centerX is also mentioned a few times…aha! This must be the constraint that horizontally centers the title within the menu bar. It’s time to animate this bad boy.
Animating UILabel constraints
Find the following line near the top of actionToggleMenu(_:):
isMenuOpen = !isMenuOpen
Then add the following code below that line:
titleLabel.superview?.constraints.forEach { constraint in
if constraint.firstItem === titleLabel &&
constraint.firstAttribute == .centerX {
constraint.constant = isMenuOpen ? -100.0 : 0.0
return
}
}
Here you loop over the list of constraints affecting the menu bar view, but this time you are looking for a certain constraint to adjust.
Do you recall the equation for the horizontal center constraint? It looked something like the following:
Superview.CenterX = 1.0 * UILabel.CenterX + 0.0
The NSLayoutConstraint properties map to the above equation in a very straightforward manner:
The if condition makes more sense now when you look back at the last piece of code you added: for each constraint, you check if the secondItem is the title label and if the constraint aligns with the title’s CenterX.
When you find the correct constraint, you adjust constant to 100 pt to push the title to the left when the menu opens.
Note: There’s a slightly easier way to dynamically find an existing constraint and work with it; you’ll look into that next.
Build and run your project; tap the + button to see how your new constraint logic works:
Remember you’re calling layoutIfNeeded() from within a spring animation API, so the title animation bounces a bit.
Note: If by any chance your animation doesn’t kick in, check out the Center Horizontally in Superview constraint on the label; make sure the label is the first item and the menu bar view is the second.
Your UI is starting to look really cool, but you know you can take this even further. The next section shows you how to replace constraints to create some neat animations.
Animating by replacing constraints
At this point in the chapter, you’ve only modified the constant property of your constraints. Ironically, the constant property is a mutable property in the NSLayoutConstraint class!
If you want to modify the multiplier, or change a constraint in any other way, you’ll need to remove the constraint then add a new one in its place.
To learn how to do that, you’ll animate the vertical alignment of the menu title to move it up a bit as the menu opens. This should leave enough empty space at the bottom of the menu to show some more content, which you’ll add later in this chapter.
This time around, you’ll use a different technique to make sure you’ve got the correct constraint.
In Interface Builder you can assign an identifier to each constraint, which can help you easily get hold of it at run time.
Open Main.storyboard and find the Align Center Y constraint of the title label:
Double-click the constraint and in the Identifier text box enter TitleCenterY:
Back in ViewController.swift find the following spot in the code within actionToggleMenu, at the end of the for loop:
Insert the following code at the point indicated above:
if constraint.identifier == "TitleCenterY" {
constraint.isActive = false
//add new constraint
return
}
You check if the identifier of the constraint is the same as the one you want to replace and if so, you remove the constraint. You do that by setting isActive to false; this causes the view hierarchy to remove the constraint. If you don’t also have a reference to it, the constraint object will be deleted from memory.
Build and run your project; tap + and observe what happens:
Since there’s no longer a constraint to keep the view aligned, the title simply bounces to the top of its superview. The + button obediently tags along because its own CenterY is attached to the title’s CenterY.
Amusing as the effect is, you’ll need to add a new constraint and fix the layout.
Adding constraints programmatically
When the menu is retracted you want the title vertically centered within the menu view like so:
Or, to express the constraint via its equation:
Title.CenterY = Menu.CenterY * 1.0 + 0.0
But when the menu expands, you’d like to move the title a bit upwards to make space for the list of packing items later on:
Here’s the constraint’s equation spelled out in detail:
Title.CenterY = Menu.CenterY * 0.67 + 0.0
Find the placeholder comment //add new constraint in the code you just added, and replace it with the following:
let newConstraint = NSLayoutConstraint(
item: titleLabel,
attribute: .centerY,
relatedBy: .equal,
toItem: titleLabel.superview!,
attribute: .centerY,
multiplier: isMenuOpen ? 0.67 : 1.0,
constant: 0)
newConstraint.identifier = "TitleCenterY"
newConstraint.isActive = true
NSLayoutConstraint’s initializer takes a cartload of parameters, but happily they map exactly to all parts of the constraint’s equation. The parameters are as follows:
-
item: The first item in the equation; in this case, the title label. -
attribute: The attribute of the first item of the new constraint. -
relatedBy: A constraint can represent either a mathematical equality or an inequality. In this book, you’ll only use equality expressions, so here you use.equalto represent this relationship. -
toItem: The second item in the constraint equation; in this case, it’s your title’s superview. -
attribute: The attribute of the second item of the new constraint. -
multiplier: The equation multiplier as discussed earlier. -
constant: The equation constant.
As an additional step you give the constraint the identifier TitleCenterY. You will use that identifier the next time the user toggles the menu to find and replace the constraint you just created.
Finally you need to set the active property on the constraint to true and that tells Auto Layout to apply it to the current layout.
Build and run your project; open the menu and the title should move up to a spot just above the vertical center of the menu bar as shown below:
Adding menu content
Your next job is to show a list of items in the menu; these are all possible items you can add to your packing list.
The HorizontalItemList class that came with your starter project will assist you in displaying that list of items.
Note: This chapter won’t cover
HorizontalItemListin detail; its implementation isn’t relevant to creating animations. However, you’re still welcome to peek into HorizontalItemList.swift if you want to see how it works!
Still in ViewController.swift, scroll to the bottom of actionToggleMenu and add the following code:
if isMenuOpen {
slider = HorizontalItemList(inView: view)
slider.didSelectItem = {index in
print("add \(index)")
self.items.append(index)
self.tableView.reloadData()
self.actionToggleMenu(self)
}
self.titleLabel.superview!.addSubview(slider)
} else {
slider.removeFromSuperview()
}
If the menu is about to open, you create a new instance of HorizontalItemList in slider to hold your new items, assign a closure expression to didSelectItem and then finally add slider to the menu bar.
didSelectItem runs when the user taps an image in the list; in this event, you add the image index to the list of selected items and reload the table view.
In the else branch, when the menu is about to close, you simply remove the image list from its parent view.
Build and run your project; add a few items to see what the list of images looks like:
You’re nearly done; the final section of this chapter will walk you through dynamically creating and animating views in code.
Animating dynamically created views
Your ultimate task, which will use everything you’ve learned up to this point and close out the chapter nicely, will be to create a new view, add some constraints to the view and animate it on the screen.
showItem(_:) in ViewController is called when you tap a table row.
Your task is to create an image view with the tapped image index and display it at the bottom of the screen as shown below:
Add the following code to showItem(_:) to create an image view out of the selected image:
let imageView = UIImageView(image: UIImage(named: "summericons_100px_0\(index).png"))
imageView.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.5)
imageView.layer.cornerRadius = 5.0
imageView.layer.masksToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imageView)
In the code above, you load the selected image and create an image view from it. You give it a semi-transparent black background and round the corners slightly. Notice that you don’t set the position of the image view in its parent view.
Next you’ll create the constraints for the image view, one by one. Add the following code directly below the code you just added:
let conX = imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
This method uses the new NSLayoutAnchor class, which makes creating common constraints really easy. Here, you’re creating a constraint between the center x anchor of the image view and the view controller’s view.
Next, add the code below to give the image view a bottom constraint:
let conBottom = imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: imageView.frame.height)
This constraint sets the bottom of the image view to match the bottom of the view controller’s view, plus the image height; this positions the image just off the bottom edge of the screen, which will serve as the starting point of the animation.
Next you are going to fix up the image width. Add the following code:
let conWidth = imageView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.33, constant: -50.0)
This sets the image width to 1/3 of the screen width less 50 pt. The target size is 1/3 of the screen; you’ll animate away the 50 pt difference to make the image “grow” into place later.
Finally, you just need to set the height of the image. Since the images are square you can just set the height as equal to the image width.
Add one final bit of code for the last constraint, then activate them all in a group:
let conHeight = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor)
NSLayoutConstraint.activate([conX, conBottom, conWidth, conHeight])
This last constraint is a little different from the rest, as it’s the first time you’ve created a relationship between two properties of the same view. Don’t worry — this will still work out just fine.
Build and run your project; tap a table row and you’ll see your new image view appear like so:
This is, of course, just the starting position of your animation. Your job now is to make the image pop up from the bottom.
Adding additional dynamic animations
Add the following code to showItem(_:):
UIView.animate(withDuration: 0.8, delay: 0.0,
usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0,
animations: {
conBottom.constant = -imageView.frame.size.height/2
conWidth.constant = 0.0
self.view.layoutIfNeeded()
},
completion: nil
)
Changing constant on conY moves the image up, and the adjustment to conWidth grows the image width by 50 pt to make the image return to its original size. You don’t need to set the height as it’s automatically constrained to the image width.
Finally you call layoutIfNeeded(), which kicks off the animations. Build and run your project; tap a few table rows to see your image animate:
Hang on! The image view starts from the top left of the screen then flies into the middle! What happened?
Think about it for a moment: you added a view, set some constraints, then altered those constraints and animated a layout change. However, the view never got the chance to perform its initial layout, so your image started from its default position at (0, 0) in the top left. Ah — that’s why it’s flying in like that.
To fix this, you need to make sure your initial layout happens before the animation starts. Add the following code before the animation call:
view.layoutIfNeeded()
This will immediately set your initial layout before anything else happens. All constraint changes you make between the call to layoutIfNeeded() and the next one will be part of your animation.
Build and run your project now and the animations should work as intended.
It’s a little annoying that the images keep piling up on top of each other; you’ll fix this in the challenge at the end of this chapter.
Don’t forget to try the project in different simulators and orientations; your constraint animations should look good in all of them:
Challenge
Challenge: Animate the image out of the screen
OK — now you get to fix those pesky image views that stay stuck on the screen.
In showItem(_:), keep the image visible for 1 second and then animate it back out of the screen.
Use the animation method that lets you set a delay for the animation, animate the image back out of the screen, and finally remove the image view from the view hierarchy in the animation completion closure.
That’s all you need to know to complete the challenge. Good luck!
You now have a good understanding of how to create view animations in Auto Layout projects. Although not all projects in this book make use of Auto Layout, try to use it when you can to keep your skillset fresh!