Creating iOS Apps without Storyboard – Part 2

Layout Anchors

Layout Anchors provide a nice clean API for creating easy to read constraints. The different views in Xcode expose layout anchors for their edges, center, baseline, and size. Similarly, ViewControllers also expose their properties such as layout guides.
We simply link the layout anchor on one view with the layout anchor of another. We then establish a constraint which indicates the relationship between the two. For example:

 appTitle.widthAnchor.constraint(greaterThanOrEqualTo: icon.widthAnchor, multiplier: 4.0).isActive = true  
Basically what we are saying is that the width of appTitle must be greater than or equal to four times the width of icon. We access the width using the widthAnchor.

Here is another example.
 nameField.widthAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true 

The constrain here states that the width of nameField must be greater than or equal to 100.0.

An interesting point to note here is the fact that we have to explicitly activate constraints. The constraint method returns an object of type NSLayoutConstraint which needs to be activated. If we forget this step then the constraint will not get applied.


Let us look at how we can implement Layout Anchors in an actual project. I am continuing from the previous topic.

5. Create a new extension to hold the code that applies the constraints.
6. Write the following function to apply constraints to the title elements.

func apply_constraints_to_title()
{
    icon.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.horizontal)
    icon.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.vertical)
    appTitle.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.horizontal)
    appTitle.widthAnchor.constraint(greaterThanOrEqualTo: icon.widthAnchor, multiplier: 4.0).isActive           = true
}

let us examine each line.

icon.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.horizontal)

This sets the compression resistance for the UIImageView icon to low priority in the horizontal direction.

icon.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.vertical)

This sets the compression resistance for the UIImageView icon to low priority in the vertical direction.

appTitle.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.horizontal)

This sets the content hugging priority for the UILabel appTitle to high priority in the horizontal direction.

appTitle.widthAnchor.constraint(greaterThanOrEqualTo: icon.widthAnchor, multiplier: 4.0).isActive = true

Here we hook the widthAnchor for the appTitle to the widthAnchor of icon stating that the appTitle width is greater than or equal to 4 times the icon width.
IMPORTANT: Remember that we have to manually make the constraint active by setting the isActive property of the constraint to true.

7. Similarly we will write code for applying constraints to the other components.

func apply_constraints_to_fields()
{
    nameField.widthAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive                                = true
    emailField.widthAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive                               = true
    nameField.widthAnchor.constraint(equalTo: emailField.widthAnchor, multiplier: 1.0).isActive                 = true
}

func apply_constraints_to_age()
{
    age.widthAnchor.constraint(equalTo: ageValue.widthAnchor, multiplier: 1.0).isActive                         = true
    ageValue.widthAnchor.constraint(equalToConstant: 50.0).isActive                                             = true
    ageSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: 150.0).isActive                              = true
}

func apply_constraints_to_service()
{
    serviceRating.widthAnchor.constraint(greaterThanOrEqualTo: serviceLbl.widthAnchor, constant: 1.5).isActive  = true
    serviceLbl.widthAnchor.constraint(greaterThanOrEqualToConstant: 100.0).isActive                             = true
    serviceLbl.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.horizontal)
}

func apply_constraints_to_satisfaction()
{
    satisfaction.widthAnchor.constraint(equalTo: satisfactionLbl.widthAnchor, constant: 1.5).isActive           = true
    satisfactionLbl.widthAnchor.constraint(greaterThanOrEqualToConstant: 100.0).isActive                        = true
    satisfactionLbl.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.horizontal)
}

func apply_constraints_to_buttons()
{
    saveBtn.widthAnchor.constraint(equalTo: fetchBtn.widthAnchor, multiplier: 1.0).isActive                     = true
}

func apply_constraints_to_all_Stacks()
{
    titleStack.widthAnchor.constraint(greaterThanOrEqualToConstant: 100.0).isActive                             = true
    titleStack.setContentHuggingPriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.horizontal)
    ageStack.widthAnchor.constraint(greaterThanOrEqualToConstant: 100.0).isActive                               = true
    serviceStack.widthAnchor.constraint(greaterThanOrEqualToConstant: 100.0).isActive                           = true
    satisfactionStk.widthAnchor.constraint(greaterThanOrEqualToConstant: 100.0).isActive                        = true
    buttonStk.widthAnchor.constraint(greaterThanOrEqualToConstant: 100.0).isActive                              = true

    serviceStack.heightAnchor.constraint(equalTo: satisfactionStk.heightAnchor, multiplier: 1.0).isActive       = true
    satisfactionStk.heightAnchor.constraint(equalTo: buttonStk.heightAnchor, multiplier: 1.0).isActive          = true
    buttonStk.heightAnchor.constraint(greaterThanOrEqualToConstant: 30.0).isActive                              = true
    ageStack.heightAnchor.constraint(equalTo: buttonStk.heightAnchor, multiplier: 1.0).isActive                 = true
    titleStack.heightAnchor.constraint(equalTo: ageStack.heightAnchor, multiplier: 2.0).isActive                = true

    agePicker.setContentHuggingPriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.vertical)
    nameField.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.vertical)
    emailField.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.vertical)
}

func apply_constraints_to_enclosing_stack()
{
    enclosingStack.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10.0).isActive          = true
    enclosingStack.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10.0).isActive       = true
    enclosingStack.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -30.0).isActive           = true
    enclosingStack.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 170.0).isActive                 = true
}

8. We will now implement the function that calls all these individual methods.

func apply_constraints()
{
    self.apply_constraints_to_title()
    self.apply_constraints_to_fields()
    self.apply_constraints_to_age()
    self.apply_constraints_to_service()
    self.apply_constraints_to_satisfaction()
    self.apply_constraints_to_buttons()
    self.apply_constraints_to_all_Stacks()
    self.apply_constraints_to_enclosing_stack()
}

9. Add the call to this function in the viewDidLoad method.

override func viewDidLoad() 
{
    super.viewDidLoad()
    // Do any additional setup after loading the view.
        
    self.navigationItem.title               = "Feedback"
    self.configureUIElements()
    self.configureStacks()
    self.apply_constraints()
}

Run the project and see if the views render correctly. You may have to make some changes to get the desired output based on your device.

10. To complete the code let us modify the ListViewController.swift. Add the following code to the extension of ListViewController.

func apply_constraints()
{
    list.translatesAutoresizingMaskIntoConstraints                                                      = false
    list.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 170.0).isActive                   = true
    list.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 1.0).isActive               = true
    list.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 1.0).isActive             = true
    list.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 1.0).isActive           = true
}

11. Update the viewDidLoad method of the ListViewController to call this function.

override func viewDidLoad() 
{
    super.viewDidLoad()

    // Do any additional setup after loading the view.
    self.createTable()
    self.apply_constraints()
}

Run the project and see how the second screen comes up. Feel free to add or remove constraints to see how the UI renders.

Here is the link to the completed project using Layout Anchors.

Advertisement

One thought on “Creating iOS Apps without Storyboard – Part 2

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s