Creating iOS Apps without Storyboard – Part 2

Visual Format Language

This is yet another option to apply constraints. This uses ASCII like art forms to express constraints. This makes it easier to visualise the constraints.

Advantages
  • Using VFL makes it easier to read the constraint error messages printed in console
  • We can combine multiple constraints in single line
  • VFL ensures that you only create valid constraints
Disadvantages
  • Doesn’t work for all constraints
  • No validation, bugs discovered at runtime only

Here is an example of how VFL looks

"V:|-20-[redSquare(30)]-20-[greenSquare]-20-|"

It can appear cryptic to first timers but it can get simple. Before we look at the format above let us look at the typical VFL structure.

<Orientation>?<SuperView><Connection>?[VIEW]<Connection>[VIEW]!< Connection>[VIEW]?<SuperView>
  • ORIENTATION: This is the direction in which the constraints are being applied.
  • SUPERVIEW: This is the leading connection of the superview
  • CONNECTION: This the the actual connection
  • VIEW: This is the view on which we want to apply constraints.
  • ?: Not required
  • !: May appear 0 or more times

You could have multiple connections and views in a single line. Let us now examine the example:

"V:|-20-[redSquare(30)]-20-[greenSquare]-20-|"
  1. We start off with a V: which indicates vertical orientation
  2. Then we have | which indicates the leading edge of the superview
  3. Followed by a connection
  4. Then a view called redSquare with a width constraint of 30
  5. Followed by another connection
  6. A spacing value of 20
  7. Followed by another connection
  8. Then another view called greenSquare
  9. Followed by another connection
  10. A spacing value of 20
  11. Followed by another connection
  12. Lastly we have another | which indicates the trailing edge of the superview.

As we can see we have described an entire line in a single statement. Let us look at our project and see how we can implement auto-layout using Visual Format Language.


I will be continuing from the UIStackView part of the project.

  1. Switch to the ViewController.swift file. Add the following lines of code below the variable declaration.
//Constraint Variables for VFL
lazy var viewMap : [String : UIView] = ["icon"               : self.icon,
                                        "appView"            : self.appView,
                                        "nameField"          : self.nameField,
                                        "emailField"         : self.emailField,
                                        "age"                : self.age,
                                        "ageSlider"          : self.ageSlider,
                                        "ageValue"           : self.ageValue,
                                        "agePicker"          : self.agePicker,
                                        "serviceLbl"         : self.serviceLbl,
                                        "serviceRating"      : self.serviceRating,
                                        "satisfactionLbl"    : self.satisfactionLbl,
                                        "satisfactionRating" : self.satisfaction,
                                        "saveBtn"            : self.saveBtn,
                                        "fetchBtn"           : self.fetchBtn,
                                        "titleStack"         : self.titleStack,
                                        "ageStack"           : self.ageStack,
                                        "serviceStack"       : self.serviceStack,
                                        "satisfactionStack"  : self.satisfactionStk,
                                        "buttonStack"        : self.buttonStk,
                                        "enclosingStack"     : self.enclosingStack]
    
var allConstraints  : [NSLayoutConstraint]  = [NSLayoutConstraint]()

The first variable is a Dictionary having Key: String, Value: UIView. This mapping variable will be used to map the view mentioned in the VFL string with the actual view.

The second variable holds all the constraints in the form of an array.

2. Create an extension for the ViewController class. We will write our code there.
3. Now we will 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)

    let row1HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[appTitle(>=icon)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row1HoriontalConstraints)
}

The first 2 statements are the same as before. The new statement is the one which applies the constraints to the appTitle. Let us examine that statement.

let row1HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[appTitle(>=icon)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)

Basically we are stating that in the horizontal direction the appTitle width is greater than or equal to the width of icon view.

allConstraints.append(contentsOf: row1HoriontalConstraints)

We then take the constraints returned by that method and add it to the allConstraints property.

3. Now we will apply constraints to all the remaining views.

func apply_constraints_to_fields()
{
    let row2_1HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[nameField(>=100.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row2_1HoriontalConstraints)
        
    let row2_2HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[emailField(==nameField)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row2_2HoriontalConstraints)
}
    
func apply_constraints_to_age()
{
    let row3_1HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[age(==ageValue)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row3_1HoriontalConstraints)
        
    let row3_2HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[ageSlider(>=150.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row3_2HoriontalConstraints)
        
    let row3_3HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[ageValue(==50.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row3_3HoriontalConstraints)
}
    
func apply_constraints_to_service()
{
    let row4_1HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[serviceLbl(>=100.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row4_1HoriontalConstraints)
        
    let row4_2HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[serviceRating(>=serviceLbl)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row4_2HoriontalConstraints)
        
    serviceLbl.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.horizontal)
}
    
func apply_constraints_to_satisfaction()
{
    let row5_1HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[satisfactionLbl(>=100.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row5_1HoriontalConstraints)
        
    let row5_2HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[satisfactionRating(>=satisfactionLbl)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row5_2HoriontalConstraints)
        
    satisfactionLbl.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.horizontal)
}
    

func apply_constraints_to_buttons()
{
    let row5_1HoriontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:[saveBtn(==fetchBtn)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row5_1HoriontalConstraints)
}
    

func apply_constraints_to_all_Stacks()
{
    let titleStackHorizontalConstraints         : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "H:[titleStack(>=100.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: titleStackHorizontalConstraints)
        
    let ageStackHorizontalConstraints           : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "H:[ageStack(>=100.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: ageStackHorizontalConstraints)
            
    let serviceStackHorizontalConstraints       : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "H:[serviceStack(>=100.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: serviceStackHorizontalConstraints)
        
    let satisfactionStkHorizontalConstraints    : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "H:[satisfactionStack(>=100.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: satisfactionStkHorizontalConstraints)
        
    let buttonStkHorizontalConstraints          : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "H:[buttonStack(>=100.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: buttonStkHorizontalConstraints)
        
    let titleStackVerticalConstraints           : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "V:[titleStack(>=ageStack)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: titleStackVerticalConstraints)
        
    let ageStackVerticalConstraints             : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "V:[ageStack(==buttonStack)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: ageStackVerticalConstraints)
        
    let serviceStackVertictalConstraints        : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "V:[serviceStack(==satisfactionStack)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: serviceStackVertictalConstraints)
        
    let satisfactionStkVerticalConstraints      : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "V:[satisfactionStack(==buttonStack)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: satisfactionStkVerticalConstraints)
        
    let buttonStkVerticalConstraints            : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "V:[buttonStack(>=30.0)]", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: buttonStkVerticalConstraints)
        
    titleStack.setContentHuggingPriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.horizontal)
    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()
{
    let enclosingStackHorizontalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "H:|-10.0-[enclosingStack]-10-|", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: enclosingStackHorizontalConstraints)

    let enclosingStackVerticalConstraints : [NSLayoutConstraint] = NSLayoutConstraint.constraints(withVisualFormat: "V:|-170.0-[enclosingStack]-30-|", options: NSLayoutConstraint.FormatOptions.alignAllTop, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: enclosingStackVerticalConstraints)
}

Basically we are applying constraints to all the different views in our project. The task is a lot simpler because we have UIStackViews doing a lot of the work for us.

4. Next let us implement one function that calls all these functions.

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()
    NSLayoutConstraint.activate(allConstraints)
}

5. Now call 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()
}

6. Next we will make change to the ListViewController. Add the following variables to its variable declaration list.

lazy var viewMap    : [String : UIView]    = ["list": self.list]
var allConstraints  : [NSLayoutConstraint] = [NSLayoutConstraint]()

7. Add an extension for that class.
8. Implement the following function to add constraints.

func apply_constraints()
{
    list.translatesAutoresizingMaskIntoConstraints          = false
    let row1horizontalConstraints   : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "H:|-1.0-[list]-1.0-|", options: NSLayoutConstraint.FormatOptions.alignAllLeft, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row1horizontalConstraints)
        
    let row1VerticalConstraints     : [NSLayoutConstraint]  = NSLayoutConstraint.constraints(withVisualFormat: "V:|-170.0-[list]-1.0-|", options: NSLayoutConstraint.FormatOptions.alignAllTop, metrics: nil, views: viewMap)
    allConstraints.append(contentsOf: row1VerticalConstraints)
        
    NSLayoutConstraint.activate(allConstraints)
}

9. Call this function in the viewDidLoad method.

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 UI Renders.

Here is the link to the completed project using Visual Formatting Language.

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