How to Add Stack Views Programmatically and (almost) avoid AutoLayout

Some people don’t like Interface Builder and like to code instead. Sometimes your layout is so dynamic you need to lay out in code. In either case, programmatic layout of views becomes the answer instead of auto layout. In Xcode 7 Apple introduced stack views. In an earlier post, I introduced stack views in Interface Builder. There are still some problems with stack views in Interface Builder, but those problems do not appear in code. In this lesson I’ll introduce how to use stack views in code.

The Theory

Stacks views are really simple: they are arrays of views with a few properties. You initialize the stackview with an array of views and set a few properties: axis, distribution, fill and spacing. At run time, the views create constraints the fulfill those requirements.

By doing this you can almost avoid auto layout with one exception: you need to use auto layout to layout the stack view on the superview. In our lesson, we’ll layout our stack view over the entire view and then  have a view that works with lots of embedded views and stack views.

Set up the Project

Create a new Universal project in Swift called StackViewCodeDemo. Since Xcode no longer marks the launchscreen, add a label to the launchscreen.storyboard with the title StackViewCodeDemo. I usually center this by clicking the align menu button and then center horizontally and vertically in the container and updating the constraints like this:

center alignment

Since the is programmatic, we need nothing on the storyboard.

The Button Generator

I’m going to use a series of buttons for my view objects. Go to the ViewController.swift code and add the following declarations to the class:

//MARK: Properties
let colorDictionary = [
    "Red":UIColor(
        red: 1.0,
        green: 0.0,
        blue: 0.0,
        alpha: 1.0),
    "Green":UIColor(
        red: 0.0,
        green: 1.0, 
        blue: 0.0, alpha: 1.0),
    "Blue":UIColor(
        red: 0.0,
        green: 0.0,
        blue: 1.0,
        alpha: 1.0),
]

This dictionary gives us a color name for a key and a UIColor for a value. Now add the follow function to the class:

//MARK: Instance methods
func colorButton(
     withColor color:UIColor,
     title:String) -> UIButton
{
   let newButton = UIButton(type: .System)
   newButton.backgroundColor = color
   newButton.setTitle(
       title,
       forState: .Normal)       
   newButton.setTitleColor(
       UIColor.whiteColor(),
       forState: .Normal)
   return newButton
}
 

I’m keeping this simple. I built a method to make a button quickly with certain properties set. I left off the action, but you can certainly put it in here. I did this so I can iterate through my dictionary and make buttons. Make another method with this code:

 func displayKeyboard(){
//generate an array of buttons
    var buttonArray = [UIButton]()
    for (myKey,myValue) in colorDictionary{
         buttonArray += [colorButton(
             withColor: myValue,
             title: myKey)
         ]
}

We have an array of buttons that take their color and title from the dictionary.

Making a Stack View

We are now ready to make a stack view and add it to the superview. I’ll make one that will have the three buttons next to each other. Under the displayKeyboard we just wrote, add the following:

let stackView = UIStackView(arrangedSubviews: buttonArray)
stackView.axis = .Horizontal
stackView.distribution = .FillEqually
stackView.alignment = .Fill
stackView.spacing = 5
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)

The arrangedSubviews parameter of the UIStackView initializer takes an array of UIView objects and makes a stack view from it. We set the properties for the stack view, and shut off the autoresizing mask. I used a .Fill distribution which will stretch the buttons vertically to take the entire view. The distribution property makes equal width buttons. I set the buttons 5 points away from each other.

This stack view needs autolayout, which is why it was critical to shut off the autoresizing mask. We’ll lay out the stack view to be the view with a margin. I pinned the view to the left and right margins which are 20 points from the left and right sides, and 30 points above and below the top and bottom of the superview. Add this to your code:

//autolayout the stack view - pin 30 up 20 left 20 right 30 down
        let viewsDictionary = ["stackView":stackView]
        let stackView_H = NSLayoutConstraint.constraintsWithVisualFormat(
            "H:|-20-[stackView]-20-|",  //horizontal constraint 20 points from left and right side
            options: NSLayoutFormatOptions(rawValue: 0),
            metrics: nil,
            views: viewsDictionary)
        let stackView_V = NSLayoutConstraint.constraintsWithVisualFormat(
            "V:|-30-[stackView]-30-|", //vertical constraint 30 points from top and bottom 
            options: NSLayoutFormatOptions(rawValue:0),
            metrics: nil,
            views: viewsDictionary)
        view.addConstraints(stackView_H)
        view.addConstraints(stackView_V)

I’ll leave this code as-is. It works fine for a template to use the entire view for a stack View. If you want to know more about Auto layout in code, see my post on it.

Finally add the method to the viewDidLoad

 override func viewDidLoad() {
        super.viewDidLoad()
        displayKeyboard()
    
    }

Build and run. You will have three buttons:

2015-11-11_07-31-41

Rotate and the buttons automatically change size and shape.

2015-11-11_07-31-42

Nesting Stack Views

Once we have one stack view, we can also nest stack views. comment out the following code:

/*
let stackView = UIStackView(arrangedSubviews: buttonArray)
        stackView.axis = .Horizontal
        stackView.distribution = .FillEqually
        stackView.alignment = .Fill
        stackView.spacing = 5
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        */

Add underneath it the following code:

let subStackView = UIStackView(arrangedSubviews: buttonArray)
subStackView.axis = .Horizontal
subStackView.distribution = .FillEqually
subStackView.alignment = .Fill
subStackView.spacing = 5
//set up a label
let label = UILabel()
label.text = "Color Chooser"
label.textColor = UIColor.whiteColor()
label.backgroundColor = UIColor.blackColor()
label.textAlignment = .Center

let blackButton = colorButton(withColor: UIColor.blackColor(), title: "Black")

I made a stack view like our last stack view, a label with white text on a black background, and a black button. Under that code add this:

let stackView = UIStackView(arrangedSubviews: [label,subStackView,blackButton])
stackView.axis = .Vertical
stackView.distribution = .FillEqually
stackView.alignment = .Fill
stackView.spacing = 10
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)

We make a vertical stack view with the three views we defined. The order of the array is left to right in horizontal and top to bottom in vertical. I kept the same distribution and alignment as our first example, but added 10 points between the vertical view. Since our auto layout constraints for stackView are already set, we are ready to build and run. Check the view in both portrait and landscape.

2015-11-11_07-49-21

2015-11-11_07-49-32

Without a lot of coding for layout, stack views can make some quick adaptive layouts with a minimal amount of auto layout. Apple’s documentation encourages using stack views for the easiest and most adaptive layout, making coding for devices from a iPhone to an AppleTV easy. I kept this simple. In a real app, you probably want to add actions and have your views defined outside the displayKeyboard method as properties. You might want to play around with that and with changing the properties of the stack view to see the variation you can get with a stack view.

The Whole Code

//
//  ViewController.swift
//  StackViewCodeDemo
//
//  Created by Steven Lipton on 11/11/15.
//  Copyright © 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit



class ViewController: UIViewController {
    //MARK: Properties
   
    let colorDictionary = [
        "Red":UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0),
        "Green":UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0),
        "Blue":UIColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0),
    ]
    
    //MARK: Instance methods
    func colorButton(withColor color:UIColor, title:String) -> UIButton{
        let newButton = UIButton(type: .System)
        newButton.backgroundColor = color
        newButton.setTitle(title, forState: .Normal)
        newButton.setTitleColor(UIColor.whiteColor(), forState: .Normal)
        return newButton
    }
    
    
    func displayKeyboard(){
                 //generate an array
       
        
        var buttonArray = [UIButton]()
        for (myKey,myValue) in colorDictionary{
            buttonArray += [colorButton(withColor: myValue, title: myKey)]
        }
        /* //Iteration one - singel stack view
        let stackView = UIStackView(arrangedSubviews: buttonArray)
        stackView.axis = .Horizontal
        stackView.distribution = .FillEqually
        stackView.alignment = .Fill
        stackView.spacing = 5
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        */
        
        //Iteration two - nested stack views
        //set up the stack view
        let subStackView = UIStackView(arrangedSubviews: buttonArray)
        subStackView.axis = .Horizontal
        subStackView.distribution = .FillEqually
        subStackView.alignment = .Fill
        subStackView.spacing = 5
        //set up a label
        let label = UILabel()
        label.text = "Color Chooser"
        label.textColor = UIColor.whiteColor()
        label.backgroundColor = UIColor.blackColor()
        label.textAlignment = .Center
        
        let blackButton = colorButton(withColor: UIColor.blackColor(), title: "Black")
        
        let stackView = UIStackView(arrangedSubviews: [label,subStackView,blackButton])
        stackView.axis = .Vertical
        stackView.distribution = .FillEqually
        stackView.alignment = .Fill
        stackView.spacing = 10
        stackView.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(stackView)
    
        //autolayout the stack view - pin 30 up 20 left 20 right 30 down
        let viewsDictionary = ["stackView":stackView]
        let stackView_H = NSLayoutConstraint.constraintsWithVisualFormat("H:|-20-[stackView]-20-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
        let stackView_V = NSLayoutConstraint.constraintsWithVisualFormat("V:|-30-[stackView]-30-|", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary)
        view.addConstraints(stackView_H)
        view.addConstraints(stackView_V)
    }
    //MARK: Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        displayKeyboard()
    
    }



}


19 thoughts on “How to Add Stack Views Programmatically and (almost) avoid AutoLayout”

  1. Hi Steven,

    I took a stab at replicating your example using MarkupKit. I created a file named StackView.xml that contains the following:

    I was then able to reduce the view controller code to this:

    class ViewController: UIViewController {
    func displayKeyboard(){
    let stackView = LMViewBuilder.viewWithName(“StackView”, owner: self, root: nil)
    stackView.translatesAutoresizingMaskIntoConstraints = false

    view.addSubview(stackView)

    // (remaining code is the same)
    }
    }

    The key is obviously the first line of displayKeyboard(), which loads the markup and assigns the return value to “stackView”. But the end result is identical to your original example.

    Just thought I would share it in case you were interested.

    Greg

  2. Ugh. Of course, you can’t see the markup. Let me try again:

    <UIStackView axis=”vertical” distribution=”fillEqually” spacing=”10″>
    <UILabel text=”Color Chooser” textAlignment=”center”
    textColor=”#ffffff” backgroundColor=”#000000″/>

    <UIStackView axis=”horizontal” distribution=”fillEqually” spacing=”5″>
    <UIButton normalTitle=”Blue” titleLabel.textColor=”#ffffff”
    backgroundColor=”#0000ff”/>
    <UIButton normalTitle=”Green” titleLabel.textColor=”#ffffff”
    backgroundColor=”#00ff00″/>
    <UIButton normalTitle=”Red” titleLabel.textColor=”#ffffff”
    backgroundColor=”#ff0000″/>
    </UIStackView>

    <UIButton normalTitle=”Black” titleLabel.textColor=”#ffffff”
    backgroundColor=”#000000″/>
    </UIStackView>

  3. Pingback: OK|Cancel
  4. hi Steven,

    Thanks for the nice tutorial!

    can you add one little thing? how to add outlets and actions to the buttons? let’s say I have an array of buttons in a stackView and I want to make them into an outlet collection? how to do?

    1. With programmatic buttons, we use methods and properties of UIButton instead of outlets and actions. I was looking for a topic for next Monday’s post, and I think this will be it. I’ll have examples for you next Monday.

  5. cool. that would be great… I’d especially love to know how to make something like the following code less clunky… (i know it’s a kludge to refer to “last” when who knows how things could move around in unpredictable ways?

    
            let add = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: #selector(ViewController.addWebViewAction))
            let delete = UIBarButtonItem(barButtonSystemItem: .Trash, target: self, action: #selector(ViewController.deleteWebView))
            navigationItem.rightBarButtonItems = [delete, add]
            navigationItem.rightBarButtonItems!.first?.enabled = false
    
    1. Ohh. Change of target, so to speak. Well, I wasn’t going to cover that in my Monday post, I was sticking with UButton, with a few appearances by UILabel and UIStepper — and another stack view. Bar button items are a different animal all together.

      I’ll write back to you by tomorrow. I’m on my phone right now and its hard to write code on it.

    2. AS I started writing this up, I realized this is probably better explained in a short post. Its too good a question not to share, and brings up dynamic changes to UINavigationItem. Look for it in the next day or so.

      1. I ran into an interesting little problem that I’d like to understand the whys and how’s about… That is I had an addWebView(url: NSURL) function… So if the text should return delegate method called it, it would try to add the passed url. I then tried to make the same func be the #selector for the add button… But the thing is when I call that function, that does have only one parameter, url, e button passes what it feels like, namely itself as an object and an action… So the constant url becomes a UIBarButton object instead of an NSURL… What I really wanted was not to duplicate code and have lets say a default URL such as https://Google.com come up when pressing the add button, or when typing a blank text field… Then whatever the contents of the text field were otherwise.. I couldn’t get it to work that way so I ended up making a new addWeViewButtonPressed function that simply calls addWevView with the default page defined in properties at the top of the class.. But it really feels clunky and I’m hopin there is a much nicer pattern….

  6. Hi! Thank you for this great tutorial.
    I have error for view.addConstraint(stackView_H)
    “Cannot convert value of type ‘[NSLayoutConstraint]’ to expected argument type ‘NSLayoutConstraint'”
    cause method constraintsWithVisualFormat returns array, but method addConstraint waits just NSLayoutConstraint

    It works fine with view.addConstraint(stackView_H[0])

    I’m just beginner in swift maybe I’m wrong?

    1. Easily missed minor error. I have an s, and you didn’t. I used view.addConstraints(stackView_H) you used view.addConstraint(stackView_H). Several API’s have a singular method for adding one object and a plural method for adding an array of objects. This is one of those cases. They can get confusing and a pain to debug.

  7. THANKS this helped me a lot

    SWIFT 3: Various changes capitalisations and the like
    //
    // ViewController.swift
    // layout
    //
    // Created by Stephen Thornber on 22/12/2016.
    // Copyright © 2016 Medanaid. All rights reserved.
    //

    import UIKit

    class ViewController: UIViewController {
    //MARK: Properties
    let colorDictionary = [
    “Red”:UIColor(
    red: 1.0,
    green: 0.0,
    blue: 0.0,
    alpha: 1.0),
    “Green”:UIColor(
    red: 0.0,
    green: 1.0,
    blue: 0.0, alpha: 1.0),
    “Blue”:UIColor(
    red: 0.0,
    green: 0.0,
    blue: 1.0,
    alpha: 1.0),
    ]
    override func viewDidLoad() {
    super.viewDidLoad()
    displayKeyboard()

    // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
    }
    //MARK: Instance methods
    func colorButton(
    withColor color:UIColor,
    title:String) -> UIButton
    {
    let newButton = UIButton(type: .system)
    newButton.backgroundColor = color
    newButton.setTitle(title,for: .normal)
    newButton.setTitleColor(UIColor.white,for: .normal)
    return newButton
    }

    func displayKeyboard(){
    //generate an array of buttons
    var buttonArray = [UIButton]()
    for (myKey,myValue) in colorDictionary{
    buttonArray += [colorButton(
    withColor: myValue,
    title: myKey)
    ]
    }
    /* let stackView = UIStackView(arrangedSubviews: buttonArray)
    stackView.axis = .horizontal
    stackView.distribution = .fillEqually
    stackView.alignment = .fill
    stackView.spacing = 5
    stackView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(stackView)
    */

    let subStackView = UIStackView(arrangedSubviews: buttonArray)
    subStackView.axis = .horizontal
    subStackView.distribution = .fillEqually
    subStackView.alignment = .fill
    subStackView.spacing = 5
    //set up a label
    let label = UILabel()
    label.text = “Color Chooser”
    label.textColor = UIColor.white
    label.backgroundColor = UIColor.black
    label.textAlignment = .center

    let blackButton = colorButton(withColor: UIColor.black, title: “Black”)

    let stackView = UIStackView(arrangedSubviews: [label,subStackView,blackButton])
    stackView.axis = .vertical
    stackView.distribution = .fillEqually
    stackView.alignment = .fill
    stackView.spacing = 10
    stackView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(stackView)

    //autolayout the stack view – pin 30 up 20 left 20 right 30 down
    let viewsDictionary = [“stackView”:stackView]
    let stackView_H = NSLayoutConstraint.constraints(
    withVisualFormat: “H:|-20-[stackView]-20-|”, //horizontal constraint 20 points from left and right side
    options: NSLayoutFormatOptions(rawValue: 0),
    metrics: nil,
    views: viewsDictionary)
    let stackView_V = NSLayoutConstraint.constraints(
    withVisualFormat: “V:|-30-[stackView]-30-|”, //vertical constraint 30 points from top and bottom
    options: NSLayoutFormatOptions(rawValue:0),
    metrics: nil,
    views: viewsDictionary)
    view.addConstraints(stackView_H)
    view.addConstraints(stackView_V)
    }
    }

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 )

Google+ photo

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

Connecting to %s