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.

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

In MapViewController.swift, update loadView() to add a segmented control to the interface. (Note that due to page size restrictions we are showing the first declaration split across two lines. You should enter each declaration on a single line.)

Listing 5.2  Adding a segmented control (MapViewController.swift)

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.systemBackground
    segmentedControl.selectedSegmentIndex = 0

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

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 still 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 use anchors to create your constraints. Anchors are properties on a 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().

Listing 5.3  Adding layout constraints for the segmented control (MapViewController.swift)

let segmentedControl
        = UISegmentedControl(items: ["Standard", "Hybrid", "Satellite"])
segmentedControl.backgroundColor = UIColor.systemBackground
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 display an alert on each new line, indicating that you have not used the variable you defined yet. You will address this in a moment.

Anchors have a constraint(equalTo:) method that creates a constraint between 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 complaints.

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

Listing 5.4  Activating the programmatic layout constraints (MapViewController.swift)

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 nearest common ancestor of the views associated with the constraint. Figure 5.3 shows a view hierarchy with the common ancestor for two views highlighted.

Figure 5.3  Common ancestor

Common ancestor

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

When the isActive property on a constraint is 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 isActive 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 5.4).

Figure 5.4  Segmented control added to the screen

Segmented control added to 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 the sensor housing, 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 and sensor housing issue first.

Layout guides

In Chapter 3, we mentioned the safe area – an alignment rectangle that represents the visible portion of your interface. Programmatically, you access the safe area through a property on view instances: safeAreaLayoutGuide. Using safeAreaLayoutGuide will allow your content to not underlap the status bar at the top of the screen or the tab bar at the bottom of the screen.

Layout guides like safeAreaLayoutGuide expose anchors that you can use to add constraints, such as: topAnchor, bottomAnchor, heightAnchor, and widthAnchor. Because you want the segmented control to be under the status bar and sensor housing, you will constrain the top anchor of the safe area 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 of the safe area layout guide.

Listing 5.5  Using the safe area layout guide of the map view (MapViewController.swift)

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

Build and run the application and switch to the MapViewController. The segmented control now appears below the status bar and sensor housing. And because you used the safe area layout guide 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 help your layout look good on any device.

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

Listing 5.6  Using the layout margins of the map view (MapViewController.swift)

let topConstraint =
    segmentedControl.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
                                          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, switching to the map view. The segmented control is now inset from the view’s edges (Figure 5.5).

Figure 5.5  Segmented control with updated constraints

Segmented control with updated constraints

Explicit constraints

It is helpful to understand how the methods 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 5.6.

Figure 5.6  NSLayoutConstraint equation

NSLayoutConstraint 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