Programmatic Constraints

In Chapter 3, you learned about Auto Layout constraints and how to add them using Interface Builder. In this section, you will learn how to add constraints to an interface programmatically.

Apple recommends that you create and constrain your views in Interface Builder whenever possible. However, if your views are created in code, then you will need to constrain them programmatically. The interface for MapViewController is created programmatically, so it is a great candidate for programmatic constraints.

To learn about programmatic constraints, you are going to add a UISegmentedControl to MapViewController’s interface. A segmented control allows the user to choose between a discrete set of options, and you will use one to allow the user to switch between map types: standard, hybrid, and satellite.

In MapViewController.swift, update loadView() to add a segmented control to the interface.

override func loadView() {
    // Create a map view
    mapView = MKMapView()

    // Set it as *the* view of this view controller
    view = mapView

    let segmentedControl
            = UISegmentedControl(items: ["Standard", "Hybrid", "Satellite"])
    segmentedControl.backgroundColor
            = UIColor.white.withAlphaComponent(0.5)
    segmentedControl.selectedSegmentIndex = 0

    segmentedControl.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(segmentedControl)
}

(Note that due to page size restrictions we are showing some of these declarations split across two lines. You should enter each declaration on a single line.)

The line of code regarding translating constraints has to do with an older system for scaling interfaces – autoresizing masks. Before Auto Layout was introduced, iOS applications used autoresizing masks to allow views to scale for different-sized screens at runtime.

Every view has an autoresizing mask. By default, iOS creates constraints that match the autoresizing mask and adds them to the view. These translated constraints will often conflict with explicit constraints in the layout and cause an unsatisfiable constraints problem. The fix is to turn off this default translation by setting the property translatesAutoresizingMaskIntoConstraints to false. (There is more about Auto Layout and autoresizing masks at the end of this chapter.)

Anchors

When you work with Auto Layout programmatically, you will use anchors to create your constraints. Anchors are properties on the view that correspond to attributes that you might want to constrain to an anchor on another view. For example, you might constrain the leading anchor of one view to the leading anchor of another view. This would have the effect of the two views’ leading edges being aligned.

Let’s create some constraints to do the following.

  • The top anchor of the segmented control should be equal to the top anchor of its superview.

  • The leading anchor of the segmented control should be equal to the leading anchor of its superview.

  • The trailing anchor of the segmented control should be equal to the trailing anchor of its superview.

In MapViewController.swift, create these constraints in loadView().

let segmentedControl
        = UISegmentedControl(items: ["Standard", "Hybrid", "Satellite"])
segmentedControl.backgroundColor
        = UIColor.white.withAlphaComponent(0.5)
segmentedControl.selectedSegmentIndex = 0

segmentedControl.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(segmentedControl)

let topConstraint
        = segmentedControl.topAnchor.constraint(equalTo: view.topAnchor)
let leadingConstraint
        = segmentedControl.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let trailingConstraint
        = segmentedControl.trailingAnchor.constraint(equalTo: view.trailingAnchor)

Xcode will alert you to a problem with each line you have entered. You will fix them in a moment.

Anchors have a method constraint(equalTo:) that will create a constraint between the two anchors. There are a few other constraint creation methods on NSLayoutAnchor, including one that accepts a constant as an argument:

func constraint(equalTo anchor: NSLayoutAnchor<AnchorType>,
                constant c: CGFloat) -> NSLayoutConstraint

Activating constraints

You now have three NSLayoutConstraint instances. However, these constraints will have no effect on the layout until you explicitly activate them by setting their isActive properties to true. This will resolve Xcode’s complaint.

In MapViewController.swift, activate the constraints at the end of loadView().

let topConstraint =
    segmentedControl.topAnchor.constraint(equalTo: view.topAnchor)
let leadingConstraint =
    segmentedControl.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let trailingConstraint =
    segmentedControl.trailingAnchor.constraint(equalTo: view.trailingAnchor)

topConstraint.isActive = true
leadingConstraint.isActive = true
trailingConstraint.isActive = true

Constraints need to be added to the most recent common ancestor for the views associated with the constraint. Figure 6.3 shows a view hierarchy along with the common ancestor for two views.

Figure 6.3  Common ancestor

Diagrammatic representation of the concept of common ancestor.

If a constraint is related to just one view (such as when adding a width or height constraint to a view), then that view is considered the common ancestor.

By setting the active property on a constraint to true, the constraint will work its way up the hierarchy for the items to find the common ancestor to add the constraint to. It will then call the method addConstraint(_:) on the appropriate view. Setting the active property is preferable to calling addConstraint(_:) or removeConstraint(_:) yourself.

Build and run the application and switch to the MapViewController. The segmented control is now pinned to the top, leading, and trailing edges of its superview (Figure 6.4).

Figure 6.4  Segmented control added to the screen

Screenshot of the top portion of an iOS device shows the tabs of the Map App “Standard, Hybrid, and Satellite” overlapped by Carrier, Wi-Fi connectivity symbol and Time elements of the screen.

Although the constraints are doing the right thing, the interface does not look good. The segmented control is underlapping the status bar, and it would look better if the segmented control was inset from the leading and trailing edges of the screen. Let’s tackle the status bar issue first.

Layout guides

View controllers expose two layout guides to assist with layout content: the topLayoutGuide and the bottomLayoutGuide. The layout guides indicate the extent to which the view controller’s view contents will be visible. Using topLayoutGuide will allow your content to not underlap the status bar or navigation bar at the top of the screen. (You will learn about navigation bars in Chapter 14.) Using the bottomLayoutGuide will allow your content to not underlap the tab bar at the bottom of the screen.

The layout guides expose three anchors that you can use to add constraints: topAnchor, bottomAnchor, and heightAnchor. Because you want the segmented control to be under the status bar, you will constrain the bottom anchor of the top layout guide to the top anchor of the segmented control.

In MapViewController.swift, update the segmented control’s constraints in loadView(). Make the segmented control be 8 points below the top layout guide.

let topConstraint =
    segmentedControl.topAnchor.constraint(equalTo: view.topAnchor)
let topConstraint =
    segmentedControl.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor,
                                          constant: 8)
let leadingConstraint =
    segmentedControl.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let trailingConstraint =
    segmentedControl.trailingAnchor.constraint(equalTo: view.trailingAnchor)

topConstraint.isActive = true
leadingConstraint.isActive = true
trailingConstraint.isActive = true

Build and run the application. The segmented control now appears below the status bar. By using the layout guides instead of a hardcoded constant, the views will adapt based on the context they appear in.

Now let’s update the segmented control so that it is inset from the leading and trailing edges of its superview.

Margins

Although you could inset the segmented control using a constant on the constraint, it is much better to use the margins of the view controller’s view.

Every view has a layoutMargins property that denotes the default spacing to use when laying out content. This property is an instance of UIEdgeInsets, which you can think of as a type of frame. When adding constraints, you will use the layoutMarginsGuide, which exposes anchors that are tied to the edges of the layoutMargins.

The primary advantage of using the margins is that the margins can change depending on the device type (iPad or iPhone) as well as the size of the device. Using the margins will give you content that looks good on any device.

Update the segmented control’s leading and trailing constraints in loadView() to use the margins.

let topConstraint =
    segmentedControl.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor,
                                          constant: 8)
let leadingConstraint =
    segmentedControl.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let trailingConstraint =
    segmentedControl.trailingAnchor.constraint(equalTo: view.trailingAnchor)

let margins = view.layoutMarginsGuide
let leadingConstraint =
    segmentedControl.leadingAnchor.constraint(equalTo: margins.leadingAnchor)
let trailingConstraint =
    segmentedControl.trailingAnchor.constraint(equalTo: margins.trailingAnchor)

topConstraint.isActive = true
leadingConstraint.isActive = true
trailingConstraint.isActive = true

Build and run the application again. The segmented control is now inset from the view’s margins (Figure 6.5).

Figure 6.5  Segmented control with updated constraints

Screenshot of the top portion of an iOS device shows the tabs of the Map App “Standard, Hybrid, and Satellite” now under the Carrier, Wi-Fi connectivity symbol and Time elements of the screen.

Explicit constraints

It is helpful to understand how these methods that you have used create constraints. NSLayoutConstraint has the following initializer:

convenience init(item view1: Any,
                 attribute attr1: NSLayoutAttribute,
                 relatedBy relation: NSLayoutRelation,
                 toItem view2: Any?,
                 attribute attr2: NSLayoutAttribute,
                 multiplier: CGFloat,
                 constant c: CGFloat)

This initializer creates a single constraint using two layout attributes of two view objects. The multiplier is the key to creating a constraint based on a ratio. The constant is a fixed number of points, similar to what you used in your spacing constraints.

The layout attributes are defined as constants in the NSLayoutConstraint class:

  • NSLayoutAttribute.left

  • NSLayoutAttribute.right

  • NSLayoutAttribute.leading

  • NSLayoutAttribute.trailing

  • NSLayoutAttribute.top

  • NSLayoutAttribute.bottom

  • NSLayoutAttribute.width

  • NSLayoutAttribute.height

  • NSLayoutAttribute.centerX

  • NSLayoutAttribute.centerY

  • NSLayoutAttribute.firstBaseline

  • NSLayoutAttribute.lastBaseline

There are additional attributes that handle the margins associated with a view, such as NSLayoutAttribute.leadingMargin.

Let’s consider a hypothetical constraint. Say you wanted the width of the image view to be 1.5 times its height. You could make that happen with the following code. (Do not type this hypothetical constraint in your code! It will conflict with others you already have.)

let aspectConstraint = NSLayoutConstraint(item: imageView,
                                          attribute: .width,
                                          relatedBy: .equal,
                                          toItem: imageView,
                                          attribute: .height,
                                          multiplier: 1.5,
                                          constant: 0.0)

To understand how this initializer works, think of this constraint as the equation shown in Figure 6.6.

Figure 6.6  NSLayoutConstraint equation

Figure depicts the attributes of NSLayout Constraint equation.

You relate a layout attribute of one view to the layout attribute of another view using a multiplier and a constant to define a single constraint.

..................Content has been hidden....................

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