Creating views by subclassing UIView

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 and custom views

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.

Tip

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)

Constraints and the visual format language

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()
}

Note

If there are ambiguous constraints, then an error will be printed to the console when the view is shown. Messages that include the NSAutoresizingMaskLayout constraints indicate that the view has not disabled the automatic translation of auto-resizing mask into constraints.

Adding the custom view to the table

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:

Adding the custom view to the table
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset