Written by Aly Yakan
Today, we’ll walk you through creating constraints programmatically -- building a simple mobile application’s UI completely in code without the use of Storyboards or NIBs. I will not go into the debate of which is better because, simply, they all have their pros and cons, so I’ll just leave this link here which dives more into that matter: https://www.toptal.com/ios/ios-user-interfaces-storyboards-vs-nibs-vs-custom-code.
Overview
This tutorial was written using Xcode 9 and Swift 4. I also assume that you’re familiar with Xcode, Swift, and CocoaPods.
Without further delay, let’s start building our project: a simple Contact Card application. This tutorial aims to teach you how to build your application’s UI in code, and as such, it will not contain any logic about the application’s functionality unless it serves this tutorial’s purpose.
Setting up the project
Start by firing up Xcode -> “Create a New Xcode Project”. Select “Single View App” and press “Next”.
Name the project anything you’d like, I chose to call it “ContactCard”, for no obvious reasons. Untick all three options below and, of course, choose Swift to be the programming language, then press “Next”.
Choose a location on your computer to save the project. Uncheck “Create Git Repository on my Mac” and press “Create”.
Since we won’t be using Storyboards, or NIBs for that matter, go ahead and delete “Main.storyboard”, which can be found in the Project Navigator here:
After that, click on the project in the Project Navigator and under the “General” tab find the section that says “Deployment Info” and delete whatever’s written next to “Main Interface”, usually it is the word “Main”. This is what tells Xcode which Storyboard file to load with the application startup, but since we just deleted “Main.storyboard”, leaving this line would crash the app since Xcode would not find the file.
So go ahead and delete the word “Main”.
Creating ViewController
At this point, if you run the application, a black screen will appear as the application now does not have any source of UI to present for the user, so the next part is where we will provide it with one. Open “AppDelegate.swift” and inside application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?)
, insert this snippet of code:
self.window = UIWindow(frame: UIScreen.main.bounds)
let viewController = ViewController()
self.window?.rootViewController = viewController
self.window?.makeKeyAndVisible()
What this does is basically provide a window for the user’s interaction with the application. This window’s view controller is the one provided with the project on creation, which can be found in “ViewController.swift”. Just as a quick test that everything’s working, head to “ViewController.swift” and inside the viewDidLoad()
method, insert the following line:
self.view.backgroundColor = .blue
Now run the application on your preferred simulator device.
A useful shortcut to navigate between files in Xcode is “⇧⌘O” and then typing the file’s name or even a piece of code that you’re looking for and a list of files will come up on the screen from which you can choose.
After running the application, this should be the result on your simulator’s screen:
Of course we won’t use that hideous blue so just turn the background back to white by replacing .blue
with .white
inside viewDidLoad()
.
Laying out the UI
For laying out our UI, we’ll be using a very helpful library that will make our lives so much easier. Its repo can be found at https://github.com/PureLayout/PureLayout. To install PureLayout, you should first open up your terminal and “cd” into the project’s directory. You can do this by typing cd
, then a space, then drag and drop your project’s folder into the terminal and press “Enter”. Now run the following commands inside the terminal:
pod init
pod install
This should be the output of your terminal after running the second command:
After that, close Xcode, open the folder inside Finder, and you should find something called “.xcworkspace”. This is what we’ll be opening to access our application if we ever need to use CocoaPods. Now locate a file called “PodFile” and write the following line under the phrase use_frameworks!
pod “PureLayout”
Run pod install in your terminal again and then build your project by pressing “Command + B”.
Coffee break
Now that everything is set up, let’s start with the real work. Head over to “ViewController.swift” and grab a cup of coffee because here’s what the final result will look like:
Creating an ImageView
Insert the line import PureLayout
underneath import UIKit
to be able to use the library in this file. Next, underneath the class declaration and outside of any function, we’ll start by creating the Avatar ImageView
lazy variable as follows with this snippet of code:
lazy var avatar: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "avatar.jpg"))
imageView.autoSetDimensions(to: CGSize(width: 128.0, height: 128.0))
imageView.layer.borderWidth = 3.0
imageView.layer.borderColor = UIColor.lightGray.cgColor
imageView.layer.cornerRadius = 64.0
imageView.clipsToBounds = true
return imageView
}()
As for the image, get any image on your desktop that you’d like to use as an avatar, draw and drop it in Xcode under folder, which in my case is “ContactCard”, and check on “Copy items if needed”.
Then click “Finish”. After that, write that file’s name along with its extension inside the declaration of the UIImage
instead of “avatar.jpg”.
For those of you who don’t know, lazy variables are like normal variables except they do not get initialized (or allocated any memory space) until they are needed, or being called, for the first time. This means that lazy variables don’t get initialized when the view controller is initialized, but rather they wait until a later point when they are actually needed, which saves processing power and memory space for other processes. These are especially useful in the case of initializing UI components.
PureLayout in action
As you can see inside the initialization, this line imageView.autoSetDimensions(to: CGSize(width: 128.0, height: 128.0))
is PureLayout in action. With just a single line, we set a constraint for both the height and width of the UIImageView
and all the necessary NSLayoutConstraint
lines are created without the hassle of dealing with their huge function calls. If you’ve dealt with creating constraints programmatically, then you must have fallen in love by now with this brilliant library.
To make this image view round, we set its corner radius to half its width, or height, which is 64.0 points. Also, for the image itself to respect the roundness of the image view, we set the clipsToBounds
property to true, which tells the image that it should clip anything outside the radius that we just set.
We then move to creating a UIView
that will serve as the upper part of the view behind the avatar which is colored in gray. The following lazy var is a declaration for that view:
lazy var upperView: UIView = {
let view = UIView()
view.autoSetDimension(.height, toSize: 128)
view.backgroundColor = .gray
return view
}()
Adding subviews
Before we forget, let’s create a function called func addSubviews()
that adds the views we just created (and all the other views that we’re going to create) as subviews to the view controller’s view:
func addSubviews() {
self.view.addSubview(avatar)
self.view.addSubview(upperView)
}
And now add the following line to viewDidLoad(): self.addSubviews()
Setting up constraints
Just to get a picture of how far we are, let’s setup the constraints for these two views. Create another function called func setupConstraints() and insert the following constraints:
func setupConstraints() {
avatar.autoAlignAxis(toSuperviewAxis: .vertical)
avatar.autoPinEdge(toSuperviewEdge: .top, withInset: 64.0)
upperView.autoPinEdge(toSuperviewEdge: .left)
upperView.autoPinEdge(toSuperviewEdge: .right)
upperView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
}
Now inside viewDidLoad()
call setupConstraints()
by adding its function call as follows: self.setupConstraints()
. Add this AFTER the call to addSubviews()
. This should be the final output:
Bring to front
Oops, that doesn’t seem right. As you can see, our upperView
lays on top of the avatar
. This is because we added avatar
as a subview before upperView
, and since those subviews are arranged in a stack of some sort, then it is only natural to see this result. To fix this, we can just replace those two lines with each other, but there is also another trick that I want to show you, which is: self.view.bringSubview(toFront: avatar)
.
This method will bring the avatar all the way from the bottom of the stack right back to the top, no matter how many views there were above it. So choose whichever method you’d like. Of course for readability, it’s better to add the subviews in the order that they should appear above the other, if they would ever happen to intersect, while keeping in mind that the first added subview will be at the bottom of the stack and so any other intersecting view will appear on top of it.
And this is how it should really look like:
Creating segmented control
Moving on, we’ll now create the segmented control, which is the gray bar that contains three sections. Creating the segmented control is actually simple. Just do the following:
lazy var segmentedControl: UISegmentedControl = {
let control = UISegmentedControl(items: ["Personal", "Social", "Resumè"])
control.autoSetDimension(.height, toSize: 32.0)
control.selectedSegmentIndex = 0
control.layer.borderColor = UIColor.gray.cgColor
control.tintColor = .gray
return control
}()
I believe everything is clear, the only different thing is that upon initialization we provide it with an array of strings, each string representing one of our desired sections’ title. We also set the selectedSegmentIndex to 0, which tells the segmented control to highlight/choose the first segment upon initialization. The rest is just styling which you can play around with.
Now let’s go ahead and add it as a subview by inserting the following line to the end of func addSubviews(): self.view.addSubview(segmentedControl)
and its constraints will be:
segmentedControl.autoPinEdge(toSuperviewEdge: .left, withInset: 8.0)
segmentedControl.autoPinEdge(toSuperviewEdge: .right, withInset: 8.0)
segmentedControl.autoPinEdge(.top, to: .bottom, of: avatar, withOffset: 16.0)
Take a moment to wrap your head around these. We tell the segmented control that we want to pin it to the left side of its superview, however, we want a a little bit of spacing instead of sticking it directly to the edge of the screen. If you notice, I’m using what is called an eight-point grid where all spacings and sizes are a multiple of eight. I do the same to the right side of the segmented control. As for the last constraint, it simply says to pin its top to the bottom of avatar with a spacing of 16 points.
After adding the constraints above to func setupConstraints()
, run the code and make sure that it looks like the following:
Adding a button
Now comes the last piece of UI for this small tutorial, which is the “Edit” button. Add the following lazy variable:
lazy var editButton: UIButton = {
let button = UIButton()
button.setTitle("Edit", for: .normal)
button.setTitleColor(.gray, for: .normal)
button.layer.cornerRadius = 4.0
button.layer.borderColor = UIColor.gray.cgColor
button.layer.borderWidth = 1.0
button.tintColor = .gray
button.backgroundColor = .clear
button.autoSetDimension(.width, toSize: 96.0)
button.autoSetDimension(.height, toSize: 32.0)
return button
}()
Don’t be alarmed by how big the initialization is, but pay attention to how I set the title and its color by calling the function button.setTitle and button.setTitleColor. For certain reasons, we cannot set a button’s title by accessing its titleLabel directly and this is because there are different states for a button and many people would find it convenient to have different titles/colors for different states.
Now add the button as a subview like the rest of the components and add the following constraints to have it appear where it is supposed to be:
editButton.autoPinEdge(.top, to: .bottom, of: upperView, withOffset: 16.0)
editButton.autoPinEdge(toSuperviewEdge: .right, withInset: 8.0)
Here we only set the right and top constraints for the button, and since we gave it a size, it won’t expand and will need nothing else. Now go ahead and run the project to see the final result:
Some final notes
Play around, add as many UI elements as you want. Try to re-create any application’s views that you see challenging. Start simple and build up from there. Try to draw the UI components on a piece of paper so you can imagine how they fit together.
Learn more at blog.instabug.com.
Top comments (2)
Thanks for this guide! I always forget those first few steps. PureLayout seems really nice too.
Apple pushes the interface builder so hard, but programmatic layouts are definitely the way to go. NIBs are a pain to manage in version control, and Storyboards are even worse. Programmatic layouts are just so much easier to maintain.
Nice Article bro, keep the good work