[Updated for Swift 2.0 9/16/15 SJL]
Auto Layout in interface builder, can be a frustrating experience for those who do not know how to use it. Once you understand how to use Auto Layout, it becomes part of a simple workflow. However, there are times you code a button or label instead of using Interface Builder. For those cases you need to code your constraints too. Constraints in code, is an incredibly powerful way of laying out UIViews into a useful interface. This lesson will introduce the steps in Swift to programmatically add UIView
and its subclasses to an already existent view, then how to place them using Auto Layout.
Make and Set Up the Project
Create a new project for iPhone named SwiftAutoLayout with a single view. Since we want to explore how rotation affects Auto Layout, check all the device orientations. Click into the ViewController.Swift file, and under viewDidLoad()
add this code:
// How to set the orientation. override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { return UIInterfaceOrientationMask.All }
This is the override to use all orientations. There’s a lot going on here in line 3. Unlike Objective-C there are no factory constants, only structs. If you look for the constant UIInterfaceOrientationMaskAll
in the auto suggest, you will not find it. You will find UIInterfaceOrientationMask
instead and then need to use dot notation to get the All
or other possibilities. We’ll see more examples of this later.
Next in our code is to set up viewDidLoad()
. Change it to the following:
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. view.backgroundColor = UIColor( red: 0.9, green: 0.9, blue: 1, alpha: 1.0) makeLayout() }
We set a background color and call the method where we will do most of our work. Since we set up that method, you can add it near the top of the ViewController
class.
func makeLayout(){ }
Add Two UIViews
We will use two colored rectangles to start our layout. In makeLayout()
add the following:
// Make a view let view1 = UIView() view1.translatesAutoresizingMaskIntoConstraints = false view1.backgroundColor = UIColor.redColor() //Make a second view let view2 = UIView() view2.translatesAutoresizingMaskIntoConstraints = false view2.backgroundColor = UIColor( red: 0.75, green: 0.75, blue: 0.1, alpha: 1.0)
The code for these two is similar. In line 2 and 7, we instantiate a view. Line 3 and 8 is critical if you are using Auto Layout. It shuts down the auto resizing mask, which is an automatic way for Xcode to calculate constraints. Xcode has a very bad habit of making far too many constraints, and will cause a run-time error claiming you have too many constraints if you set any constraint programmatically. Set this to false and give yourself total control of the constraints. In the last line of both views, we set a color for the view.
Under this code, add the following:
//Add the views view.addSubview(view1) view.addSubview(view2)
I tend to keep my adding of sub views together as much as possible, so I can control and document the stacking order. With that, we have two views.
Size the Views with Auto Layout
If you build and run, you will see a colored background and nothing else. Our views have no size yet. With Auto Layout it is easiest to set the size constraints of the views on the view itself. Add the following code below our adding of the sub views:
//--------------- constraints //make dictionary for views let viewsDictionary = [ "view1":view1, "view2":view2]
In order to use constraints, we need a dictionary of our views. Line 4 creates this dictionary, which looks pretty obvious to the eye. The key has a value of the view. Now that we have this, we can add the following under it:
//sizing constraints //view1 let view1_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat( "H:[view1(50)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary) let view1_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat( "V:[view1(50)]", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary) view1.addConstraints(view1_constraint_H) view1.addConstraints(view1_constraint_V)
Line 3 and 4 use the constraintsWithVisualFormat
class method to make horizontal and vertical sizing constraints — the height and width. The two lines are very similar in code except for the first parameter, which is the visual format string. Visual Format Language is a way to describe how to set up the constraints using keyboard characters. A keyword surrounded by square brackets []
is a view. After the name, in parentheses ()
is the size of the view. The first two characters are either H:
or V:
standing for horizontal or vertical . Horizontal is the default and can be left off, but for code readability, I tend to add it. In our example "H:[view1(50)]"
means we want a 50 point wide view and "V:[view1(50)]"
means we want a 50 point high view. We have to send the options:
a value from the struct NSLayoutFormatOptions
. However None
is missing or left out of the possible choices. The NSLayoutFormatOptions
does have an initializer which takes a value using rawValue
. By setting the initializer to rawValue:0
, we tell teh compiler we are doing nothing here. The parameter metrics:
we will discuss shortly, but for now leave it nil. Our last parameter is our view dictionary, which allows the method to know which views we are referring to in []
.
Build and run this code. You’ll get a single red square in the upper left of the screen.
Let’s make constraints to size the second view. Add the following code:
//view2 let view2_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat( "H:[view2(50)]", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary) let view2_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat( "V:[view2(>=40)]", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary) view2.addConstraints(view2_constraint_H) view2.addConstraints(view2_constraint_V)
This is the same as the code for the first view, with one exception. The format string in line 3 is "V:[view2(<=40)]"
. We have a height of 40 pixels. We also have a <=
stating it can be 40 points or bigger, should the constraints call for it. When we place view2
we’ll place it to extend all the way to the bottom of the super view. By writing it this way, it does not matter what device or orientation we are on. Auto Layout will figure out how high to make view2
.
Arrange the Views
Build and Run.
There are two shy little views cowering in the corner. The corner is coordinate (0,0) and they are hiding there since we have given no position constraints. Add the following:
//position constraints //views let view_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat( "H:|-[view2]", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary) let view_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat( "V:|-36-[view1]-8-[view2]-0-|", options: NSLayoutFormatOptions.AlignAllLeading, metrics: nil, views: viewsDictionary) view.addConstraints(view_constraint_H) view.addConstraints(view_constraint_V)
As in our size constraints, we use the same method constraintsWithVisualFormat()
. We have two different format strings and the options in line 5 is now NSLayoutFormatOptions.AlignAllLeading
. We add these constraints to view
, not view1
and view2
for positioning constraints. We add to the closest parent of the views.
Line 2’s format string "H:|-[view2]"
is a horizontal string that sets view2
a standard width away from the left edge of the superview. The |
is the edge of the superview, the -
means a standard width between the superview and view2. Line 3’s format string is "V:|-36-[view1]-8-[view2]-0-|"
. In a vertical string, the beginning of the string is the top and the end is the bottom. While in line three we did not specify the right, here we specify top and bottom. The -36-
and -0-
are the same as -
in line 2, but specify how many points apart the view and super view are. The -8-
staes the views should be 8 points away from each other. So if we were to read this in plain language we have view1
36 points below the top of the superview, 8 points between view1
and view2
and no space between view2
and the bottom. Since view2
can stretch in height, this will turn view2
into a stripe down the entire view.
Looking at the code, we did not specify a horizontal position in the visual language for view1
. We used an alignment to do this. The constant NSLayoutFormatOptions.AlignAllLeading
in the options:
parameter of line 5 does this, aligning the two views on their leading side.
Build and run. we now have a square and a stripe.
Rotate the device in the simulator with command and right or left arrow, and you have a shorter stripe.
A Little Bit About Metrics
We have been using literal numbers in our code to specify widths and spacing. We don’t have to. We can use identifiers. That is what the metrics:
parameter is all about. The metrics:
parameter refers to a dictionary with keys and values we can use for those spacing and sizes. Add the following line under viewsDictionary
let metricsDictionary = [ "view1Height": 50.0, "view2Height":40.0, "viewWidth":50.0 ]
This sets a dictionary of heights and widths for the views. We are keeping the widths the same so we use one value. Now change our sizing code to use this in the visual format and in options:
to this:
//sizing constraints //view1 let view1_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat( "H:[view1(viewWidth)]", options: NSLayoutFormatOptions(rawValue:0), metrics: metricsDictionary, views: viewsDictionary) let view1_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat( "V:[view1(view1Height)]", options: NSLayoutFormatOptions(rawValue:0), metrics: metricsDictionary, views: viewsDictionary) view1.addConstraints(view1_constraint_H) view1.addConstraints(view1_constraint_V) //view2 let view2_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat( "H:[view2(viewWidth)]", options: NSLayoutFormatOptions(rawValue:0), metrics: metricsDictionary, views: viewsDictionary) let view2_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat( "V:[view2(>=view2Height)]", options: NSLayoutFormatOptions(rawValue:0), metrics: metricsDictionary, views: viewsDictionary) view2.addConstraints(view2_constraint_H) view2.addConstraints(view2_constraint_V)
Build and run, and you will see no change.
Change in the metrics dictionary "viewWidth":50.0
to "viewWidth":100.0
. Build and run again and we get a wider stripe.
Like anywhere else we use variables or constants instead of literals, this make changes to code a lot easier. For clarity in the rest of our examples I’ll use literals, but be aware using metrics is a good practice to get into.
Add the Button and Label
Views of colored backgrounds are cute, but they don’t do much. Adding some more useful controls would be nice. Let’s add a label and a button to our code. First we need a few constants declared. Just under the class definition, add the following constants:
//Make button and label let button1:UIButton! = UIButton(type: .System) let label1:UILabel! = UILabel() let atRest = "Doesn't do much" let atWork = "Secret Agent"
We declared two strings we’ll use in the target action for the button we’ll set up in a bit, a button and a label. We made a button and a label. Their scope is for the entire subclass, not just the methods like we did with the views. Declaring the label in Swift is just like declaring any other class: call its initializer, in this case UILabel()
. For the button we have a initializer method UIButton(type:)
which takes a constant from the struct UIButtonType
. Most buttons will be UIButtonType.System
.
Now we can add the label and button. Under the code making view2
, add the following:
//Make a label label1.text = atRest label1.translatesAutoresizingMaskIntoConstraints = false //Make a button button1.translatesAutoresizingMaskIntoConstraints = false button1.setTitle("Platypus", forState: UIControlState.Normal) button1.addTarget(self, action: "buttonPressed", forControlEvents: UIControlEvents.TouchUpInside) button1.backgroundColor = UIColor.blueColor() button1.setTitleColor(UIColor.whiteColor(), forState: UIControlState.Normal) view2.addSubview(button1) view2.addSubview(label1)
Since we already instantiated the label and button, we need to set properties. For both, we set the translatesAutoresizingMaskIntoConstraints
property to false
to keep Auto Layout from barking at us later. Let’s look more closely at the addTarget()
method
button1.addTarget(self, action: "buttonPressed", forControlEvents: UIControlEvents.TouchUpInside)
This code tells Swift what method we will use when the event happens we use a selector. this is the equivalent of a control-Drag in the storyboard. In Swift, we use a string when identifying a selector, whihc is the method we will call on a TouchUpInside
event. In our example, that method is "buttonPressed"
. This means we need a method buttonPressed
. We’ll make a simple toggle. Add this method above viewDidLoad
func buttonPressed(){ if label1.text == atRest{ label1.text = atWork }else{ label1.text = atRest } }
To demonstrate Auto Layout on a hierarchy of views, we are going to add the controls to view2
instead of view
. Add the following code just below view.addSubview(view2)
//controls let control_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat( "H:|-[button1(>=80)]-20-[label1(>=100)]", options: NSLayoutFormatOptions.AlignAllCenterY, metrics: nil, views: viewsDictionary) let control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat( "V:[button1]-40-|", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary) view2.addConstraints(control_constraint_H) view2.addConstraints(control_constraint_V)
Instead of making separate sizing constraints, we joined them into the positioning constraints. We already have an example of vertical alignment, so here is an example of horizontal alignment. Our visual format strings are "H:|-[button1(>=80)]-20-[label1(>=100)]"
and "V:[button1(40)]-40-|"
with options:
set to NSLayoutFormatOptions.AlignAllCenterY
in line 2. Line 3 vertically lays out button1
40 points from the bottom of the superview with a height of 40. Line 2 places button1
the standard distance from the left of the superview, then places label1
20 points to the right of that, and aligns both vertically to their centers. Line 2 also makes the button at least 80 points wide and the label at least 100 points wide.
Build and run. You will get a run-time error.
One More Bug
If you go through the exception that crashes the app, you will find towards the beginning:
'NSInvalidArgumentException',
reason: 'Unable to parse constraint format:
button1 is not a key in the views dictionary.
For our bug portion of this post, I wanted to share two of the common mistakes people make with setting up Auto Layout. One is forgetting to add a view to the view dictionary. Find your viewsDictionary
code:
let viewsDictionary = [ "view1":view1, "view2":view2]
The dictionary has no elements for button1
and label1
. Change the dictionary to this.
let viewsDictionary = [ "view1":view1, "view2":view2, "button1":button1, "label1":label1]
Build and run and you should see the app working:
The second error is the auto resizing mask. Comment out this line:
//view2.translatesAutoresizingMaskIntoConstraints = false
Build and run. The run-time error message begins
SwiftAutoLayout[1874:60997] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it.
This error message is for too many constraints on a view. If we align center label1
as we did above then set a vertical constraint that it is 10 points from the top of the superview, we have conflicting information. This error tells us we have constraint conflicts. Our format is correct in this case — or so it seems. If we don’t send the message setTranslatesAutoresizingMaskIntoConstraints(false)
to view2
Auto Layout will create constraints for us. When we add our own they will always be too many. As I mentioned above always make setTranslatesAutoresizingMaskIntoConstraints()
false when you are working programmatically with constraints. Uncomment the line again ad the app works perfectly.
That is the basics of setting up Auto Layout constraints in Swift. There is a lot more here to cover, but this is enough to get a developer busy working without a storyboard.
The Whole Code
The code is a single file this time. Here is all of it in working order.
// // ViewController.swift // SwiftProgrammaticAutoLayout // // Created by Steven Lipton on 9/15/15. // Copyright © 2015 MakeAppPie.Com. All rights reserved. // import UIKit class ViewController: UIViewController { //Make button and label let button1:UIButton! = UIButton(type: .System) let label1:UILabel! = UILabel() let atRest = "Doesn't do much" let atWork = "Secret Agent" func makeLayout(){ //Make a view let view1 = UIView() view1.translatesAutoresizingMaskIntoConstraints = false view1.backgroundColor = UIColor.redColor() //Make a second view let view2 = UIView() view2.translatesAutoresizingMaskIntoConstraints = false view2.backgroundColor = UIColor(red: 0.75, green: 0.75, blue: 0.1, alpha: 1.0) //Add the views view.addSubview(view1) view.addSubview(view2) //--------------- constraints //make dictionary for views let viewsDictionary = ["view1":view1,"view2":view2,"button1":button1,"label1":label1] let metricsDictionary = ["view1Height": 50.0,"view2Height":40.0,"viewWidth":100.0 ] //sizing constraints //view1 let view1_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:[view1(50)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: metricsDictionary, views: viewsDictionary) let view1_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[view1(50)]", options: NSLayoutFormatOptions(rawValue:0), metrics: metricsDictionary, views: viewsDictionary) view1.addConstraints(view1_constraint_H) view1.addConstraints(view1_constraint_V) //view2 let view2_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:[view2(50)]", options: NSLayoutFormatOptions(rawValue:0), metrics: metricsDictionary, views: viewsDictionary) let view2_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[view2(>=40)]", options: NSLayoutFormatOptions(rawValue:0), metrics: metricsDictionary, views: viewsDictionary) view2.addConstraints(view2_constraint_H) view2.addConstraints(view2_constraint_V) //position constraints //views let view_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:|-[view2]", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary) let view_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:|-36-[view1]-8-[view2]-0-|", options: NSLayoutFormatOptions.AlignAllLeading, metrics: nil, views: viewsDictionary) view.addConstraints(view_constraint_H) view.addConstraints(view_constraint_V) //Make a label label1.text = atRest label1.translatesAutoresizingMaskIntoConstraints = false //Make a button button1.translatesAutoresizingMaskIntoConstraints = false button1.setTitle("Platypus", forState: UIControlState.Normal) button1.addTarget(self, action: "buttonPressed", forControlEvents: UIControlEvents.TouchUpInside) button1.backgroundColor = UIColor.blueColor() button1.setTitleColor(UIColor.whiteColor(), forState: UIControlState.Normal) view2.addSubview(button1) view2.addSubview(label1) //controls let control_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:|-[button1(>=80)]-20-[label1(>=100)]", options: NSLayoutFormatOptions.AlignAllCenterY, metrics: nil, views: viewsDictionary) let control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[button1]-40-|", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary) view2.addConstraints(control_constraint_H) view2.addConstraints(control_constraint_V) } func buttonPressed(){ if label1.text == atRest{ label1.text = atWork }else{ label1.text = atRest } } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. view.backgroundColor = UIColor(red: 0.9, green: 0.9, blue: 1, alpha: 1.0) makeLayout() } override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { return UIInterfaceOrientationMask.All } }
Leave a Reply