Animating NSLayoutAnchor Constraints and the Pitfalls Involved (Swift 4.0)
November 24, 2017
Correctly laying out an iPhone application sometimes feels like the 20% of a project that takes 80% of your effort/time. That's why when Apple hit us with NSLayoutAnchor in iOS 9 I jumped on the opportunity to make life easier. If you haven't spent time using NSLayoutAnchor check out Getting Started with NSLayoutAnchor (Swift 4.0) my article real quick and then come back. Today I wanna show you how to animate those constraints and best practices I've come across.
Animating NSLayoutAnchor Constraints
UIView.animate(withDuration: 0.33) {
self.view.layoutIfNeeded()
}
Check it out. That's it. When you set new constraints on a view or object you just simply add these three lines of code and BAM you've got an animation. You can use any variation of .animateWithDuration as long as you make sure to call .layoutIfNeeded() inside the animation block on the parent view that holds the view you're wanting to animate.
Complete Example
class ViewController: UIViewController {
// Initialized view we want to animate
let viewToAnimate = UIView(frame: .zero)
override func viewDidLoad() {
super.viewDidLoad()
// Add to the view hierarchy
self.view.addSubview(viewToAnimate)
viewToAnimate.backgroundColor = .red
// ***** [1] *****
viewToAnimate.translatesAutoresizingMaskIntoConstraints = false
viewToAnimate.heightAnchor.constraint(equalToConstant: 200).isActive = true
viewToAnimate.widthAnchor.constraint(equalToConstant: 200).isActive = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// ***** [2] *****
viewToAnimate.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
viewToAnimate.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
// ***** [3] *****
UIView.animate(withDuration: 0.33) {
self.view.layoutIfNeeded()
}
}
}
- It's important to set some initial constraints such as height and width because we initially gave it a frame with a height equal to zero and a width equal to zero.
- Inside of viewDidAppear, we're telling our view viewToAnimate that it needs to set its centerYAnchor and centerXAnchor equal to its superView's and activate them.
- Here we've created an animation block and given it how long we'd like it to animate. Inside of the block we're telling our ViewController's view to layoutIfNeeded because we've just activated some new constraints and they need to be shown.
ERROR: Unable to simultaneously satisfy constraints.
.translatesAutoresizingMaskIntoConstraints
What does that even mean "Unable to simultaneously satisfy constraints"? It means you've added some extra constraints that aren't playing nicely with some constraints you previously set (or didn't). If you ever see this error the FIRST thing you should look for is if you wrote this line of code in ViewDidLoad.
viewToAnimate.translatesAutoresizingMaskIntoConstraints = false
It's set to true by default and that means the compiler is trying to help you make constraints but since you decided you'd do it yourself the compiler is confused.
NSLayoutConstraint.deactivate
If you remembered to do that then my next suggestion is to check if you deactivated your constraints before adding new ones. The view is simply a soldier trying to satisfy all your orders but doesn't know how to all at once.
class ViewController: UIViewController {
// Initialized view we want to animate
let viewToAnimate = UIView(frame: .zero)
override func viewDidLoad() {
super.viewDidLoad()
// Add to the view hierarchy
self.view.addSubview(viewToAnimate)
viewToAnimate.backgroundColor = .red
viewToAnimate.translatesAutoresizingMaskIntoConstraints = false
viewToAnimate.heightAnchor.constraint(equalToConstant: 200).isActive = true
viewToAnimate.widthAnchor.constraint(equalToConstant: 200).isActive = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// ***** [1] *****
let viewConstraints = viewToAnimate.constraints
// ***** [2] *****
NSLayoutConstraint.deactivate(viewConstraints)
viewToAnimate.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
viewToAnimate.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
UIView.animate(withDuration: 0.33) {
self.view.layoutIfNeeded()
}
}
}
- The class UIView has a property called constraints that returns an Array of NSLayoutConstraints currently associated with the view. Here we're creating a constant linked to those constraints.
- NSLayoutConstraint has a class function called deactivate that accepts an array of NSLayoutConstraints and deactivates them all.
Why is my view disappearing?
If you've been following along you might've noticed your view is vanishing like all your friends when you ask for a ride. This is where constraints can become a headache. When you deactivated all your views constraints you also deactivated the constraints telling it how wide and tall it is. So how do you tell your view which constraints to keep and get rid of?
There are a few ways to do this but I'm gonna show you the way I've found to be easiest.
class ViewController: UIViewController {
// Initialized view we want to animate
let viewToAnimate = UIView(frame: .zero)
// ***** [1] *****
var topAnchor: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Add to the view hierarchy
self.view.addSubview(viewToAnimate)
viewToAnimate.backgroundColor = .red
viewToAnimate.translatesAutoresizingMaskIntoConstraints = false
viewToAnimate.heightAnchor.constraint(equalToConstant: 200).isActive = true
viewToAnimate.widthAnchor.constraint(equalToConstant: 200).isActive = true
viewToAnimate.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
// ***** [2] *****
topAnchor = viewToAnimate.topAnchor.constraint(equalTo: self.view.bottomAnchor)
topAnchor.isActive = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// ***** [3] *****
topAnchor.isActive = false
viewToAnimate.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
UIView.animate(withDuration: 0.33, delay: 0.5, options: .curveEaseInOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
- Create the variable we'll set equal to viewToAnimate's topAnchor.
- Set the topAnchor variable we create and then do the normal procedure to activate it.
- Because we have a reference to the topAnchor originally created we can easily deactivate it. If you didn't deactivate the constraint you'd probably still get the desired results but it will be in conflict with the anchor we set just below it and turn on.
This new example might be a bit confusing because the variable we created topAnchor is of type NSLayoutConstraint but we'be been using NSLayoutAnchors. The trick is that when creating the constraints we're given back an NSLayoutConstraint. The class NSLayoutAnchor is syntactic sugar for NSLayoutConstraint basically. But to that end, we can grab a reference to the constraint we need to deactivate to animate our view into view. Since we don't just deactivate all of viewToAnimates's constraints we don't need to worry about it disappearing.
One more optimization...
Occasionally, you might find yourself needing to activate and deactivate a lot of constraints for the same object a lot. That can lead to repetitive calls to NSLayoutConstraint throughout your code. Here's my little helper variable I like to create if I find myself doing that.
var activeViewConstraints: [NSLayoutConstraint] = [] {
willSet {
NSLayoutConstraint.deactivate(activeViewConstraints)
}
didSet {
NSLayoutConstraint.activate(activeViewConstraints)
}
}
You can use this variable to automatically deactivate and activate new constraints when you set them to this variable. Using property observer willSet to deactivate the old constraints that activeConstraints was holding and then in didSet to active the new constraints activeConstraints is set to. With that addition, I can update my previous example to this.
import UIKit
class ViewController: UIViewController {
// Initialized view we want to animate
let viewToAnimate = UIView(frame: .zero)
// ***** [1] *****
var activeViewConstraints: [NSLayoutConstraint] = [] {
willSet {
NSLayoutConstraint.deactivate(activeViewConstraints)
}
didSet {
NSLayoutConstraint.activate(activeViewConstraints)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Add to the view hierarchy
self.view.addSubview(viewToAnimate)
viewToAnimate.backgroundColor = .red
viewToAnimate.translatesAutoresizingMaskIntoConstraints = false
viewToAnimate.heightAnchor.constraint(equalToConstant: 200).isActive = true
viewToAnimate.widthAnchor.constraint(equalToConstant: 200).isActive = true
// ***** [2] *****
activeViewConstraints = [
viewToAnimate.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
viewToAnimate.topAnchor.constraint(equalTo: self.view.bottomAnchor)
]
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// ***** [3] *****
activeViewConstraints = [
viewToAnimate.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
viewToAnimate.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
]
UIView.animate(withDuration: 0.33, delay: 0.5, options: .curveEaseInOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
- Creating a new helper variable called activeViewConstraints that is an array of NSLayoutConstraints.
- Setting the starting constraints on viewToAnimate.
- Setting the new constraints we'd like viewToAnimate to use.
This can be a very helpful way to reduce some repetitive code. The drawback is you'll wanna include any constraints you might need to animate so it can lead to repetitive code as well when compared to my previous example. Pretty awesome huh? You've got a great way to keep up with your currently active constraints. All I ask is you don't abuse this power ;).
Final Product
If you'd like to see a complete project here it is on GitHub for all to see. Feel free to let me know any improvements you've made when animating constraints.
If you've read this far thank you very much! I really hope it was worth the read and that you'll share/like/heart and let others know it would be worth their time too! Please leave me a comment, I'd love to talk with anyone who enjoyed this post! Auf wiedersehen!