I have a header view with a label and a button. I'm adding the constraints like this:
//create a view, button and label
view.addSubview(label)
view.addSubview(button)
label.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
let views = ["label": label, "button": button, "view": view]
let horizontallayoutContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-19-[label]-60-[button]-22-|", options: .alignAllCenterY, metrics: nil, views: views)
view.addConstraints(horizontallayoutContraints)
let verticalLayoutContraint = NSLayoutConstraint(item: label, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: 0)
view.addConstraint(verticalLayoutContraint)
return view
This works really well but now I'd like to add a divider view that spans the width above the label and button. Something like this:
let frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: 10)
let divView = UIView(frame: frame)
divView.backgroundColor = UIColor.lightGray
I can't seem to figure out the combination of constraints to make this happen. Basically I want the divView to span the width of the tableview and the existing views to sit below it. Ideally I could nest it like this:
V:|[divView]-20-[H:|-19-[label]-60-[button]-22-|]-20-|
Any experts out there that can help me figure this out? I could just make a NIB but I'd prefer to do it programmatically.
Granted, I have done very, very little with Visual Format Language, but as far as I know you cannot nest in that manner.
Depending on exactly what you are trying to get as an end result, and what else might get added to this view (labels? images? etc), you might find it easier to use a UIStackView or two.
However, here is an example of using VFL... this will run as-is in a Playground, so it will be easy to make adjustments to see the effect. Also note that I did it two ways - align the label and button to the divider, or align the divider to the label and button. The comments and the if block should be pretty self-explanatory. The colors are just to make it easy to see where the elements' frames end up.
import UIKit
import PlaygroundSupport
let container = UIView(frame: CGRect(x: 0, y: 0, width: 600, height: 600))
container.backgroundColor = UIColor.green
PlaygroundPage.current.liveView = container
// at this point, we have a 600 x 600 green square to use as a playground "canvas"
// create a 400x100 view at x: 40 , y: 40 as a "header view"
let headerView = UIView(frame: CGRect(x: 40, y: 40, width: 400, height: 100))
headerView.backgroundColor = UIColor.blue
// add the Header View to our "main container view"
container.addSubview(headerView)
var label: UILabel = {
let l = UILabel()
l.backgroundColor = UIColor.yellow
l.text = "The Label"
return l
}()
var button: UIButton = {
let l = UIButton()
l.backgroundColor = UIColor.red
l.setTitle("The Button", for: .normal)
return l
}()
var divView: UIView = {
let v = UIView()
v.backgroundColor = UIColor.lightGray
return v
}()
headerView.addSubview(divView)
headerView.addSubview(label)
headerView.addSubview(button)
divView.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
var vcs: [NSLayoutConstraint]
var views = ["divView": divView, "label": label, "button": button, "headerView": headerView]
let bAlignToDivider = true
if bAlignToDivider {
// use the width of the divView to control the left/right edges of the label and button
// V: pin divView to the top, with a height of 10
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"V:|[divView(10)]", options: [], metrics: nil, views: views)
headerView.addConstraints(vcs)
// H: pin divView 20 from the left, and 20 from the right
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"H:|-20-[divView]-20-|", options: [], metrics: nil, views: views)
headerView.addConstraints(vcs)
// V: pin label to bottom of divView (plus spacing of 8)
// using .alignAllLeft will pin the label's left to the divView's left
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"V:[divView]-8-[label]", options: .alignAllLeft, metrics: nil, views: views)
headerView.addConstraints(vcs)
// V: pin button to bottom of divView (plus spacing of 8)
// using .alignAllRight will pin the button's right to the divView's right
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"V:[divView]-8-[button]", options: .alignAllRight, metrics: nil, views: views)
headerView.addConstraints(vcs)
// H: add ">=0" spacing between label and button, so they use intrinsic widths
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"H:[label]-(>=0)-[button]", options: .alignAllCenterY, metrics: nil, views: views)
headerView.addConstraints(vcs)
}
else
{
// use left/right edges of the label and button to control the width of the divView
// H: pin label 20 from left
// pin button 20 from right
// use ">=0" spacing between label and button, so they use intrinsic widths
// also use .alignAllCenterY to vertically align them
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"H:|-20-[label]-(>=0)-[button]-20-|", options: .alignAllCenterY, metrics: nil, views: views)
headerView.addConstraints(vcs)
// V: pin divView to the top, with a height of 10
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"V:|[divView(10)]", options: [], metrics: nil, views: views)
headerView.addConstraints(vcs)
// V: pin label to bottom of divView (plus spacing of 8)
// using .alignAllLeft will pin the divView's left to the label's left
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"V:[divView]-8-[label]", options: .alignAllLeft, metrics: nil, views: views)
headerView.addConstraints(vcs)
// V: pin button to bottom of divView (plus spacing of 8)
// using .alignAllRight will pin the divView's right to the button's right
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"V:[divView]-8-[button]", options: .alignAllRight, metrics: nil, views: views)
headerView.addConstraints(vcs)
}
Edit: Here is another variation.
This time, the "header view" will have only the x,y position set... Its width and height will be auto-determined by its content.
The gray "div" view's position and width will be controlled by constraining it to the label and button, which will use your specified values:
"H:|-19-[label]-60-[button]-22-|"
Again, you can just copy/paste this into a playground page...
import UIKit
import PlaygroundSupport
let container = UIView(frame: CGRect(x: 0, y: 0, width: 600, height: 600))
container.backgroundColor = UIColor.green
PlaygroundPage.current.liveView = container
// at this point, we have a 600 x 600 green square to use as a playground "canvas"
var label: UILabel = {
let l = UILabel()
l.backgroundColor = UIColor.yellow
l.text = "This is a longer Label"
return l
}()
var button: UIButton = {
let l = UIButton()
l.backgroundColor = UIColor.red
l.setTitle("The Button", for: .normal)
return l
}()
var divView: UIView = {
let v = UIView()
v.backgroundColor = UIColor.lightGray
return v
}()
var headerView: UIView = {
let v = UIView()
v.backgroundColor = UIColor.blue
return v
}()
// add our header view
container.addSubview(headerView)
// add div, label and button as subviews in headerView
headerView.addSubview(divView)
headerView.addSubview(label)
headerView.addSubview(button)
// disable Autoresizing Masks
headerView.translatesAutoresizingMaskIntoConstraints = false
divView.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
var vcs: [NSLayoutConstraint]
var views = ["divView": divView, "label": label, "button": button, "headerView": headerView]
// init "header view" - we'll let its contents determine its width and height
// these two formats will simply put the header view at 20,20
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"H:|-20-[headerView]", options: [], metrics: nil, views: views)
container.addConstraints(vcs)
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"V:|-20-[headerView]", options: [], metrics: nil, views: views)
container.addConstraints(vcs)
// H: pin label 19 from left
// pin button 22 from right
// use 60 spacing between label and button
// width of label and button auto-determined by text
// also use .alignAllCenterY to vertically align them
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"H:|-19-[label]-60-[button]-22-|", options: .alignAllCenterY, metrics: nil, views: views)
headerView.addConstraints(vcs)
// V: pin divView to the top, with a height of 10
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"V:|[divView(10)]", options: [], metrics: nil, views: views)
headerView.addConstraints(vcs)
// V: pin label to bottom of divView (plus spacing of 20)
// using .alignAllLeft will pin the divView's left to the label's left
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"V:[divView]-20-[label]", options: .alignAllLeft, metrics: nil, views: views)
headerView.addConstraints(vcs)
// V: pin button to bottom of divView (plus spacing of 20)
// using .alignAllRight will pin the divView's right to the button's right
vcs = NSLayoutConstraint.constraints(withVisualFormat:
"V:[divView]-20-[button]|", options: .alignAllRight, metrics: nil, views: views)
headerView.addConstraints(vcs)
Related
I want the second label (subview2) to stay intact and have the first label (subview1) shrink as the size of the container view decreases in size, but the second label is still shrink no matter how I adjust the priority:
import UIKit
import PlaygroundSupport
let containerView = UIView(frame: CGRect(origin: .zero, size: .init(width: 200, height: 300)))
let subview1 = UILabel()
subview1.text = "Hello"
subview1.backgroundColor = .red
containerView.addSubview(subview1)
subview1.translatesAutoresizingMaskIntoConstraints = false
let subview2 = UILabel()
subview2.text = "Hello"
subview2.backgroundColor = .cyan
containerView.addSubview(subview2)
subview2.translatesAutoresizingMaskIntoConstraints = false
let views: [String: Any] = ["subview1": subview1, "subview2": subview2]
let con1 = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(20)-[subview1]", metrics: nil, views: views)
let con2 = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(20)-[subview2]", metrics: nil, views: views)
let con3 = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(30)-[subview1]", metrics: nil, views: views)
let con4 = NSLayoutConstraint.constraints(withVisualFormat: "H:[subview2]-(30)-|", metrics: nil, views: views)
let con5 = NSLayoutConstraint.constraints(withVisualFormat: "H:[subview1(>=100)]-(>=30)-[subview2(>=100)]", metrics: nil, views: views)
NSLayoutConstraint.activate(con1 + con2 + con3 + con4 + con5)
subview1.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: .horizontal)
subview2.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: .horizontal)
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = containerView
I've tried increasing the priority of subview2 manually, but still didn't work:
let priority1 = subview2.contentCompressionResistancePriority(for: .horizontal)
print(priority1) // 750
let priority2 = subview2.contentCompressionResistancePriority(for: .horizontal)
print(priority2) // 750
subview2.setContentCompressionResistancePriority(priority2 + 100, for: .horizontal)
VFL (Visual Format Language) is very out-dated and limited. I strongly suggest you switch to more modern (and more flexible) syntax.
That said, the problem is with this line:
let con5 = NSLayoutConstraint.constraints(withVisualFormat:
"H:[subview1(>=100)]-(>=30)-[subview2(>=100)]", metrics: nil, views: views)
Your code is saying:
subview1 width must be greater-than-or-equal-to 100
subview2 width must be greater-than-or-equal-to 100
so, the setContentCompressionResistancePriority lines do nothing. You've already said "these are the required minimum widths."
If you change that line to:
let con5 = NSLayoutConstraint.constraints(withVisualFormat:
"H:[subview1]-(>=30)-[subview2]", metrics: nil, views: views)
you should get your desired result.
Here, though, is your code using more modern constraint syntax -- you may find it much more logical and easier to understand:
NSLayoutConstraint.activate([
// both labels 20-pts from containerView Top
subview1.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20.0),
subview2.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20.0),
// left label Leading 30-pts from containerView Leading
subview1.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 30.0),
// right label Trailing 30-pts from containerView Trailing
subview2.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -30.0),
// right label Leading at least 30-pts from left label Trailing
subview2.leadingAnchor.constraint(greaterThanOrEqualTo: subview1.trailingAnchor, constant: 30.0),
])
// don't do any of this...
//let views: [String: Any] = ["subview1": subview1, "subview2": subview2]
//let con1 = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(20)-[subview1]", metrics: nil, views: views)
//let con2 = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(20)-[subview2]", metrics: nil, views: views)
//let con3 = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(30)-[subview1]", metrics: nil, views: views)
//let con4 = NSLayoutConstraint.constraints(withVisualFormat: "H:[subview2]-(30)-|", metrics: nil, views: views)
//let con5 = NSLayoutConstraint.constraints(withVisualFormat: "H:[subview1]-(>=30)-[subview2]", metrics: nil, views: views)
//NSLayoutConstraint.activate(con1 + con2 + con3 + con4 + con5)
Edit - to answer comment...
If you want the widths of your labels to each be >= 100, but allow them to compress (left-label compresses first), you need to add width constraints with Priority of less-than .required:
// both labels should have a width of >= 100
let leftWidth = subview1.widthAnchor.constraint(greaterThanOrEqualToConstant: 100.0)
let rightWidth = subview2.widthAnchor.constraint(greaterThanOrEqualToConstant: 100.0)
// set the Priority on the width constraints to less than required
// to allow auto-layout to break that constraint if needed
leftWidth.priority = .defaultHigh
rightWidth.priority = .defaultHigh
// activate the width constraints
leftWidth.isActive = true
rightWidth.isActive = true
// tell leftLabel to compress before rightLabel
subview1.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: .horizontal)
subview2.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: .horizontal)
I'm trying to programmatically create a 3x33 grid of UIButtons using nested UIStackViews inside a UIScrollView. The outer UIStackView has a vertical axis, while the inner UIStackViews have horizontal axes and distribution = .fillEqually.
If I don't have the enclosing UIScrollView, the buttons properly fill the width of the screen. With the scroll view, however, the buttons fill only half the width of the screen.
Here is a code example. In IB, ViewController's view property has been set to a UIScrollView instance.
class ViewController: UIViewController {
var stackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
title = "No Filling :("
stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 5
view.addSubview(stackView)
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[stackView]-|", options: .alignAllCenterX, metrics: nil, views: ["stackView": stackView!]))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[stackView]|", options: .alignAllCenterY, metrics: nil, views: ["stackView": stackView!]))
for _ in 1...33 {
let hView = UIStackView()
hView.translatesAutoresizingMaskIntoConstraints = false
hView.axis = .horizontal
hView.distribution = .fillEqually
hView.spacing = 5
stackView.addArrangedSubview(hView)
stackView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[hView]|", options: .alignAllCenterX, metrics: nil, views: ["hView": hView]))
for i in 1...3 {
let button = UIButton(type: .system)
button.setTitle("Button \(i)", for: .normal)
hView.addArrangedSubview(button)
button.layer.borderWidth = 1
button.layer.cornerRadius = 7
}
}
}
}
You want your scrollView's content width to be the width of the scrollView itself so that it fills the screen and scrolls vertically.
In order to do that, you need to establish the width of the scrollView's content. Right now, it is getting its width from the intrinsic size of the three buttons.
You need to add a constraint that explicitly makes the scrollView's content (the stackView + left and right offsets) equal to the width of the scrollView:
view.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "H:|-20-[stackView]-20-|", options: .alignAllCenterX,
metrics: nil, views: ["stackView": stackView!]))
view.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "V:|[stackView]|", options: .alignAllCenterY,
metrics: nil, views: ["stackView": stackView!]))
stackView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -40).isActive = true
Note: The adjustment constant -40 is the sum of the left and right offsets (20 each). In order for the scrollView to not scroll horizontally: stackView.width + 20 + 20 == scrollView.width, or equivalently, stackView.width == scrollView.width - 40 which is what the constraint specifies.
As you can see, two's a label , two images and two labels, all independent. I need to center all of these items,
i am doing this job programmatically. please suggest
try with way but failed :
func setupViews(){
view.addSubview(orderLocation)
orderLocation.addSubview(ownOrderTagImage)
orderLocation.addSubview(ownOrderTagLabel)
orderLocation.addSubview(ownOrderLocationImage)
orderLocation.addSubview(ownOrderLocationLabel)
//Change the width and height accordingly
view.addConstraintsWithFormat(format: "H:|[v0]|", views: orderLocation)
view.addConstraintsWithFormat(format: "V:|-50-[v0(20)]|", views: orderLocation)
orderLocation.addConstraintsWithFormat(format: "H:|[v0(18)][v1(70)]-20-[v2(18)][v3(80)]|", views: ownOrderTagImage, ownOrderTagLabel,ownOrderLocationImage,ownOrderLocationLabel)
orderLocation.addConstraintsWithFormat(format: "V:|[v0(20)]", views: ownOrderTagImage)
orderLocation.addConstraintsWithFormat(format: "V:|[v0(20)]", views: ownOrderTagLabel)
orderLocation.addConstraintsWithFormat(format: "V:[v0(20)]|", views: ownOrderLocationImage)
orderLocation.addConstraintsWithFormat(format: "V:[v0(20)]|", views: ownOrderLocationLabel)
}
You have a group of four views that you want to lay out in a row, and you want to center the group as a whole within a superview.
There is no anchor at the center of the group for you to use in a constraint. All of the anchors available are at the centers or edges of the individual subviews.
You cannot constrain “distance from leftmost item to superview” to be equal to “distance from rightmost item to superview”, because that constraint would involve four anchors, and an auto layout constraint can only involve two anchors.
One solution is to add a helper view that tightly surrounds the group of views. Then you can constrain the helper view's center anchor to the superview's center anchor.
The simplest way to do it is to put the group of views in a stack view, and center the stack view. If you use a stack view, you'll also need a “spacer” view to separate your “tag” views from your “location” views.
Here's my test result:
Here's my test playground:
import UIKit
import PlaygroundSupport
let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 100))
rootView.backgroundColor = #colorLiteral(red: 0.9568627477, green: 0.6588235497, blue: 0.5450980663, alpha: 1)
PlaygroundPage.current.liveView = rootView
let tagImageView = UIImageView(image: UIImage(named: "Toggle"))
tagImageView.contentMode = .scaleAspectFit
let tagLabel = UILabel()
tagLabel.text = "#YUIO9N"
let locationImageView = UIImageView(image: UIImage(named: "Gear"))
locationImageView.contentMode = .scaleAspectFit
let locationLabel = UILabel()
locationLabel.text = "Garmany"
let spacer = UIView()
let rowView = UIStackView(arrangedSubviews: [tagImageView, tagLabel, spacer, locationImageView, locationLabel])
rowView.axis = .horizontal
rowView.translatesAutoresizingMaskIntoConstraints = false
rootView.addSubview(rowView)
NSLayoutConstraint.activate([
tagImageView.widthAnchor.constraint(equalToConstant: 18),
spacer.widthAnchor.constraint(equalToConstant: 20),
locationImageView.widthAnchor.constraint(equalToConstant: 18),
rowView.heightAnchor.constraint(equalToConstant: 20),
rowView.centerXAnchor.constraint(equalTo: rootView.centerXAnchor),
rowView.topAnchor.constraint(equalTo: rootView.topAnchor, constant: 20)])
addConstraintsWithFormat is not inherently a part of swift, it's a an extension on the Visual Format Language.
I believe you are using the following code as your extension.
extension UIView {
func addConstraintsWithFormat(_ format: String, views : UIView...) {
var viewsDictionary = [String: UIView]()
for(index, view) in views.enumerated(){
let key = "v\(index)"
viewsDictionary[key] = view
view.translatesAutoresizingMaskIntoConstraints = false
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
You must remember to include information like that in future questions.
Anyways, you are missing the view you are adding the views to.
In your case you should have an ImageView as your superview
First create the ImageView object which in your case is going to be that bar
let imageView: UIImageView = {
let imgV = UIImageView()
//put the name of the image you are using in the quotes
imgV.image = UIImage(named: "")
imgV.layer.masksToBounds = true
imgV.contentMode = .scaleAspectFill
return imgV
}()
Make sure the photo you use is properly formatted and add barView as the super view of your sub views and their constraints.
Try
//here you add the view
view.addSubview(imageView)
imageView.addSubview(ownOrderTagImage)
imageView.addSubview(ownOrderTagLabel)
imageView.addSubview(ownOrderLocationImage)
imageView.addSubview(ownOrderLocationLabel)
//Change the width and height accordingly
view.addConstraintsWithFormat(format: "H:|[v0(100)]|", views: imageView)
view.addConstraintsWithFormat(format: "V:|[v0(10)]|", views: imageView)
imageView.addConstraintsWithFormat(format: "H:|[v0][v1]|", views: ownOrderTagImage, ownOrderTagLabel)
imageView.addConstraintsWithFormat(format: "H:|[v0][v1]|", views: ownOrderLocationImage, ownOrderLocationLabel)
imageView.addConstraintsWithFormat(format: "V:|[v0(44)]", views: ownOrderTagImage)
imageView.addConstraintsWithFormat(format: "V:|[v0(44)]", views: ownOrderTagLabel)
imageView.addConstraintsWithFormat(format: "V:[v0(30)]|", views: ownOrderLocationImage)
imageView.addConstraintsWithFormat(format: "V:[v0(30)]|", views: ownOrderLocationLabel)
I'm trying to programmatically generate a 'score page' where by I have a UILabel and a UISlider for each attribute's score. Since there isn't a fixed number of attributes, I've decided to do this programmatically (as opposed to in story board)
My idea of going about doing this was to create a UIView for each attribute and then insert one UILabel and one UISlider into the UIView, and then setting up constraints after.
However, I'm running into a problem whereby I'm unable to set up the constraints properly, or another huge error that I might have missed out due to inexperience in doing such things. As a result, all the UIViews are stuck to the top left of the screen (0,0) and are on top of one another.
Here's my code so far :
func addLabels(attributesArray: [String], testResultsDictionary: [String:Double]){
var viewsDictionary = [String:AnyObject]()
//let numberOfAttributes = attributesArray.count //(vestigial, please ignore)
let sliderHeight = Double(20)
let sliderWidth = Double(UIScreen.mainScreen().bounds.size.width)*0.70 // 70% across screen
let labelToSliderDistance = Float(10)
let sliderToNextLabelDistance = Float(30)
let edgeHeightConstraint = Float(UIScreen.mainScreen().bounds.size.height)*0.10 // 10% of screen height
for attribute in attributesArray {
let attributeView = UIView(frame: UIScreen.mainScreen().bounds)
attributeView.backgroundColor = UIColor.blueColor()
attributeView.translatesAutoresizingMaskIntoConstraints = false
attributeView.frame.size = CGSize(width: Double(UIScreen.mainScreen().bounds.size.width)*0.80, height: Double(80))
self.view.addSubview(attributeView)
var attributeViewsDictionary = [String:AnyObject]()
let attributeIndex = attributesArray.indexOf(attribute)! as Int
let attributeLabel = UILabel()
attributeLabel.translatesAutoresizingMaskIntoConstraints = false
attributeLabel.text = attribute.stringByReplacingOccurrencesOfString("_", withString: " ")
attributeLabel.sizeToFit()
let attributeSlider = UISlider()
attributeSlider.translatesAutoresizingMaskIntoConstraints = false
attributeSlider.setThumbImage(UIImage(), forState: .Normal)
attributeSlider.frame.size = CGSize(width: sliderWidth, height: sliderHeight)
attributeSlider.userInteractionEnabled = false
if let sliderValue = testResultsDictionary[attribute] {
attributeSlider.value = Float(sliderValue)
}
else {
attributeSlider.value = 0
}
attributeView.addSubview(attributeLabel)
attributeView.addSubview(attributeSlider)
//attributeView.sizeToFit()
attributeViewsDictionary["Label"] = attributeLabel
attributeViewsDictionary["Slider"] = attributeSlider
viewsDictionary[attribute] = attributeView
print(viewsDictionary)
let control_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:|-[\(attribute)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
var control_constraint_V = [NSLayoutConstraint]()
if attributeIndex == 0 {
control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:|-\(edgeHeightConstraint)-[\(attribute)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
}
else if attributeIndex == attributesArray.indexOf(attributesArray.last!){
control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[\(attribute)]-\(edgeHeightConstraint)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
}
else {
control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[\(attributesArray[attributeIndex-1])]-\(sliderToNextLabelDistance)-[\(attribute)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
}
self.view.addConstraints(control_constraint_H)
self.view.addConstraints(control_constraint_V)
let interAttributeConstraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[Label]-\(labelToSliderDistance)-[Slider]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: attributeViewsDictionary)
//let interAttributeConstraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:[Label]-5-[Slider]", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: attributeViewsDictionary)
attributeView.addConstraints(interAttributeConstraint_V)
//attributeView.addConstraints(interAttributeConstraint_H)
//attributeView.sizeToFit()
}
}
Extra Notes:
- An attributeArray looks something like this: ["Happiness", "Creativity", "Tendency_To_Slip"]
The code is extremely messy and unnecessarily long as it is a prototype, so sorry! Please bear with it!
The issue is that these views do not have their constraints fully defined (notably, there were a lot of missing vertical constraints). I also note that you've attempted to set the size of the frame of various views, but that is for naught because when you use auto layout, all frame values will be discarded and recalculated by the auto layout process. Instead, make sure the views dimensions are fully defined entirely by the constraints.
For example:
let spacing: CGFloat = 10
func addLabels(attributesArray: [String], testResultsDictionary: [String: Float]) {
var previousContainer: UIView? // rather than using the index to look up the view for the previous container, just have a variable to keep track of the previous one for you.
for attribute in attributesArray {
let container = UIView()
container.backgroundColor = UIColor.lightGrayColor() // just so I can see it
container.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(container)
// set top anchor for container to superview if no previous container, otherwise link it to the previous container
if previousContainer == nil {
container.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: spacing).active = true
} else {
container.topAnchor.constraintEqualToAnchor(previousContainer!.bottomAnchor, constant: spacing).active = true
}
previousContainer = container
// set leading/trailing constraints for container to superview
NSLayoutConstraint.activateConstraints([
container.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor, constant: spacing),
view.trailingAnchor.constraintEqualToAnchor(container.trailingAnchor, constant: spacing),
])
// create label
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = attribute
container.addSubview(label)
// create slider
let slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
slider.value = testResultsDictionary[attribute]!
container.addSubview(slider)
// constraints for label and slider
NSLayoutConstraint.activateConstraints([
label.topAnchor.constraintEqualToAnchor(container.topAnchor, constant: spacing),
slider.topAnchor.constraintEqualToAnchor(label.bottomAnchor, constant: spacing),
container.bottomAnchor.constraintEqualToAnchor(slider.bottomAnchor, constant: spacing),
label.leadingAnchor.constraintEqualToAnchor(container.leadingAnchor, constant: spacing),
slider.leadingAnchor.constraintEqualToAnchor(container.leadingAnchor, constant: spacing),
container.trailingAnchor.constraintEqualToAnchor(label.trailingAnchor, constant: spacing),
container.trailingAnchor.constraintEqualToAnchor(slider.trailingAnchor, constant: spacing)
])
}
}
Now, I happen to be using the iOS 9 syntax for defining constraints (it is expressive and concise), but if you want/need to use VFL you can do that, too. Just make sure that you define an equivalent set of constraints which are unambiguously defined (top, bottom, leading and trailing). Also note that rather than hardcoding the size of these container views, I let it infer it from the size of its subviews and the container views will resize accordingly.
Having said all of this, I look at this UI and I might be inclined to do this with a table view, which gets you out of the business of having to define all of these constraints, but also gracefully handles the scenario where there are so many of these that you want to enjoy scrolling behavior, too. Or, if I knew that these were always going to be able to fit on a single screen, I might use a UIStackView. But if you want to do it with constraints, you might do something like above.
The iOS 9.0 comes with UIStackView which makes it easier to layout views according to their content size. For example, to place 3 buttons in a row in accordance with their content width you can simply embed them in stack view, set axis horizontal and distribution - fill proportionally.
The question is how to achieve the same result in older iOS versions where stack view is not supported.
One solution I came up with is rough and doesn't look good. Again, You place 3 buttons in a row and pin them to nearest neighbors using constraints. After doing that you obviously will see content priority ambiguity error because auto layout system has no idea which button needs to grow / shrink before others.
Unfortunately, the titles are unknown before app's launch so you just might arbitrary pick a button. Let's say, I've decreased horizontal content hugging priority of middle button from standard 250 to 249. Now it'll grow before other two. Another problem is that left and right buttons strictly shrink to their content width without any nice looking paddings as in Stack View version.
It seems over complicated for a such simple thing. But the multiplier value of a constraint is read-only, so you'll have to go the hard way.
I would do it like this if I had to:
In IB: Create a UIView with constraints to fill horizontally the superView (for example)
In IB: Add your 3 buttons, add contraints to align them horizontally.
In code: programmatically create 1 NSConstraint between each UIButton and the UIView with attribute NSLayoutAttributeWidth and multiplier of 0.33.
Here you will get 3 buttons of the same width using 1/3 of the UIView width.
Observe the title of your buttons (use KVO or subclass UIButton).
When the title changes, calculate the size of your button content with something like :
CGSize stringsize = [myButton.title sizeWithAttributes:
#{NSFontAttributeName: [UIFont systemFontOfSize:14.0f]}];
Remove all programmatically created constraints.
Compare the calculated width (at step 4) of each button with the width of the UIView and determine a ratio for each button.
Re-create the constraints of step 3 in the same way but replacing the 0.33 by the ratios calculated at step 6 and add them to the UI elements.
Yes we can get the same results by using only constraints :)
source code
Imagine, I have three labels :
firstLabel with intrinsic content size equal to (62.5, 40)
secondLabel with intrinsic content size equal to (170.5, 40)
thirdLabel with intrinsic content size equal to (54, 40)
Strucuture
-- ParentView --
-- UIView -- (replace here the UIStackView)
-- Label 1 --
-- Label 2 --
-- Label 3 --
Constraints
for example the UIView has this constraints :
view.leading = superview.leading, view.trailing = superview.trailing, and it is centered vertically
UILabels constraints
SecondLabel.width equal to:
firstLabel.width * (secondLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth)
ThirdLabel.width equal to:
firstLabel.width * (thirdLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth)
I will back for more explanations
You may want to consider a backport of UIStackView, there are several open source projects. The benefit here is that eventually if you move to UIStackView you will have minimal code changes. I've used TZStackView and it has worked admirably.
Alternatively, a lighter weight solution would be to just replicate the logic for a proportional stack view layout.
Calculate total intrinsic content width of the views in your stack
Set the width of each view equal to the parent stack view multiplied by its proportion of the total intrinsic content width.
I've attached a rough example of a horizontal proportional stack view below, you can run it in a Swift Playground.
import UIKit
import XCPlayground
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.grayColor().CGColor
view.backgroundColor = UIColor.whiteColor()
XCPlaygroundPage.currentPage.liveView = view
class ProportionalStackView: UIView {
private var stackViewConstraints = [NSLayoutConstraint]()
var arrangedSubviews: [UIView] {
didSet {
addArrangedSubviews()
setNeedsUpdateConstraints()
}
}
init(arrangedSubviews: [UIView]) {
self.arrangedSubviews = arrangedSubviews
super.init(frame: CGRectZero)
addArrangedSubviews()
}
convenience init() {
self.init(arrangedSubviews: [])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConstraints() {
removeConstraints(stackViewConstraints)
var newConstraints = [NSLayoutConstraint]()
for (n, subview) in arrangedSubviews.enumerate() {
newConstraints += buildVerticalConstraintsForSubview(subview)
if n == 0 {
newConstraints += buildLeadingConstraintsForLeadingSubview(subview)
} else {
newConstraints += buildConstraintsBetweenSubviews(arrangedSubviews[n-1], subviewB: subview)
}
if n == arrangedSubviews.count - 1 {
newConstraints += buildTrailingConstraintsForTrailingSubview(subview)
}
}
// for proportional widths, need to determine contribution of each subview to total content width
let totalIntrinsicWidth = subviews.reduce(0) { $0 + $1.intrinsicContentSize().width }
for subview in arrangedSubviews {
let percentIntrinsicWidth = subview.intrinsicContentSize().width / totalIntrinsicWidth
newConstraints.append(NSLayoutConstraint(item: subview, attribute: .Width, relatedBy: .Equal, toItem: self, attribute: .Width, multiplier: percentIntrinsicWidth, constant: 0))
}
addConstraints(newConstraints)
stackViewConstraints = newConstraints
super.updateConstraints()
}
}
// Helper methods
extension ProportionalStackView {
private func addArrangedSubviews() {
for subview in arrangedSubviews {
if subview.superview != self {
subview.removeFromSuperview()
addSubview(subview)
}
}
}
private func buildVerticalConstraintsForSubview(subview: UIView) -> [NSLayoutConstraint] {
return NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
}
private func buildLeadingConstraintsForLeadingSubview(subview: UIView) -> [NSLayoutConstraint] {
return NSLayoutConstraint.constraintsWithVisualFormat("|-0-[subview]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
}
private func buildConstraintsBetweenSubviews(subviewA: UIView, subviewB: UIView) -> [NSLayoutConstraint] {
return NSLayoutConstraint.constraintsWithVisualFormat("[subviewA]-0-[subviewB]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subviewA": subviewA, "subviewB": subviewB])
}
private func buildTrailingConstraintsForTrailingSubview(subview: UIView) -> [NSLayoutConstraint] {
return NSLayoutConstraint.constraintsWithVisualFormat("[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
}
}
let labelA = UILabel()
labelA.text = "Foo"
let labelB = UILabel()
labelB.text = "FooBar"
let labelC = UILabel()
labelC.text = "FooBarBaz"
let stack = ProportionalStackView(arrangedSubviews: [labelA, labelB, labelC])
stack.translatesAutoresizingMaskIntoConstraints = false
labelA.translatesAutoresizingMaskIntoConstraints = false
labelB.translatesAutoresizingMaskIntoConstraints = false
labelC.translatesAutoresizingMaskIntoConstraints = false
labelA.backgroundColor = UIColor.orangeColor()
labelB.backgroundColor = UIColor.greenColor()
labelC.backgroundColor = UIColor.redColor()
view.addSubview(stack)
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-0-[stack]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["stack": stack]))
view.addConstraint(NSLayoutConstraint(item: stack, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: 0))
Use autolayout to your advantage. It can do all the heavy lifting for you.
Here is a UIViewController that lays out 3 UILabels, as you have in your screen shot, with no calculations. There are 3 UIView subviews that are used to give the labels "padding" and set the background color. Each of those UIViews has a UILabel subview that just shows the text and nothing else.
All of the layout is done with autolayout in viewDidLoad, which means no calculating ratios or frames and no KVO. Changing things like padding and compression/hugging priorities is a breeze. This also potentially avoids a dependency on an open source solution like TZStackView. This is just as easily setup in interface builder with absolutely no code needed.
class StackViewController: UIViewController {
private let leftView: UIView = {
let leftView = UIView()
leftView.translatesAutoresizingMaskIntoConstraints = false
leftView.backgroundColor = .blueColor()
return leftView
}()
private let leftLabel: UILabel = {
let leftLabel = UILabel()
leftLabel.translatesAutoresizingMaskIntoConstraints = false
leftLabel.textColor = .whiteColor()
leftLabel.text = "A medium title"
leftLabel.textAlignment = .Center
return leftLabel
}()
private let middleView: UIView = {
let middleView = UIView()
middleView.translatesAutoresizingMaskIntoConstraints = false
middleView.backgroundColor = .redColor()
return middleView
}()
private let middleLabel: UILabel = {
let middleLabel = UILabel()
middleLabel.translatesAutoresizingMaskIntoConstraints = false
middleLabel.textColor = .whiteColor()
middleLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
middleLabel.textAlignment = .Center
return middleLabel
}()
private let rightView: UIView = {
let rightView = UIView()
rightView.translatesAutoresizingMaskIntoConstraints = false
rightView.backgroundColor = .greenColor()
return rightView
}()
private let rightLabel: UILabel = {
let rightLabel = UILabel()
rightLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.textColor = .whiteColor()
rightLabel.text = "OK"
rightLabel.textAlignment = .Center
return rightLabel
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(leftView)
view.addSubview(middleView)
view.addSubview(rightView)
leftView.addSubview(leftLabel)
middleView.addSubview(middleLabel)
rightView.addSubview(rightLabel)
let views: [String : AnyObject] = [
"topLayoutGuide" : topLayoutGuide,
"leftView" : leftView,
"leftLabel" : leftLabel,
"middleView" : middleView,
"middleLabel" : middleLabel,
"rightView" : rightView,
"rightLabel" : rightLabel
]
// Horizontal padding for UILabels inside their respective UIViews
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[leftLabel]-(16)-|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[middleLabel]-(16)-|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[rightLabel]-(16)-|", options: [], metrics: nil, views: views))
// Vertical padding for UILabels inside their respective UIViews
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[leftLabel]-(6)-|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[middleLabel]-(6)-|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[rightLabel]-(6)-|", options: [], metrics: nil, views: views))
// Set the views' vertical position. The height can be determined from the label's intrinsic content size, so you only need to specify a y position to layout from. In this case, we specified the top of the screen.
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][leftView]", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][middleView]", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][rightView]", options: [], metrics: nil, views: views))
// Horizontal layout of views
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[leftView][middleView][rightView]|", options: [], metrics: nil, views: views))
// Make sure the middle view is the view that expands to fill up the extra space
middleLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)
middleView.setContentHuggingPriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)
}
}
Resulting view: