iOS & Swift Tutorials

Learn iOS development in Swift. Over 2,000 high quality tutorials!

Building an App with only code using Auto Layout

Learn how to make your iOS app’s UI in code using Auto Layout without using Storyboards or XIBs, and how it can make working in a team easier.

4.4/5 19 Ratings

Version

  • Swift 5, iOS 13, Xcode 11

Making your app look good in both portrait and landscape layouts is something every developer needs to do on a daily basis. With the introduction of SwiftUI, it’s easier than ever to do so. However, since SwiftUI is still some years away from world domination, you may have to stick with your old friend Auto Layout a little longer.

In the beginning of the iPhone era, Apple made only one device: the original iPhone. With only one device, you only had to cater to one screen size.

But as years passed, Apple introduced more and more screen sizes as well as the iPad. Now, developers have to cater to a plethora of screen sizes. To make things easier, Apple introduced Auto Layout.

Auto What?

Auto Layout is a system of constraints, or UI-based rules, that govern the size and position of elements on the screen. That may sound simple on the surface but, as you’ll see in this tutorial, Auto Layout can get quite complex very quickly! With this in mind, you can use Auto Layout in two forms: via the Storyboard or programmatically.

I’m sure every developer has tried making an app via Storyboards. It’s simple, concise and has almost no learning curve.

But managing Storyboards can become very difficult if you have a big app. Additionally, a big app often means a big team. Collaborating with Storyboards is not an ideal experience if merge conflicts occur.

Implementing Auto Layout programmatically is a great solution to these problems. You can clearly see which constraints are applied. If merge conflicts occur they are in a Swift file, which you’re used to solving, and not in an alienated XML file that Storyboard generates behind the scenes.

Implementing Auto Layout by Code

There are different techniques you can use when implementing Auto Layout programmatically. The NSLayoutConstraint class defines a relationship between two objects. The Auto Layout Visual Format Language, or VFL, allows you to define constraints by using an ASCII formatted string.

For this tutorial, you’ll use the NSLayoutAnchor class. It has a very fluent API for creating constraints in code.

Gallery Example

In this tutorial, you’ll make a Gallery App using programmatic constraints. The app consists of a 2×2 grid of cards, each of which has the image of a character and its name. By harnessing the power of Auto Layout, you’ll ensure the app is consistent in both landscape and portrait layouts.

Getting Started

Use the Download Materials button at the top or bottom of this tutorial to download the Gallery begin project. Build and run. You’ll get a blank screen:

Since you’ll make this app with code, you’ll need to get rid of the Main.storyboard and make the necessary adjustments so your app gets an entry point.

Select Main.storyboard in the Project navigator and delete it from the project. Now, click on the project in the Project navigator, find the Main Interface option, and delete Main from there as well.

Now, go over to the Info.plist, and expand the Application Scene Manifest entry. Keep on expanding the entries till you see the Storyboard Name entry and then delete it from there. Also delete the Main storyboard file base name entry from the plist.

You now have an app with no storyboard and no entry point. Not very useful. :]

Now you need to set up SceneDelegate.swift to create an entry point for the app. Open it and enter the following code in scene(_:willConnectTo session:options connectionOptions:):

guard let windowScene = scene as? UIWindowScene else { return }
//1
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
//2
window?.windowScene = windowScene
//3
window?.rootViewController = GalleryController()
//4
window?.makeKeyAndVisible()

Here’s what you did:

  1. First, you set the global window variable to a newly created UIWindow with it’s frame as the bounds of the windowScene.
  2. Next you set the windowScene property of the window to the unwrapped windowScene property.
  3. Next, we assign the rootViewController of the window to an instance of GalleryViewController.
  4. Finally, you made the window the key window and also made it visible.

Build and run the app. Since you set the root view controller of the window to GalleryController it becomes the entry point of the app. You’ll see a blank screen again:

Adding Your First Constraints

To layout a view correctly, you have to set its constraints to something meaningful so Auto Layout can figure out its x-position, y-position, width and height. There are certain properties on every UIView:

You have to set a combination of these constraints so Auto Layout can figure out the view’s position on the screen.

Note: The leading and trailing anchors represent a left-to-right reading locale in the above diagram. For a right-to-left locale, the leading and trailing constraints are swapped automatically.

Open GalleryController.swift and have a quick look at the code already there. The GalleryController already creates four instances of a CardView — one for each of the cards you want to show. Notice that the translatesAutoresizingMaskIntoConstraints property is set to false because you want Auto Layout to dynamically calculate the views’ positions and sizes according to their constraints.

viewDidLoad() then calls two pre-defined methods to add cards to the view and set up the card constraints. In setupViews() only one card is currently being added to the view and setupConstraints() — where you’ll create the constraints — is currently empty. It’s time to fix that.

Insert the following code in setupConstraints():

//1
NSLayoutConstraint.activate([
  //2
  cardView1.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  cardView1.centerYAnchor.constraint(equalTo: view.centerYAnchor),
  //3
  cardView1.widthAnchor.constraint(equalToConstant: 120),
  cardView1.heightAnchor.constraint(equalToConstant: 200)
])

Here’s what you did:

  1. You added a call to NSLayoutConstraint to activate an array of constraints. The array contains:
  2. centerX and centerY constraint anchors to make sure the card is laid out in the exact horizontal and vertical center of the view.
  3. width and height constraint anchors to fix the width and height of the card.

Build and run the app and you’ll see the following screen:


Wait, what? The card is not what you expected.

That’s because you still have to add layout constraints to the subviews of the card, imageView and textLabel. Go to CardView.swift and insert the following code in setupViews():

addSubviews(imageView, textLabel)

This will add your subviews to the card view’s hierarchy, but these need constraints to position them correctly. Add the following code in setupConstraints():

//1
NSLayoutConstraint.activate([
  imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: padding),
  imageView.leadingAnchor.constraint(
    equalTo: self.leadingAnchor,
    constant: padding),
  imageView.trailingAnchor.constraint(
    equalTo: self.trailingAnchor,
    constant: -padding)
])


NSLayoutConstraint.activate([
  //2
  textLabel.topAnchor.constraint(
    equalTo: imageView.bottomAnchor,
    constant: padding),
  //3
  textLabel.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
  textLabel.trailingAnchor.constraint(
    equalTo: self.trailingAnchor,
    constant: -padding),
  textLabel.bottomAnchor.constraint(
    equalTo: self.bottomAnchor,
    constant: -padding)
])

Here’s a breakdown of what you added:

  1. You’ve given the imageView a topAnchor, leadingAnchor and trailingAnchor constraint. This tells the imageView where its top, left and right sides should be positioned in relation to the parent view (referred to by self here).
  2. Then you give textLabel a topAnchor that is constrained relative to the bottom of imageView — wherever the bottom of the image is, the label will be padding points below it.
  3. You then add leadingAnchor, trailingAnchor and bottomAnchor constraints, telling the textLabel where its left, right and bottom sides should be positioned, relative to the parent view.
Note: You used negative padding for bottom and trailing constraints. This is because the padding is measured from the parent’s trailing and bottom edges respectively, so it has to be negative in the x or y direction respectively too.

Build and run the app. You’ll see this:

Notice that the textLabel is not the right size. That’s because you didn’t provide a constraint to determine the position of the bottom of the imageView and it doesn’t know when to stop expanding or contracting. Time to fix that.

Layout Priorities

You must be wondering why the label is shrinking, when you have all the constraints that you want? Actually AutoLayout at this point does not know how much to scale the textLabel or the imageView because neither of them has a fixed height. This results in a Content Priority Ambiguity Error.

For different screen sizes, you’re going to have different amounts of space to distribute between the two elements. So, how should Auto Layout distribute that extra space? Does it give it to the imageView or the textLabel? Or do they both get equal amounts?

If you don’t solve this problem yourself, then Auto Layout will try to do it for you, and the results are unpredictable.

The solution here is to set the label’s Content Hugging priority. Here, Hugging means something like size to fit. A higher value of Content Hugging will make the label stick to its original bounds and not expand.

You’ll also have to set the Compression Resistance priority of the label. Here, Compression Resistance means the label will resist compressing. A higher value of Compression Resistance will prevent the label from compressing or shrinking with respect to other views.

Add these lines in setupConstraints():

textLabel.setContentHuggingPriority(.defaultLow + 1, for: .vertical)
textLabel
  .setContentCompressionResistancePriority(.defaultHigh + 1, for: .vertical)

With this:

  • You set the contentHuggingPriority for textLabel to a value slightly higher than the default (which is what the imageView is still using). Similarly, you set the contentCompressionResistance to a value slightly higher than the default. The scale used for both is arbitrary, going between 0 and 1000.

Build and run the app. You’ll see the textLabel take the correct size now:

Laying Out the Rest of the Cards

Now that you’ve completely laid out the first CardView, hop back to GalleryController.swift and layout all of the other cards.

First, replace the code in setupViews() with the following:

view.addSubviews(cardView1, cardView2, cardView3, cardView4)

This adds the rest of the subviews to the view’s hierarchy.

Next, replace the code in setupConstraints() with this:

let safeArea = view.safeAreaLayoutGuide
let viewFrame = view.bounds
//card 1
NSLayoutConstraint.activate([
  cardView1.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  cardView1.topAnchor.constraint(equalTo: safeArea.topAnchor),
  cardView1.widthAnchor.constraint(equalToConstant: viewFrame.width/2),
  cardView1.heightAnchor.constraint(equalToConstant: viewFrame.height/2)
])
    
//card 2
NSLayoutConstraint.activate([
  cardView2.leadingAnchor.constraint(equalTo: cardView1.trailingAnchor),
  cardView2.topAnchor.constraint(equalTo: safeArea.topAnchor),
  cardView2.widthAnchor.constraint(equalToConstant: viewFrame.width/2),
  cardView2.heightAnchor.constraint(equalToConstant: viewFrame.height/2),
  cardView2.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
    
//card 3
NSLayoutConstraint.activate([
  cardView3.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  cardView3.topAnchor.constraint(equalTo: cardView1.bottomAnchor),
  cardView3.widthAnchor.constraint(equalToConstant: viewFrame.width/2),
  cardView3.heightAnchor.constraint(equalToConstant: viewFrame.height/2),
  cardView3.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor)
])
    
//card 4
NSLayoutConstraint.activate([
  cardView4.leadingAnchor.constraint(equalTo: cardView3.trailingAnchor),
  cardView4.topAnchor.constraint(equalTo: cardView2.bottomAnchor),
  cardView4.widthAnchor.constraint(equalToConstant: viewFrame.width/2),
  cardView4.heightAnchor.constraint(equalToConstant: viewFrame.height/2),
  cardView4.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor)
])

Here’s a breakdown:

  • cardView1 has a leading constraint, so it fixes itself with the leading edge of the view. Similarly, cardView2 has a trailing constraint so it fixes itself with the trailing edge of the view. Both have a top constraint so they align themselves to the top of the view.
  • cardView3 and cardView4 have similar leading and trailing constraints to cardView1 and cardView2, but have a bottom constraint instead of a top constraint. They also have a top constraint which aligns to the bottom of cardView1 and cardView2 respectively.
  • All of these views have a width and height constraint equal to half the view’s width and height respectively.
  • You also constrain the subviews to the view’s safeAreaLayoutGuide‘s top and bottom constraints to automatically take care of devices with notches, such as the iPhone X and iPhone 11.

Build and run the app. You’ll see the cards layout exactly as you need them. :]

But as soon as you turn your simulator into landscape mode, it doesn’t look right. You can turn your simulator to landscape mode pressing Command-right arrow or by selecting Simulator ▸ Hardware ▸ Rotate Right.

This is because you haven’t accounted for the change in the view’s width and height when the phone isn’t in the portrait orientation. It’s a good idea to avoid making constraints to sizes that could change, like the width or height of another view.

To fix this, you need to add constraints that take care of themselves even if the view’s orientation changes. You’ll do this by using the multiplier property of each constraint to allow Auto Layout to calculate the actual values each time it makes a layout pass.
Also note that we add all constraints to the view’s safeAreaLayoutGuide, so that it takes care of the notch devices on it’s own.

Replace the code in setupConstraints() once more with the following:

let safeArea = view.safeAreaLayoutGuide

NSLayoutConstraint.activate([
  cardView1.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
  cardView1.topAnchor.constraint(equalTo: safeArea.topAnchor),
  cardView1.widthAnchor.constraint(equalTo: safeArea.widthAnchor, multiplier: 0.5),
  cardView1.heightAnchor.constraint(equalTo: safeArea.heightAnchor, 
                                    multiplier: 0.5),

  cardView2.leadingAnchor.constraint(equalTo: cardView1.trailingAnchor),
  cardView2.topAnchor.constraint(equalTo: safeArea.topAnchor),
  cardView2.widthAnchor.constraint(equalTo: safeArea.widthAnchor, multiplier: 0.5),
  cardView2.heightAnchor.constraint(equalTo: safeArea.heightAnchor, 
                                    multiplier: 0.5),
  cardView2.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),

  cardView3.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
  cardView3.topAnchor.constraint(equalTo: cardView1.bottomAnchor),
  cardView3.widthAnchor.constraint(equalTo: safeArea.widthAnchor, multiplier: 0.5),
  cardView3.heightAnchor.constraint(equalTo: safeArea.heightAnchor, 
                                    multiplier: 0.5),
  cardView3.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor),

  cardView4.leadingAnchor.constraint(equalTo: cardView3.trailingAnchor),
  cardView4.topAnchor.constraint(equalTo: cardView2.bottomAnchor),
  cardView4.widthAnchor.constraint(equalTo: safeArea.widthAnchor, multiplier: 0.5),
  cardView4.heightAnchor.constraint(equalTo: safeArea.heightAnchor, 
                                    multiplier: 0.5),
  cardView4.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor),
])

For each card, you have made its width and height to be half of the available width and height.

Note: This last iteration of setupConstraints() illustrates another best practice when setting up Auto Layout in code. Each call to NSLayoutConstraint.activate(_:) results in a layout pass. As a result, Apple recommends activating all constraints in a single call, as you see above.

Build and run the app once again to get the desired result:

Sweet! You’ve got your cards laid out in both landscape and portrait orientations and you did it completely in code!

Where to Go From Here?

In this tutorial, you learned on how to layout views programmatically, without using Storyboards. It can seem a little bit time consuming and daunting at first. But once you get the hang of it then it’s really a breeze, especially with large teams.

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

If you want to learn more about Auto Layout using Storyboards, check here.

You can also check out SnapKit, an Auto Layout wrapper for iOS, here. It abstracts all boilerplate code and provides a declarative API to make constraints by code. However, you can also write your own wrapper to abstract out the redundant code.

If you have any questions or comments, please don’t hesitate to join the forum discussion below.

Average Rating

4.4/5

Add a rating for this content

19 ratings

Contributors

Comments