Creating layouts is a huge part of every iOS developer work. Working with UIKit is one of the basics which we need to master. From simple login screens where you have two text fields and a button to more complex screens with nested stack views, custom collection view cells where there is also a need to implement animations improving user experience.
Positioning views can be done in a few ways:
- Manually setting views frame position
- Layout constraints created in code
The first option is probably used only by masochists. Storyboards are ideal for prototyping and building small apps. Collaboration when using storyboards can be very hard and resolving merge conflicts can make you cry. So this leaves us with the third option. In Netguru we decided that our policy is to always build our views using code. It makes collaboration easier.
Some of us may remember times when a common way of adding layout constraints was using Visual Format Language. For those who do not remember what was that about, here is a “simple” snippet:
let verticalConstraints = NSLayoutConstraint.constraints( withVisualFormat: "V:|-40-[logoImageView(50)]-[loginTextField]-20-[passwordTextField]-20-[loginButton]-15@750-|", options: , metrics: nil, views: views )
Simple? Not at all. It’s not very descriptive. It takes a huge amount of time to master VFL. Because of that and a fact that NSLayoutConstraint API was not very friendly, a lot of libraries for adding constraints were created. You probably heard about PureLayout, SnapKit, Masonry, EasyPeasy and many many others. All of those were trying to make adding constraints easier. I used some of those and none of them made me spend a lot of time to learn how to create constraints using those.
Some time ago, Apple decided to improve our lives and introduced NSLayoutAnchor. At first glance, it looks very nice. Every view have the same set of anchors that we can use for creating constraints. Let's take a look at the example:
let topConstraint = logoView.topAnchor.constraint(equalTo: self.topAnchor, constant: 50) let heightConstraint = emailTextField.heightAnchor.constraint(equalToConstant: 100) let widthConstraint = emailTextField.widthAnchor.constraint(equalToConstant: 100)
So, where is the pain?
Actually, that’s not all the code you need to write. To make it work you need to set translatesAutoresizingMaskIntoConstraints flag to false for every view that you will be setting up. Next thing is that every constraint is not active after creation. To make it all work we need code like this:
self.translatesAutoresizingMaskIntoConstraints = false logoView.translatesAutoresizingMaskIntoConstraints = false logoView.topAnchor.constraint(equalTo: self.topAnchor, constant: 50).isActive = true logoView.topAnchor.constraint(equalTo: self.topAnchor, constant: 50).isActive = true logoView.heightAnchor.constraint(equalToConstant: 100).isActive = true logoView.widthAnchor.constraint(equalToConstant: 100).isActive = true
And remember, UIKit will not help you. If you will forget about setting correct value to translatesAutoresizingMaskIntoConstraints or isActive you can end up debugging layout issues from code that “should work”. Trust me, been there, done that.
How can we deal with layouts without using any external dependency and without all NSLayoutAnchor boilerplate. First thing that comes to mind is to write micro framework that will wrap creating constraints using NSLayoutAnchors. Before writing any helper for that I came across a post written by Chris Eidhof describing how to approach this problem. His idea is to use key paths. How great is that? I loved it from beginning. I will not describe how it works in detail, Chris did a great job explaining this in his post, there is even a free Swift Talk about it. You should check it out.
If Chris did all the necessary work, where is my part here? Actually his implementation is missing some of features that I needed. He handled case only when we want to add constraints along with adding view as subview. It was not possible to add relations between two views using his solution. I’ve got cracking and came up with small wrapper, that in my opinion helps creating constraints a lot. By small I mean, that it has around 200 lines of code of which most are docs.
Syntax looks like this:
loginTextField.addConstraints([ equal(\.heightAnchor, to: 30), equal(logoImageView, \.topAnchor, \.bottomAnchor, constant: 20), equal(superView, \.leadingAnchor, constant: 20), equal(superView, \.trailingAnchor, constant: -20) ])
What does this do is:
- makes view 30pts height
- attaches top border to bottom border of logoView with 20pts spacing
- attaches leading and trailing anchors to corresponding anchors of superView with offset 20pts
I hope that without my explanation you could easily tell how this works. If yes, then i can consider my goal achieved.
For more info please refer to the code and documentation for the wrapper. If you want to play with this, I’ve prepared swift playground with one simple view created.