iOS Training from beginner to advanced
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.
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.
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:
Since the is programmatic, we need nothing on the storyboard.
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.
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:
Rotate and the buttons automatically change size and shape.
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.
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.
// // 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() } }
Pingback: How to Add Stack Views Programmatically and (almost) avoid AutoLayout | Dinesh Ram Kali.
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
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>
OK, so the formatting isn’t great, but if you copy/paste it into a decent XML editor, it should be pretty readable. Let me know what you think.
Pingback: OK|Cancel
Nope, pasting is no good. Here it is:
http://gkbrown.org/2015/11/13/221/
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?
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.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?
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.
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.
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….
I’m afraid I cant help with that one.
I thought about it a bit more. I might not stick them on the navigation bar, but otherwise that is about right.
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?
Easily missed minor error. I have an s, and you didn’t. I used
view.addConstraints(stackView_H)
you usedview.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.Pingback: Swift: StackViews – baotrandotco
Hi! Thank you for this great tutorial.
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)
}
}
This is very nice tutorial for beginners.Keep it up.
Hi, I’m a beginner in programming in Swift. I tried to make a stackview with a label and a button programmatically. I can’t understand why they don’t apear on the simulator or on the my device. I have Xcode 9 and. The code is:
import UIKit
class ViewController: UIViewController {
// Adding a UILabel programmatically
var myLabel: UILabel!
// Adding a UIButton programmatically
var myButton: UIButton!
// Adding a UIStackView programmatically
var myStackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Set the myLabel
myLabel = UILabel()
myLabel.frame.size = CGSize(width: 250, height: 21)
myLabel.backgroundColor = UIColor.darkGray
myLabel.textColor = UIColor.black
myLabel.text = “New label.”
// Set the myButton
myButton = UIButton(type: .custom)
myButton.frame.size = CGSize(width: 100, height: 100)
myButton.backgroundColor = UIColor.clear
myButton.setTitleColor(UIColor.black, for: .normal)
myButton.setTitle(“New button”, for: .normal)
// Set the myStackView
myStackView = UIStackView(arrangedSubviews: [myLabel,myButton])
myStackView.axis = .vertical
myStackView.distribution = .fillProportionally
myStackView.alignment = .fill
myStackView.spacing = 10
self.view.addSubview(myStackView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Where I am wrong?
Hi, I have just completed this and made all the changes for the latest Swift/xcode
The following works
import UIKit
class ViewController: UIViewController {
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),
]
func colorButton(
withColor color:UIColor,
title:String) -> UIButton
{
let newButton = UIButton(type: .system)
newButton.backgroundColor = color
newButton.setTitle(
title,
for: UIControl.State.normal)
newButton.setTitleColor(
UIColor.white,
for: UIControl.State.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: NSLayoutConstraint.FormatOptions(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: NSLayoutConstraint.FormatOptions(rawValue:0),
metrics: nil,
views: viewsDictionary)
view.addConstraints(stackView_H)
view.addConstraints(stackView_V)
}
}
override func viewDidLoad() {
super.viewDidLoad()
displayKeyboard()
}