4 Construct Auto Layout with Code¶
There are two ways to implement Auto Layout into your projects. You already learned how to implement Auto Layout using Interface Builder. Now, it’s time to learn the second approach: using code.
Almost every iOS developer eventually raises the question: Should you construct your UI using storyboards or code? There is no silver bullet, and no one-size-fits-all solutions; however, there are solutions that fit much better based on your specific needs and requirements.
To help you achieve fluency in constructing UIs using code, you’ll learn about the following topics in this chapter:
- Launching a storyboard view controller using code.
- Launching a non-storyboard view controller using code.
- Refactoring Interface Builder UIs into code.
- Using visual format language to construct Auto Layout.
- Benefits and drawbacks of constructing Auto Layout using code.
As a developer, you’ll see projects implement their UIs using Interface Builder, code, and in some cases, both approaches within the same project. To build optimized solutions for new projects, and to help maintain existing projects, it’s vitally important to understand both methods of building an app’s UI.
By the end of this chapter, you’ll know how to use code interchangeably with Interface Builder. You’ll also gain the knowledge to make more decisive presentation logic decisions to achieve more optimal solutions.
Launching a view controller from the storyboard in code¶
Open MessagingApp.xcodeproj in the starter folder, and then open the project target’s general settings. Set the Main Interface text field to empty.
Build and run, and you’ll see a black screen.
With the Interface Builder implementation, the app launches the initial view controller of the storyboard set in the target’s Main Interface. To do something similar in code, you need to take a different approach.
Open AppDelegate.swift and replace the code inside application(_:didFinishLaunchingWithOptions:)
with the following:
// 1
let storyboard = UIStoryboard(name: "TabBar", bundle: nil)
// 2
let viewController =
storyboard.instantiateInitialViewController()
// 3
window = UIWindow(frame: UIScreen.main.bounds)
// 4
window?.rootViewController = viewController
// 5
window?.makeKeyAndVisible()
return true
Here’s what you’ve done:
- Initialize the storyboard in code using the storyboard name.
- Create a reference to the storyboard’s initial view controller.
- Set the app delegate’s
window
using the device’s screen size as the frame. - Set the window’s root view controller to the storyboard’s initial view controller.
- By calling
makeKeyAndVisible()
on yourwindow
,window
is shown and positioned in front of every window in your app. For the most part, you’ll only need to work with one window. There are instances where you’d want to create new windows to display your app’s content. For example, you’ll work with multiple windows when you want to support an external display in your app. Chapter 17, “Auto Layout for External Displays”, covers supporting external displays.
When you use storyboards, the app delegate’s window property is automatically configured. In contrast, when you use code, you need to do more manual work. This is generally true when using code over Interface Builder.
Build and run, and you’ll see the following:
That’s only a taste of what it’s like using more code and less Interface Builder. Are you ready for some more?
Launching a view controller without initializing storyboard¶
You now know how to launch a view controller in code from a storyboard. But no set rule dictates that a project can’t mix storyboards/.xibs and code. For example, there may come a time where your team’s objective is to refactor an existing codebase that uses storyboards/.xibs into one that uses code. You’re going to do that now.
First, delete the following Interface Builder files:
- Profile.storyboard
- TabBar.storyboard
Then, remove all of the code inside ProfileViewController
’s body except for viewDidLoad()
.
When you’re done, ProfileViewController.swift will look like this:
import UIKit
final class ProfileViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
Up next, you’ll create your UI properties and layout in code rather than using Interface Builder.
Open AppDelegate.swift.
Note
Press Command-Shift-O to open quickly. Type AppDelegate. Xcode will suggest AppDelegate.swift. Press Return to open the file.
Replace the existing code inside application(_:didFinishLaunchingWithOptions:)
with the following:
let viewController = TabBarController()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = viewController
window?.makeKeyAndVisible()
return true
Here, you command the app to initialize TabBarController
using its initializer method.
Build and run, and you’ll see a tab bar controller with a black background.
When you add a view controller onto a storyboard, the background view is set to white by default. In code, the view controller’s view has a nil
background color, which shows up as black. Once again, this is one of the automated steps as a result of using Interface Builder.
One of the benefits of initializing your view controller with its initializer method is that the view controller’s type is explicit. Whereas when you initialize a view controller from a storyboard’s initial view controller, you need additional code to ensure that the view controller returned is TabBarController
.
Next, you’ll rebuild the profile view controller user interface in code.
Building out a view controller’s user interface in code¶
When you built the UI in Interface Builder, the profile view controller had a header view with a gray background. The header view encapsulated the main stack view, and the main stack view encapsulated two stack views. One stack view contained the profile image view and the full name label. The other stack view contained the action buttons. It’s time to recreate those user interfaces in code.
Create a new Swift file named ProfileHeaderView.swift inside the User Profile/Viewsgroup.
Replace the existing template code with the following:
import UIKit
final class ProfileHeaderView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .groupTableViewBackground
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
With the code above, you override init(frame:)
. The method you’ve overridden is an initializer method. As the method name implies, this is where you add your initialization code, such as setting the view’s background color.
Afterward, init(coder:)
takes care of the object’s archiving and unarchiving processes for Interface Builder. This method is handy when you launch an object from storyboard/.xib and want to configure the object at the initialization phase. Generally, when a view is created in code, init(frame:)
is the initializer used, and when a view is created from a storyboard or .xib, init(coder:)
is used instead.
Layout anchors¶
Before the introduction of layout anchors, developers created constraints in code using NSLayoutConstraint
initializers. NSLayoutConstraint
describes the relationship between two user interface objects, and that relationship has to satisfy the Auto Layout engine. Although this approach still works, there’s room for improvements in the code readability and cleanliness departments. Consequently, Apple introduced layout anchors for this purpose.
NSLayoutAnchor
is a generic class built to create layout constraints using a fluent interface.
Are you ready for a comparison between constraints created using NSLayoutConstraint
initializers and constraints created using layout anchors? Sure you are!
Say you’d like to create a square view centered vertically and horizontally inside of a view controller’s view. To create this layout in code using NSLayoutConstraint
initializers, it would look like this:
NSLayoutConstraint(
item: squareView,
attribute: .centerX,
relatedBy: .equal,
toItem: view,
attribute: .centerX,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint(
item: squareView,
attribute: .centerY,
relatedBy: .equal,
toItem: view,
attribute: .centerY,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint(
item: squareView,
attribute: .width,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 0,
constant: 100).isActive = true
NSLayoutConstraint(
item: squareView,
attribute: .width,
relatedBy: .equal,
toItem: squareView,
attribute: .height,
multiplier: 1,
constant: 0).isActive = true
Whereas, creating constraints in code using layout anchors would look like this:
squareView.centerXAnchor.constraint(
equalTo: view.centerXAnchor).isActive = true
squareView.centerYAnchor.constraint(
equalTo: view.centerYAnchor).isActive = true
squareView.widthAnchor.constraint(
equalToConstant: 100).isActive = true
squareView.widthAnchor.constraint(
equalTo: squareView.heightAnchor).isActive = true
Between the two approaches, using layout anchors is comparatively cleaner, more succinct and more readable. The benefits of using layout constraints don’t just stop at the fluent interface.
Another benefit you get for using layout anchors is type checking. Type checking mitigates the chance of creating invalid constraints in code. Type checking helps validate horizontal axis, vertical axis, height and width constraints.
If you try to compile the following code:
view.leadingAnchor.constraint(equalTo: squareView.topAnchor)
The compiler won’t allow it because this code tries to create a constraint between an NSLayoutXAxisAnchor
and an NSLayoutYAxisAnchor
. The compiler recognizes that the constraint is invalid, and invalid constraints are reported as build-time errors thanks to layout anchor’s type checking.
Using layout anchors won’t prevent you from creating invalid constraints entirely. Despite the type checking efforts, layout anchors are still susceptible to invalid constraints. This is because the compiler allows you to create constraints between one view’s leading and trailing layout anchor and another view’s left/right layout anchor. This constraint is allowed because both anchors are x-axis layout anchors. Therefore, the constraint compiles fine in Xcode.
As it turns out, however, the Auto Layout engine restricts the relationship between trailing and leading layout anchors with left or right layout anchors. Such constraints will cause a runtime crash.
Now that you understand the essence of layout anchors and their benefits, it’s a great time to put them to work in your project.
Setting up profile header view¶
Open ProfileViewController.swift and add the following property to ProfileViewController
:
private let profileHeaderView = ProfileHeaderView()
Next, add the following method to ProfileViewController
:
private func setupProfileHeaderView() {
// 1
view.addSubview(profileHeaderView)
// 2
profileHeaderView.translatesAutoresizingMaskIntoConstraints =
false
// 3
profileHeaderView.leadingAnchor.constraint(
equalTo: view.leadingAnchor).isActive = true
profileHeaderView.trailingAnchor.constraint(
equalTo: view.trailingAnchor).isActive = true
profileHeaderView.topAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
profileHeaderView.bottomAnchor.constraint(
lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor)
.isActive = true
}
With this code, you:
- Add
profileHeaderView
as a subview of the view controller’s view. - Set
profileHeaderView.translatesAutoresizingMaskIntoConstraints
tofalse
. Before Auto Layout behaves as you’d expect from Interface Builder, this is a property that you need to remember to always set tofalse
. It’s set totrue
by default. Autoresizing mask is Auto Layout’s predecessor. It’s a layout system that’s a lot less comprehensive when compared to Auto Layout. - Set and activate the profile header view’s leading, trailing, top and bottom anchors.
You can now refactor the code you added earlier.
Replace the code in setupProfileHeaderView()
with this:
view.addSubview(profileHeaderView)
profileHeaderView.translatesAutoresizingMaskIntoConstraints =
false
NSLayoutConstraint.activate(
[profileHeaderView.leadingAnchor.constraint(
equalTo: view.leadingAnchor),
profileHeaderView.trailingAnchor.constraint(
equalTo: view.trailingAnchor),
profileHeaderView.topAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.topAnchor),
profileHeaderView.bottomAnchor.constraint(
lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor)])
This code looks cleaner and is relatively more performant because of the Auto Layout engine’s life cycle. Instead of bringing up the Auto Layout engine three additional times, you bring on the Auto Layout engine once and activate a list of constraints at one time versus individually. Although Interface Builder internal configurations aren’t completely explicit, it’s likely that this is also an automatic optimization implemented for developers who work with Interface Builder.
Now, add the following code to the end of viewDidLoad()
:
view.backgroundColor = .white
setupProfileHeaderView()
This code sets the view’s background color to white. It then calls the method you implemented to set up the profile header view.
Build and run, and you’ll see the following screen:
Refactoring profile image view¶
It’s time to create the profile image view in code.
Open ProfileImageView.swift and remove @IBDesignable from the class declaration.
After that, remove everything inside ProfileImageView
, and replace the body of ProfileImageView
with the following:
// 1
enum BorderShape: String {
case circle
case squircle
case none
}
let boldBorder: Bool
var hasBorder: Bool = false {
didSet {
guard hasBorder else { return layer.borderWidth = 0 }
layer.borderWidth = boldBorder ? 10 : 2
}
}
// 2
private let borderShape: BorderShape
// 3
init(borderShape: BorderShape, boldBorder: Bool = true) {
self.borderShape = borderShape
self.boldBorder = boldBorder
super.init(frame: CGRect.zero)
backgroundColor = .lightGray
}
// 4
convenience init() {
self.init(borderShape: .none)
}
// 5
required init?(coder aDecoder: NSCoder) {
self.borderShape = .none
self.boldBorder = false
super.init(coder: aDecoder)
}
Here’s how this code works:
- First, you declare a
BorderShape
enum. This contains an additionalnone
case for when you want the image view to have no particular border shape. Notice that the access modifier forBorderShape
is no longerprivate
. Instead, it’sinternal
since there is no other explicit access modifier declared on the property. This is so thatBorderShape
becomes accessible to other objects when they initializeProfileImageView
.boldBorder
is used to determineProfileImageView
’s layer border width for whenhasBorder
istrue
. WhenhasBorder
isfalse
, the view’s layer border width is simply set to zero. - You also have
borderShape
, which has changed from a string type to use the enum. One of the great benefits of building your UI in code is that you can make use of a lot more great Swift features. With Interface Builder, configuring a view’s property using an enum isn’t really an option. - Then, there’s
init(borderShape:boldBorder:)
. This method initializesProfileImageView
by taking in theborderShape
andboldBorder
parameters and setting the class properties appropriately. Then, the method calls the superclass initializer method and passes in.zero
for the frame. The frame size isn’t a concern since theProfileImageView
will use Auto Layout to determine the view’s size and position. Next, you set the background color to light gray. - Here, you have a convenience initializer which mitigates the need to pass in a
BorderShape
into the first initializer. This convenience initializer allows you initialize aProfileImageView
asProfileImageView()
. When you use this convenience initializer,borderShape
is set tonone
. - Finally, you have
init(coder:)
, which is used when you createProfileImageView
in Interface Builder. In this case, you initializeborderShape
tonone
andboldBorder
tofalse
.
You’ve added the value type, properties and initializer methods. It’s time to configure the border.
Add the following code to ProfileImageView
:
// 1
override func layoutSubviews() {
super.layoutSubviews()
setupBorderShape()
}
private func setupBorderShape() {
hasBorder = borderShape != .none
// 2
let width = bounds.size.width
let divisor: CGFloat
switch borderShape {
case .circle:
divisor = 2
case .squircle:
divisor = 4
case .none:
divisor = width
}
let cornerRadius = width / divisor
layer.cornerRadius = cornerRadius
}
Here’s what you added:
layoutSubviews()
is called when the constraint-based layout has finished its configuration. This is the time whenProfileImageView
sets up the border shape of the view by callingsetupBorderShape()
.- Inside of
setupBorderShape()
,borderShape
determines the corner radius. WhenborderShape
is acircle
, the layer’s corner radius will be half of the view’s width. WhenborderShape
is asquircle
, the layer’s corner radius will be a quarter of the view’s width. WhenborderShape
is set tonone
, the layer’s corner radius will be1
.
You’re well on your way to becoming an Auto Layout code warrior. But there’s still more to do.
Refactoring profile name label¶
Open ProfileNameLabel.swift and remove @IBDesignable from the class declaration.
Next, replace everything inside of ProfileNameLabel
with the following code:
// 1
override var text: String? {
didSet {
guard let words = text?
.components(separatedBy: .whitespaces)
else { return }
let joinedWords = words.joined(separator: "\n")
guard text != joinedWords else { return }
DispatchQueue.main.async { [weak self] in
self?.text = joinedWords
}
}
}
// 2
init(fullName: String? = "Full Name") {
super.init(frame: .zero)
setTextAttributes()
text = fullName
}
// 3
required init?(coder: NSCoder) {
super.init(coder: coder)
}
// 4
private func setTextAttributes() {
numberOfLines = 0
textAlignment = .center
font = UIFont.boldSystemFont(ofSize: 24)
}
With this code, you:
- Override
text
to add adidSet
observer. Every time thetext
value changes,didSet
will get called. Safely unwrap using theguard
statement to make sure that there’s at least one word to extract fromtext
. Iftext
isnil
, simply return and do nothing. Iftext
is notnil
, then add\n
between every word. The\n
separator creates a line spacing between each word. Afterward,ProfileImageView
’s text is set to the new string joined by a separator that you create. - The initializer method takes parameters to set its properties and calls
init(frame:)
on the super class. Next, you callsetTextAttributes()
to set up someUILabel
properties. - Implement the required initializer for when Interface Builder initializes
ProfileNameLabel
. - Finally,
setTextAttributes
sets thenumberOfLines
,textAlignment
andfont
properties.
So, that’s how you create the profile name label in code. You’re ready to look at refactoring the stack views from Interface Builder.
Refactoring stack views¶
To begin rebuilding the stack views in code, add the following extension to the bottom of ProfileHeaderView.swift (outside of the class):
private extension UIButton {
static func createSystemButton(withTitle title: String)
-> UIButton {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
return button
}
}
This is a convenience method you can use to create a system button with the given title.
Next, add the following properties to ProfileHeaderView
:
// 1
private let profileImageView =
ProfileImageView(borderShape: .squircle)
private let leftSpacerView = UIView()
private let rightSpacerView = UIView()
private let fullNameLabel = ProfileNameLabel()
// 2
private let messageButton =
UIButton.createSystemButton(withTitle: "Message")
private let callButton =
UIButton.createSystemButton(withTitle: "Call")
private let emailButton =
UIButton.createSystemButton(withTitle: "Email")
// 3
private lazy var profileImageStackView =
UIStackView(arrangedSubviews:
[leftSpacerView, profileImageView, rightSpacerView])
private lazy var profileStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews:
[profileImageStackView, fullNameLabel])
stackView.distribution = .fill
stackView.axis = .vertical
stackView.spacing = 16
return stackView
}()
private lazy var actionStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews:
[messageButton, callButton, emailButton])
stackView.distribution = .fillEqually
return stackView
}()
private lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews:
[profileStackView, actionStackView])
stackView.axis = .vertical
stackView.spacing = 16
return stackView
}()
With this code, you:
- Initialize the profile image view, left spacer view, right spacer view and full name label.
- Initialize the three action buttons using
createSystemButton(withTitle:)
, which you added earlier. - Initialize four stack views. The first stack view contains the profile image view and the spacer views. The second stack view encapsulates the profile image stack view and the full name label. The third stack view includes the action buttons. The fourth stack view stacks the second and third stack views together.
You’re ready to set up the layout of the stack view. Add the following method to ProfileHeaderView
:
private func setupStackView() {
// 1
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
// 2
NSLayoutConstraint.activate(
[stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
stackView.leadingAnchor.constraint(
greaterThanOrEqualTo: leadingAnchor, constant: 20),
stackView.leadingAnchor.constraint(
lessThanOrEqualTo: leadingAnchor, constant: 500),
stackView.bottomAnchor.constraint(
equalTo: bottomAnchor, constant: -8),
stackView.topAnchor.constraint(
equalTo: topAnchor, constant: 26),
profileImageView.widthAnchor.constraint(
equalToConstant: 120),
profileImageView.widthAnchor.constraint(
equalTo: profileImageView.heightAnchor),
leftSpacerView.widthAnchor.constraint(
equalTo: rightSpacerView.widthAnchor)
])
// 3
profileImageView.setContentHuggingPriority(
UILayoutPriority(251),
for: NSLayoutConstraint.Axis.horizontal)
profileImageView.setContentHuggingPriority(
UILayoutPriority(251),
for: NSLayoutConstraint.Axis.vertical)
fullNameLabel.setContentHuggingPriority(
UILayoutPriority(251),
for: NSLayoutConstraint.Axis.horizontal)
fullNameLabel.setContentHuggingPriority(
UILayoutPriority(251),
for: NSLayoutConstraint.Axis.vertical)
fullNameLabel.setContentCompressionResistancePriority(
UILayoutPriority(751),
for: NSLayoutConstraint.Axis.vertical)
messageButton.setContentCompressionResistancePriority(
UILayoutPriority(751),
for: NSLayoutConstraint.Axis.horizontal)
}
Here, you:
- Add
stackView
as the view’s subview. Then, settranslatesAutoresizingMaskIntoConstraints
tofalse
so that your Auto Layout constraints can behave correctly without the interference of the autoresizing masks. - Set up the Auto Layout constraints on
stackView
and its subviews to match the layout behavior from the Interface Builder implementation. - Set the content hugging priority and compression resistance priority on
profileImageView
,fullNameLabel
andmessageButton
to match the layout behavior from the Interface Builder implementation.
Finally, add the following code to init(frame:)
:
setupStackView()
And voilà! You have yourself a ProfileViewController
created entirely in code.
Build and run, and you’ll see something like this:
There’s more to learn about creating constraints in code.
Auto Layout with visual format language¶
Using visual format language is another way of constructing your Auto Layout in code. Building Auto layout in code has come a long way in terms of code readability since the debut of visual format language. As you may have guessed, visual format language isn’t exactly the most user-friendly tool available. So you may be wondering: Why should anyone learn visual format language to construct Auto Layout then?
Here are two reasons to get familiar with it:
- Refactor or maintain legacy code which constructs Auto Layout constraints using visual format language.
- Read and comprehend Auto Layout runtime errors.
The second reason is particularly significant for anyone who works with Auto Layout using Interface Builder or code. Because you’ll see a lot of symbols with visual format language, it’s a good idea to get familiar with them.
Symbols¶
For reference, here are the symbols to describe your layout in visual format language:
|
superview-
standard spacing (usually 8 points; value can be changed if it is the spacing to the edge of a superview)==
equal widths-20-
non-standard spacing (20 points)<=
less than or equal to>=
greater than or equal to@250
priority of the constraint; can have any value between 0 and 1000- 250 - low priority
- 750 - high priority
- 1000 - required priority
Visual format string example¶
H:|-[label(labelHeight)]-16-[imageView(>=250,<=300)]-16-[button(88@250)]-|
Here’s what the string above does to create constraints:
H:
indicates that the constraints are for the horizontal arrangement.|-[label
creates a constraint between the superview’s leading edge and the label’s leading edge.label
should be found in the views dictionary. More on views dictionary in the following sections.label(labelHeight)
sets the label’s height tolabelHeight
. This key should be found in the metrics dictionary. More on the metrics dictionary in the following sections.]-16-[imageView
sets a constraint with 16 spacings betweenlabel
’s trailing edge andimageView
’s leading edge.[imageView(>=250,<=300)]
sets a width greater than or equal to 250 constraint and less than or equal to 300 constraint onimageView
.]-16-[button
sets a 16 spacings constraint betweenimageView
’s trailing edge andbutton
’s leading edge.[button(88@250)]
givesbutton
a width constraint equal to 88 with a low priority. This allows the Auto Layout engine to break this constraint when needed.]-|
sets a constraint with standard spacing betweenbutton
’s trailing edge and the superview’s trailing edge.
Thinking visual format language¶
Despite having the word visual in visual format language, it isn’t precisely the most visual-friendly. It’s important to have the right strategy to think about constraints to effectively create or maintain constraints created using visual format language.
From reading visual format language string inputs, although not visual-friendly code, it can express a lot to the Auto Layout engine. In this section, you’ll learn to express visual format language string inputs.
Horizontal and vertical axes¶
When you think about visual format language, imagine either drawing a line from top-to-bottom or left-to-right on your device. Then, looking at the line you drew, track down the property name of every UI object the line passes through.
Look at the following diagram:
Count the number of red lines on the diagram. There are five. In your constraints code, you’ll create five sets of constraints using visual format language: three horizontal arrangements and two vertical arrangements. Notice the connection? Five lines, five sets of constraints. This is generally true unless you prefer to split the constraints up or utilize the layout options
parameter for certain scenarios when creating your constraints.
You use either an H:
or V:
to specify horizontal or vertical arrangements, respectively, in a visual format string.
Great, you know the sets of constraints to create when you see a layout. Now, you’ll learn to inform the constraints about view position and spacing, and you’ll learn how metrics and views in a visual format string come together to create constraints.
Metrics dictionary¶
You can define metrics string with a dictionary using visual format language. You can then use a metrics key-value pair to define a constraint’s constant or multiplier. You may want to use the metrics dictionary to pass in values for the visual format string to reference.
A metrics dictionary can look like this:
["topSpacing": topSpace,
"textFieldsSpacing": 8]
The dictionary you just saw consists of a key-value pair with a topSpacing
key. The value corresponding to the key is a constant declared as topSpace
. There’s another key-value pair with a textFieldsSpacing
key. It has a value of 8
.
When the visual format string makes use of topSpacing
or textFieldsSpacing
, the value is referenced from the metrics dictionary.
Views dictionary¶
So, how are you going to tell the constraints about the views using visual format language? This is where the views dictionary comes into play. Similar to the metrics dictionary, your key-value pairs consist of a string and the view object for the key and value, respectively.
For example, look at the following dictionary:
["textField": textField,
"imageView": imageView]
Your visual format string can use the textField
string to reference the textField
user interface object. You can also use the imageView
string to reference the imageView
user interface object.
The metrics and views dictionaries will make more sense when you build out your user interface using visual language format.
Layout options¶
When creating your constraints using visual format language, you’ll have the option to use the options
parameter. options
lets you describe the views’ constraints perpendicular to the current layout orientation.
Look at the following diagram:
Imagine the top view has its leading, top, trailing and height constraints. The bottom view has its top and height constraints. The bottom view is missing the horizontal constraints arrangement. This is where layout options are handy.
You can use the following layout options:
[.alignAllLeading, .alignAllTrailing]
This helps you achieve the diagram’s bottom view layout. Also, this tells the Auto Layout engine that you want all of your views in a visual format string to share the same leading and trailing constraints. The top view has its leading and trailing constraints. Thus, the bottom view simply infers these constraints and shares the top view’s leading and trailing constraints.
That’s enough theory for now; you’re ready to get your hands dirty with visual format language code implementation.
Setting up constraints¶
Open NewContactViewController.swift.
You can see that there’s some existing code within NewContactViewController
. The focus of this section is on the Auto Layout constraint construction for profileImageView
, firstNameTextField
and lastNameTextField
using visual format language. You’ll do this in setupViewLayout()
, which is called from viewSafeAreaInsetsDidChange()
.
When the root view’s safe area insets change, iOS calls viewSafeAreaInsetsDidChange()
to inform your app of the latest safe area insets. Upon calling the method, constraints
are deactivated and emptied in preparation for handing the newest constraints.
Add the following code to setupViewLayout()
:
// 1
let safeAreaInsets = view.safeAreaInsets
let marginSpacing: CGFloat = 16
let topSpace = safeAreaInsets.top + marginSpacing
let leadingSpace = safeAreaInsets.left + marginSpacing
let trailingSpace = safeAreaInsets.right + marginSpacing
// 2
var constraints: [NSLayoutConstraint] = []
// 3
view.addSubview(profileImageView)
profileImageView.translatesAutoresizingMaskIntoConstraints =
false
view.addSubview(firstNameTextField)
firstNameTextField.translatesAutoresizingMaskIntoConstraints =
false
view.addSubview(lastNameTextField)
lastNameTextField.translatesAutoresizingMaskIntoConstraints =
false
With this code, you:
- Define top, leading and trailing margin spacing constants that are used to create Auto Layout constraints.
- Initialize an empty array of type
NSLayoutConstraint
collection. - Add the profile image view and text fields to the view hierarchy. You also disable the autoresizing mask, which you need to do for views created in code that use Auto Layout.
Add the following code to the end of setupViewLayout()
:
// 1
let profileImageViewVerticalConstraints =
NSLayoutConstraint.constraints(withVisualFormat:
"V:|-topSpacing-[profileImageView(profileImageViewHeight)]",
options: [],
metrics:
["topSpacing": topSpace, "profileImageViewHeight": 40],
views: ["profileImageView": profileImageView])
constraints += profileImageViewVerticalConstraints
// 2
let textFieldsVerticalConstraints =
NSLayoutConstraint.constraints(withVisualFormat:
"V:|-topSpacing-[firstNameTextField(profileImageView)]-textFieldsSpacing-[lastNameTextField(firstNameTextField)]",
options: [.alignAllCenterX],
metrics: [
"topSpacing": topSpace,
"textFieldsSpacing": 8],
views: [
"firstNameTextField": firstNameTextField,
"lastNameTextField": lastNameTextField,
"profileImageView": profileImageView])
constraints += textFieldsVerticalConstraints
// 3
let profileImageViewToFirstNameTextFieldHorizontalConstraints =
NSLayoutConstraint.constraints(withVisualFormat:
"H:|-leadingSpace-[profileImageView(profileImageViewWidth)]-[firstNameTextField(>=200@1000)]-trailingSpace-|",
options: [],
metrics: [
"leadingSpace": leadingSpace,
"trailingSpace": trailingSpace,
"profileImageViewWidth": 40],
views: [
"profileImageView": profileImageView,
"firstNameTextField": firstNameTextField])
constraints +=
profileImageViewToFirstNameTextFieldHorizontalConstraints
// 4
let lastNameTextFieldHorizontalConstraints =
NSLayoutConstraint.constraints(
withVisualFormat:
"H:[lastNameTextField(firstNameTextField)]",
options: [],
metrics: nil,
views: [
"firstNameTextField": firstNameTextField,
"lastNameTextField": lastNameTextField])
constraints += lastNameTextFieldHorizontalConstraints
// 5
NSLayoutConstraint.activate(constraints)
self.constraints = constraints
In this example, the constraints look something like this on a diagram:
So, with this code, you:
- You create a constraint with
topSpacing
spacings between the superview’s top edge andprofileImageView
’s top edge.profileImageView
’s height constraint is set to equal toprofileImageViewHeight
. Finally, you add the vertical constraints you created into theconstraints
array. - You create a constraint with
topSpacing
spacings between the superview’s top edge andfirstNameTextField
’s top edge.firstNameTextField
’s height is set to equal toprofileImageView
. Next, you create a constraint withtextFieldsSpacing
spacings betweenfirstNameTextField
’s bottom edge andlastNameTextField
’s top edge.lastNameTextField
’s height constraint is set to equal tofirstNameTextField
. Finally, you add the vertical constraints you created into theconstraints
array. - You create a constraint with
leadingSpace
spacings between the superview’s leading edge andprofileImageView
’s leading edge. TheprofileImageView
’s width constraint is set to equal toprofileImageViewWidth
. You add a constraint with standard spacing betweenprofileImageView
’s trailing edge andfirstNameTextField
’s leading edge. ThefirstNameTextField
’s width constraint is set with a required constraint priority and a minimum width value of 200. Before wrapping up the visual format string, you set a spacing equal totrailingSpace
betweenfirstNameTextField
’s trailing edge and the superview’s trailing edge. Finally, you add the horizontal constraint you created into theconstraints
array. - The next set of constraints comes from the second horizontal line coming down from the diagram. You set up
lastNameTextField
’s width equal tofirstNameTextField
. It’s unnecessary to set additional spacing thanks to the centering of the x-axis from step #2. Finally, you add the horizontal constraint you created into theconstraints
array - Activate all of the constraints inside of
constraints
. Then, set the recently created constraints toNewContactViewController
’sconstraints
. This is so you can deactivate the constraints for when the safe area insets change. For example, when the device’s orientation changes.
Build and run. Tap the Contacts tab. Tap the + tab bar button. You’ll see:
There you have it: Your layout built in code using visual language format. However, it’s usually more work for the same design. Plus, visual language format is not the most user-friendly. Having said that, understanding visual format language can still assist you in debugging constraint problems, maintaining legacy codebases, refactoring legacy codebases and more.
Benefits and drawbacks from choosing the code approach¶
Whether you choose Interface Builder or code to layout your user interface, it is an unquestionably subjective matter. Before you come to a decisive conclusion on your approach, have a look at the benefits and drawbacks when using code to construct Auto Layout.
Here are five benefits of using code to construct Auto Layout constraints:
- Everything technical that storyboard can do, code can do too. But, not vice versa.
- Readable merge conflict(s) means less time spent fixing merge conflicts.
- Code compilation time reduction.
- All the user interface logic lives in code. This benefit mitigates events such as having to find whether a property is changed in Interface Builder or code.
- Easy UI maintenance with the right coding infrastructure. Manage UI constants such as fonts, colors and constraint values with ease.
Here are five drawbacks of using code:
- Higher learning curve compared to using Interface Builder.
- Naming conventions and code cleanliness are vital for code maintenance when working on a team.
- Inability to add user interface objects and create constraints for the user interface objects visually like in Interface Builder.
- More developers are familiar with using Interface Builder than developers who are familiar with using code to build an app’s layout. If you work with developers who are less familiar with using code, more time may be needed to properly onboard the developers.
- Opportunity cost of missing out some of the automation Interface Builder provides when you build your layouts in Interface Builder.
Because there’s no one-size-fits-all solution, one of the more important steps to coming to the optimal solution is clearly defining the problem. What exactly are you or your team trying to solve?
There are benefits and drawbacks to using Interface Builder and code. You need to consider the tradeoffs before deciding which to use. Questions such as:
- Does it make sense for a project to sacrifice Interface Builder automation for ease of source control when merge conflicts arise?
- Is it preferable to keep Interface Builder visualizations in the sacrifice of reduced compile time?
Personalization is a big consideration. For example:
- Which of the tradeoffs apply to the scenario at hand?
- What about personal preferences? Everyone has their personal preferences. Some people enjoy using Interface Builder over code, and others do not.
When choosing an approach to build your app’s UI, take time to consider the problems, tradeoffs and personalizations fully. The optimal solution will arise by building a solution tailored to your specific team and project requirements. Ultimately, it’s up to you and your team to decide.
Challenges¶
You’ve reached the end of the chapter. To help solidify your understanding, try these challenges:
- Recreate the
ContactListTableViewController
’s UI entirely in code. - Recreate the
ContactTableViewCell
’s UI entirely in code. - Recreate the
ContactPreviewView
’s UI entirely in code.
Key points¶
- Working with code requires more upfront manual work than working with Interface Builder.
- You can refactor UI layouts built in Interface Builder into code format.
- There are various methods to create Auto Layout constraints using code.
- Learning visual format language, although rarely seen on new projects, can assist you in debugging constraint conflicts and maintaining legacy codebases.
- Consider the pros and cons when choosing between Interface Builder and code approach to creating your UI layout.