Mastering iOS auto layout anchors programmatically from Swift
Looking for best practices of using layout anchors? Let's learn how to use the iOS autolayout system in the proper way using Swift.
Creating views and constraints programmatically
First of all I’d like to recap the UIViewController
life cycle methods, you are might familiar with some of them. They are being called in the following order:
- loadView
- viewDidLoad
- viewWillAppear
- viewWillLayoutSubviews
- viewDidLayoutSubviews
- viewDidAppear
In the pre-auto layout era, you had to do your layout calculations inside the viewDidLayoutSubviews
method, but since this is a pro auto layout tutorial we are only going to focus on the loadView
& viewDidLoad
methods. 🤓
These are the basic rules of creating view hierarchies using auto layout:
- Never calculate frames manually by yourself!
- Initialize your views with
.zero
rect frame - Set
translatesAutoresizingMaskIntoConstraints
to false - Add your view to the view hierarchy using
addSubview
- Create and activate your layout constraints
NSLayoutConstraint.activate
- Use
loadView
instead ofviewDidLoad
for creating views with constraints - Take care of memory management by using weak properties
- Set every other property like background color, etc. in
viewDidLoad
Enough theory, here is a short example:
class ViewController: UIViewController {
weak var testView: UIView!
override func loadView() {
super.loadView()
let testView = UIView(frame: .zero)
testView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(testView)
NSLayoutConstraint.activate([
testView.widthAnchor.constraint(equalToConstant: 64),
testView.widthAnchor.constraint(equalTo: testView.heightAnchor),
testView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
testView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
])
self.testView = testView
}
override func viewDidLoad() {
super.viewDidLoad()
self.testView.backgroundColor = .red
}
}
Pretty simple, huh? Just a few lines of code and you have a fixed size center aligned view with a dedicated class property reference. If you create the exact same through interface builder, the system will “make” you the loadView
method for free, but you’ll have to setup an IBOutlet
reference to the view.
The eternal dilemma: code vs Interface Builder.
It really doesn’t matters, feel free to chose your path. Sometimes I love playing around with IB, but in most of the cases I prefer the programmatic way of doing things. 😛
Common UIKit auto layout constraint use cases
So I promised that I’ll show you how to make constraints programmatically, right? Let’s do that now. First of all, I use nothing but layout anchors. You could waste your time with the visual format language, but that’s definitely a dead end. So mark my words: use only anchors or stack views, but nothing else! 😇
Here are the most common patterns that I use to create nice layouts. 😉
Set fixed with or height
First one is the most simple one: set a view’s height or a width to a fixed point.
testView.widthAnchor.constraint(equalToConstant: 320),
testView.heightAnchor.constraint(equalToConstant: 240),
Set aspect ratio
Settings a view’s aspect ratio is just constrainting the width to the height or vica versa, you can simply define the rate by the multiplier.
testView.widthAnchor.constraint(equalToConstant: 64),
testView.widthAnchor.constraint(equalTo: testView.heightAnchor, multiplier: 16/9),
Center horizontally & vertically
Centering views inside another one is a trivial task, there are specific anchors for that.
testView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
testView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
Stretch or fill inside view with padding
The only tricky part here is that trailing and bottom constraints behave a little bit different, than top & leading if it comes to the constants. Usually you have to work with negative values, but after a few tries you’ll understand the logic here. 😅
testView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 32),
testView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 32),
testView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -32),
testView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -32),
Proportional width or height
If you don’t want to work with constant values, you can use the multiplier.
testView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 1/3),
testView.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 2/3),
Using safe area layout guides
With the latest iPhone you’ll need some guides in order to keep you safe from the notch. This is the reason why views have the safeAreaLayoutGuide property. You can get all the usual anchors after calling out to the safe area guide. 💪
testView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
testView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
testView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
testView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
Animating layout constraints
Animation with constraints is easy, you shouldn’t believe what others might say. I made some rules and an example that’ll help you understanding the basic principles of animating constant values of a constraint, plus toggling various constraints. 👍
Rules:
- Use standard UIView animation with layoutIfNeeded
- Always deactivate constraints first
- Hold to your deactivated constraints strongly
- Have fun! 😛
Constraint animation example:
class ViewController: UIViewController {
weak var testView: UIView!
weak var topConstraint: NSLayoutConstraint!
var bottomConstraint: NSLayoutConstraint!
var heightConstraint: NSLayoutConstraint!
override func loadView() {
super.loadView()
let testView = UIView(frame: .zero)
testView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(testView)
let topConstraint = testView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor)
let bottomConstraint = testView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor)
NSLayoutConstraint.activate([
topConstraint,
testView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
testView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
bottomConstraint,
])
let heightConstraint = testView.heightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.5)
self.testView = testView
self.topConstraint = topConstraint
self.bottomConstraint = bottomConstraint
self.heightConstraint = heightConstraint
}
override func viewDidLoad() {
super.viewDidLoad()
self.testView.backgroundColor = .red
let tap = UITapGestureRecognizer(target: self, action: #selector(self.tapped))
self.view.addGestureRecognizer(tap)
}
@objc func tapped() {
if self.topConstraint.constant != 0 {
self.topConstraint.constant = 0
}
else {
self.topConstraint.constant = 64
}
if self.bottomConstraint.isActive {
NSLayoutConstraint.deactivate([self.bottomConstraint])
NSLayoutConstraint.activate([self.heightConstraint])
}
else {
NSLayoutConstraint.deactivate([self.heightConstraint])
NSLayoutConstraint.activate([self.bottomConstraint])
}
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
}
It’s not that bad, next: adaptivity and supporting multiple device screen sizes. 🤔
How to create adaptive layouts for iOS?
Even Apple is struggling with adaptive layouts in the built-in iOS applications. If you look at apps that are made with collection views - like photos - layouts are pretty okay on every device. However there are a few other ones, that - in my opinion - are horrible experiences on a bigger screen. 🤐
Rotation support
Your first step to adaptive layout is supporting multiple device orientations. You can check my previous article about iOS auto layout there are lots of great stuff inside that article about rotation support, working with layers inside auto layout land, etc. 🌈
Trait collections
Second step is to adapt trait collections. UITraitCollection is there for you to group all the environmental specific traits such as size classes, display scale, user interface idiom and many more. Most of the times you will have to check the vertical & horizontal size classes. There is a reference of device size classes and all the possible variations made by Apple, see the external sources section below. 😉
This little Swift code example below is demonstrating how to check size classes for setting different layouts for compact and regular screens.
class ViewController: UIViewController {
weak var testView: UIView!
var regularConstraints: [NSLayoutConstraint] = []
var compactConstraints: [NSLayoutConstraint] = []
override func loadView() {
super.loadView()
let testView = UIView(frame: .zero)
testView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(testView)
self.regularConstraints = [
testView.widthAnchor.constraint(equalToConstant: 64),
testView.widthAnchor.constraint(equalTo: testView.heightAnchor),
testView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
testView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
]
self.compactConstraints = [
testView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
testView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
testView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
testView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
]
self.activateCurrentConstraints()
self.testView = testView
}
private func activateCurrentConstraints() {
NSLayoutConstraint.deactivate(self.compactConstraints + self.regularConstraints)
if self.traitCollection.verticalSizeClass == .regular {
NSLayoutConstraint.activate(self.regularConstraints)
}
else {
NSLayoutConstraint.activate(self.compactConstraints)
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.testView.backgroundColor = .red
}
// MARK: - rotation support
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .allButUpsideDown
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
// MARK: - trait collections
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
self.activateCurrentConstraints()
}
}
Device detection
You can also check the user interface idiom through the UIDevice
class (aka. is this freakin’ device an iPhone or an iPad?) to set for example font sizes based on it. 📱
UIDevice.current.userInterfaceIdiom == .pad
Screen size
Another option to figure out your environment is checking the size of the screen. You can check the native pixel count or a relative size based in points.
//iPhone X
UIScreen.main.nativeBounds // 1125x2436
UIScreen.main.bounds // 375x812
Usually I’m trying to keep myself to these rules. I don’t really remember a scenario where I needed more than all the things I’ve listed above, but if you have a specific case or questions, please don’t hesitate to contact me. 😉
Related posts
10 little UIKit tips you should know
In this article I've gathered my top 10 favorite modern UIKit tips that I'd definitely want to know before I start my next project.
Building input forms for iOS apps
Learn how to build complex forms with my updated collection view view-model framework without the struggle using Swift.
Custom UIView subclass from a xib file
Do you want to learn how to load a xib file to create a custom view object? Well, this UIKit tutorial is just for you written in Swift.
Custom views, input forms and mistakes
Just a little advice about creating custom view programmatically and the truth about why form building with collection views sucks.