Although xib
files offer a mechanism to customize classes, the majority of UIKit views outside of standard frameworks are implemented in custom code. This makes it easier to reason what the intrinsic size should be as well as to receive code patches and understand diffs from version control systems. The downside of this approach is when using Auto Layout, writing the constraints can be a challenge and the intrinsic sizes are often misreported or return the unknown value (-1,-1)
.
A custom view can be implemented as a subclass of UIView
. Subclasses of UIView
are expected to have two initializers; one that takes frame:CGRect
and one that takes a coder:NSCoder
. The frame
is generally used in code, and the rect
specifies the position on screen (0,0
is the top-left) along with the width
and height
. The coder
is used when deserializing from a xib
file.
To allow custom subclasses to be used in either interface builder or instantiated from code, it is a best practice to ensure that both the initializers create the necessary views. This can be done by using a third method, called something similar to setupUI
, that is invoked from both.
Create a class called TwoLabels
that has two labels in a view:
import UIKit class TwoLabels: UIView { var left:UILabel = UILabel() var right:UILabel = UILabel() required init(coder:NSCoder) { super.init(coder:coder) setupUI() } override init(frame:CGRect) { super.init(frame:frame) setupUI() } // ... }
The setupUI
call will add the subviews to the view. Code that goes in here should be executed only once. There isn't a standard name, and often example code will put the setup in one or other of the init
methods instead.
It is conventional to have a separate method such as updateUI
to populate the UI with the current set of data. This can be called repeatedly based on the state of the system; for example, a field might be enabled or disabled based on some condition. This code should be repeatable so that it does not modify the view hierarchy:
func setupUI() { addSubview(left) addSubview(right) updateUI() } func updateUI() { left.text = "Left" right.text = "Right" }
In an explicitly sized environment (where the text label is being set and placed at a particular location), there is a layoutSubviews
method that is called to request the view to be laid out correctly. However, there is a better way to do this, which is to use Auto Layout and constraints.
Auto Layout is covered in the Using Auto Layout section of Chapter 4, Storyboard Applications with Swift and iOS. When creating a user interface explicitly, views must be sized and managed appropriately. The most appropriate way to manage this is to use Auto Layout, which requires constraints to be added in order to set up the views.
Constraints can be added or updated in the updateConstraints
method. This is called after setNeedsUpdateConstraints
is called. Constraints might need to be updated if views become visible or the data is changed. Typically, this can be triggered by placing a call at the end of the setupUI
method:
func setupUI() { // addSubview etc setNeedsUpdateConstraints() }
The updateConstraints
method needs to do several things. To prevent auto-resizing masks being translated into constraints, each view needs to call setTranslatesAutoresizingMaskIntoConstraints
with an argument of false
.
To facilitate the transition between springs and struts (also known as auto-resizing masks) and Auto Layouts, views can be configured to translate springs and struts into Auto Layout constraints. This is enabled by default for all views in order to provide backward compatibility for existing views, but should be disabled when implementing Auto Layouts.
Either the constraints can be incrementally updated or the existing constraints can be removed. A removeConstraints
method allows existing constraints to be removed first:
override func updateConstraints() { setTranslatesAutoresizingMaskIntoConstraints(false) left.setTranslatesAutoresizingMaskIntoConstraints(false) right.setTranslatesAutoresizingMaskIntoConstraints(false) removeConstraints(constraints()) // add constraints here }
Constraints can be added programmatically using the NSLayoutConstraint
class. The constraints added in interface builder are also instances of the NSLayoutConstraint
class.
Constraints are represented as an equation; properties of two objects are related as an equality (or inequality) of the form:
// object.property = otherObject.property * multiplier + constant
To declare that both labels are of equal width, the following can be added to the updateConstraints
method:
// left.width = right.width * 1 + 0 let equalWidths = NSLayoutConstraint( item: left, attribute: .Width, relatedBy: .Equal, toItem: right, attribute: .Width, multiplier: 1, constant: 0) addConstraint(equalWidths)
Although adding individual constraints gives ultimate flexibility, it can be tedious to set up programmatically. The visual format language can be used to add multiple constraints to a view. This is an ASCII-based representation that allows views to be related to each other in position and extrapolated into an array of constraints.
Constraints can be applied horizontally (the default) or vertically. The |
character can be used to represent either the start or end of the containing superview, and –
is used to represent the space that separates views, which are named in []
and referenced in a dictionary.
To constrain the two labels that are next to each other in the view, H:|-[left]-[right]-|
can be used. This can be read as a horizontal constraint (H:
) with a gap from the left edge (|-
) followed by the left view ([left]
), a gap (-
), a right view ([right]
), and finally a gap from the right edge (-|
). Similarly, vertical constraints can be added with a V:
prefix.
The constraintsWithVisualFormat
method on the NSLayoutConstraint
class can be used to parse visual format constraints. It takes a set of options
, metrics
, and a dictionary of views
referenced in the visual format. An array of constraints is returned, which can be passed into the addConstraints
method of the view.
To add constraints that ensure the left
and right
views have equal widths, a space between them, and a vertical space between the top of the view and the labels, the following code can be used:
override func updateConstraints() { // ... let namedViews = ["left":left,"right":right] addConstraints(NSLayoutConstraint. constraintsWithVisualFormat("H:|-[left]-[right]-|", options: nil, metrics: nil, views: namedViews)) addConstraints(NSLayoutConstraint. constraintsWithVisualFormat("V:|-[left]-|", options: nil, metrics: nil, views: namedViews)) addConstraints(NSLayoutConstraint. constraintsWithVisualFormat("V:|-[right]-|", options: nil, metrics: nil, views: namedViews)) super.updateConstraints() }
The TwoLabels
view can be tested by adding it as a footer to the SimpleTable
created previously. The footer is a special class, UITableViewHeaderFooterView
, which needs to be created and added to the tableView
. The TwoLabels
view can then be added to the footer's contentView
:
let footer = UITableViewHeaderFooterView() footer.contentView.addSubview(TwoLabels(frame:CGRect.zeroRect))tableView.tableFooterView = footer
Now, when the application is run in the simulator, the custom view will be seen: