Creating Spacers in VFL - ios

AutoLayoutbyExample via Apple Developer Documentation
Anyone have an example of how to create spacer views to set the spacing between labels equal in VFL?
labelsDictionary = [ "label1": label1,
"label2": label2,
"label3": label3,
"label4": label4 ]
let metrics = ["edgeSpacing": 20, "spacingBetween": 5, "labelWidth": 50]
let constraints = "H:|-(==edgeSpacing)-[label1(==labelWidth#999)]-[label2(==label1)]-[label3(==label1)]-[label3(==label1)]-[label4(==label1)]-(==edgeSpacing)-|"
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat( format : constraints,
options : .allZeros,
metrics : metrics,
views : labelsDictionary))
I was just wondering how to create spacers in Swift to ensure that the width between the labels are always equal but of the lowest priority.

You can add 3 spacer views, which mix with the 4 labels.
label1-spacer1-label2-spacer2-label3-spacer3-label4
Assuming you have all 7 views inside a containerView. Because label width may change according to its text, it is good to have equal space between them.
self.containerView.removeConstraints(containerView.constraints())
let views = ["label1": label1,
"label2": label2,
"label3": label3,
"label4": label4,
"spacer1": spacer1,
"spacer2": spacer2,
"spacer3": spacer3
]
let metrics = ["edgeSpacing": 20, "spacingBetween": 5]
//make spacers have equal width, and greater than 5, make all views center align vertically
var constH = NSLayoutConstraint.constraintsWithVisualFormat("H:|-edgeSpacing-[label1]-[spacer1(>=spacingBetween)]-[label2]-[spacer2(==spacer1)]-[label3]-[spacer3(==spacer1)]-[label4]-edgeSpacing-|", options: .AlignAllCenterY, metrics: metrics, views: views)
view.addConstraints(constH)
//configure label1 vertical alignment
let constV = NSLayoutConstraint.constraintsWithVisualFormat("V:|-edgeSpacing-[label1]-edgeSpacing-|", options: .AlignAllCenterY, metrics: metrics, views: views)
view.addConstraints(constV)
//make spacers have equal height
let constVForSpacer1 = NSLayoutConstraint.constraintsWithVisualFormat("V:|-edgeSpacing-[spacer1(==spacer2)]-edgeSpacing-|", options: .allZeros, metrics: metrics, views: views)
view.addConstraints(constVForSpacer1)
let constVForSpacer2 = NSLayoutConstraint.constraintsWithVisualFormat("V:|-edgeSpacing-[spacer2(==spacer3)]-edgeSpacing-|", options: .allZeros, metrics: metrics, views: views)
view.addConstraints(constVForSpacer2)

Nevermind, I was able to find another way to achieve what I wanted:
override func updateConstraints() {
super.updateConstraints()
for labelName in labelsDictionary.keys {
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[\(labelName)]-|", options: .allZeros, metrics: nil, views: labelsDictionary))
}
self.addConstraint(NSLayoutConstraint(item: label1, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 0.2, constant: 0))
self.addConstraint(NSLayoutConstraint(item: label2, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 0.4, constant: 0))
self.addConstraint(NSLayoutConstraint(item: label3, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 0.6, constant: 0))
self.addConstraint(NSLayoutConstraint(item: label4, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 0.8, constant: 0))
}

Related

Syntax for Swift 3 AddConstraintWithFormat?

I found sourcode:
view.addConstraintsWithFormat("H:|[v0]|", views: menuBar)
view.addConstraintsWithFormat("V:|[v0(50)]", views: menuBar)
And I'm using Swift 3, it's not working in my Xcode 8
Can someone tell me what code for that ?
Thank you so much!
You should write a function like this:
func addContraintsWithFormat(_ format: String, views: UIView...) {
var viewDict = [String: UIView]()
for (index, view) in views.enumerated() {
let key = "v\(index)"
view.translatesAutoresizingMaskIntoConstraints = false
viewDict[key] = view
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDict))
}
It would work for you!
May be you need Visual Format Language to use this function.
You have to write extension for UIView. Something like that:
extension UIView {
func addConstraintsWithFormat(_ format: String, views: UIView...) {
var viewsDictionary = [String: UIView]()
for (index, view) in views.enumerated() {
let key = "v\(index)"
view.translatesAutoresizingMaskIntoConstraints = false
viewsDictionary[key] = view
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
}
Swift 4 Implementation of the same constraints, just in case someone wants to use the new format instead of extending UIView:
private func setupConstraints(){
//topView is a simple UIView with a red background for example purposes
view.addSubview(topView)
topView.translatesAutoresizingMaskIntoConstraints = false
let cn1 = NSLayoutConstraint(item: topView, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1.0, constant: 0)
//Menu bar trailing end is 20 px from right edge of the screen
let cn2 = NSLayoutConstraint(item: topView, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1.0, constant: 0)
//Menu bar height = constant 60
let cn3 = NSLayoutConstraint(item: topView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 50)
//Menu bar width greater than or equal to 400
let cn4 = NSLayoutConstraint(item: topView, attribute: .width, relatedBy: .greaterThanOrEqual, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 400)
//Menu bar vertical padding from the top edge of the screen = 20
let cn5 = NSLayoutConstraint(item: topView, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1.0, constant: 50)
view.addConstraint(cn1)
view.addConstraint(cn2)
view.addConstraint(cn3)
view.addConstraint(cn4)
view.addConstraint(cn5)
}
You can use below code
let tempView = UIView()
tempView.backgroundColor = UIColor.green
tempView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tempView)
let views = ["view": view, "tempView": tempView]
let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[view]-(<=0)-[newView(100)]", options: NSLayoutFormatOptions.alignAllCenterY, metrics: nil, views: views)
let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[view]-(<=0)-[tempView(100)]", options: NSLayoutFormatOptions.alignAllCenterX, metrics: nil, views: views)
view.addConstraints(horizontalConstraints)
view.addConstraints(verticalConstraints)
Let me know if this help you or not.

Hot to add a View to a Scrollview programmatically using layout constraints

I'm triying to instert programatically a view inside an UIScrollView, but it doesn't appear here is my code:
mainScrollView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(mainScrollView)
//Add Trailing
let trailingConstraint = NSLayoutConstraint(item: mainScrollView, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1, constant: 0)
self.view.addConstraint(trailingConstraint)
//Add Leading
let leadingConstraint = NSLayoutConstraint(item: mainScrollView, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1, constant: 0)
self.view.addConstraint(leadingConstraint)
//Add Top
let topConstraint = NSLayoutConstraint(item: mainScrollView, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 0)
self.view.addConstraint(topConstraint)
//Add Bottom
let bottomConstraint = NSLayoutConstraint(item: mainScrollView, attribute: .bottom, relatedBy: .equal, toItem: self.view, attribute: .bottom, multiplier: 1, constant: 0)
self.view.addConstraint(bottomConstraint)
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.backgroundColor = .blue
mainScrollView.addSubview(contentView)
//Add Trailing
let trailingConstraintContent = NSLayoutConstraint(item: contentView, attribute: .trailing, relatedBy: .equal, toItem: mainScrollView, attribute: .trailing, multiplier: 1, constant: 0)
mainScrollView.addConstraint(trailingConstraintContent)
//Add Leading
let leadingConstraintContent = NSLayoutConstraint(item: contentView, attribute: .leading, relatedBy: .equal, toItem: mainScrollView, attribute: .leading, multiplier: 1, constant: 0)
mainScrollView.addConstraint(leadingConstraintContent)
//Add Top
let topConstraintContent = NSLayoutConstraint(item: contentView, attribute: .top, relatedBy: .equal, toItem: mainScrollView, attribute: .top, multiplier: 1, constant: 0)
mainScrollView.addConstraint(topConstraintContent)
//Add Bottom
let bottomConstraintContent = NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: mainScrollView, attribute: .bottom, multiplier: 1, constant: 0)
mainScrollView.addConstraint(bottomConstraintContent)
The first ScrollView is inserted because I added a background color and I can see it, but I cant see the contentview added as background color blue.
Any help?
UPDATE
I have tried the following to and no success:
mainScrollView.translatesAutoresizingMaskIntoConstraints = false
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
mainScrollView.backgroundColor = .red
contentView.backgroundColor = .blue
self.view.addSubview(mainScrollView)
mainScrollView.addSubview(contentView)
let viewsDictionary = ["mainScrollView": mainScrollView, "contentView": contentView]
let mainScrollViewVerticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "V:|[mainScrollView]|", options: [], metrics: nil, views: viewsDictionary)
let mainScrollViewHorizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "H:|[mainScrollView]|", options: [], metrics: nil, views: viewsDictionary)
self.view.addConstraints(mainScrollViewVerticalConstraint)
self.view.addConstraints(mainScrollViewHorizontalConstraint)
let contentViewVerticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "V:|[contentView]|", options: [], metrics: nil, views: viewsDictionary)
let contentViewHorizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "H:|[contentView]|", options: [], metrics: nil, views: viewsDictionary)
mainScrollView.addConstraints(contentViewVerticalConstraint)
mainScrollView.addConstraints(contentViewHorizontalConstraint)
When adding sub views the important thing to remember is to pin them in a way to make it possible for Auto Layout to evaluate the height, this is because the scroll view determines the content size by determining the sum of heights sub views (in case of vertical).
So you should specify the height in VFL . eg.
let contentViewVerticalConstraint=NSLayoutConstraint.constraints(withVisualFormat: "V:|[contentView(300)]", options: [], metrics: nil, views: viewsDictionary)
Create a custom view using .xib file
Apply the constraints for the custom view child components in .xib (auto layout)
Create a custom view instant
MyView *myview = [[[NSBundle mainBundle] loadNibNamed:#"nibName" owner:nil options:nil] objectAtIndex:0];
Set its frame for screen width as
CGRect screenSize = [UIScreen mainScreen].bounds.size;
myView = CGRectMake(0.0, 0.0, screenSize.width, 100.0);
Add to parent view
[parentView addSubView:myView];

Center UIView without overlapping neighbors views of different size?

What I wish to achieve, the three of them are UILabels:
Spec:
left and right are edged UIViews.
middle is centered, even though side views are not the same size.
On collision between middle and left or middle and right, the text in middle gets tail truncated while left and right do not change.
Problem:
I cannot have meet both requirements (2) and (3) at the same time.
(2) is achieved with middle.autoAlignAxisToSuperviewAxis(.Vertical)
problem: no truncate, it just overlaps leftLabel and rightLabel.
(3) is achieved with middle.autoPinEdge(.Left, toEdge: .Right, ofView: leftLabel) and same strategy for the rightLabel.
problem: middle is centered in the area between leftLabel and rightLabel, if the last two do not have the same size, middle is not superview centered anymore.
Both applied create a constraint conflict and I don't know how to fix it, hence my question:
How can I center a UILabel and truncate it to avoir overlaps with side views of different sizes?
All you need is adding two constraints, one is horizontal spacing between middle and left, set the relation to 'Greater than or Equal', and constant for example 10; another is horizontal spacing between middle and right, the same configuration like constraint one.
When the text of middle label is too long, the constraints below will prevent middle from overlapping with label left and right.
Add sample code:
I suppose you had set the constraints to UILabel left and right in storyboard or in code. The sample below is used to configu UILabel middle
func setupConstraint() {
self.middle = UILabel()
self.middle!.backgroundColor = UIColor.yellowColor()
self.view.addSubview(self.middle!)
self.middle!.numberOfLines = 0
self.middle!.translatesAutoresizingMaskIntoConstraints = false
let middle = self.middle!
var constraint = NSLayoutConstraint(item: middle, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: left, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0)
self.view.addConstraint(constraint)
constraint = NSLayoutConstraint(item: middle, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.GreaterThanOrEqual, toItem: left, attribute: NSLayoutAttribute.Right, multiplier: 1, constant: 10)
self.view.addConstraint(constraint)
constraint = NSLayoutConstraint(item: right, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.GreaterThanOrEqual, toItem: middle, attribute: NSLayoutAttribute.Right, multiplier: 1, constant: 10)
constraint.priority = 1000
self.view.addConstraint(constraint)
constraint = NSLayoutConstraint(item: middle, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0)
self.view.addConstraint(constraint)
constraint = NSLayoutConstraint(item: middle, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.GreaterThanOrEqual, toItem: middle, attribute: NSLayoutAttribute.Height, multiplier: 1, constant: 100)
self.view.addConstraint(constraint)
self.middle!.text = "self.middle.translatesAutoresizingMaskIntoConstraints = false"
}
Sanpshot
i tried to set up a solution in a playground:
import UIKit
import XCPlayground
let viewController = UIViewController()
XCPlaygroundPage.currentPage.liveView = viewController.view
viewController.view.backgroundColor = UIColor.whiteColor()
let leftView = UIView()
leftView.translatesAutoresizingMaskIntoConstraints = false
leftView.backgroundColor = UIColor.greenColor()
let leftLabel = UILabel()
leftLabel.translatesAutoresizingMaskIntoConstraints = false
leftLabel.text = "leftleftleftleftleftleftleftleftleftleftleftleftleftleft"
leftView.addSubview(leftLabel)
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-2-[leftLabel]-2-|", options: [], metrics: nil, views: ["leftLabel": leftLabel]))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-2-[leftLabel]-2-|", options: [], metrics: nil, views: ["leftLabel": leftLabel]))
let middleView = UIView()
middleView.translatesAutoresizingMaskIntoConstraints = false
middleView.backgroundColor = UIColor.redColor()
let middleLabel = UILabel()
middleLabel.translatesAutoresizingMaskIntoConstraints = false
middleLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)
middleLabel.text = "middle"
middleView.addSubview(middleLabel)
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-2-[middleLabel]-2-|", options: [], metrics: nil, views: ["middleLabel": middleLabel]))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-2-[middleLabel]-2-|", options: [], metrics: nil, views: ["middleLabel": middleLabel]))
let rightView = UIView()
rightView.translatesAutoresizingMaskIntoConstraints = false
rightView.backgroundColor = UIColor.greenColor()
let rightLabel = UILabel()
rightLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.text = "right"
rightView.addSubview(rightLabel)
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-2-[rightLabel]-2-|", options: [], metrics: nil, views: ["rightLabel": rightLabel]))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-2-[rightLabel]-2-|", options: [], metrics: nil, views: ["rightLabel": rightLabel]))
viewController.view.addSubview(leftView)
viewController.view.addSubview(middleView)
viewController.view.addSubview(rightView)
NSLayoutConstraint(item: middleView, attribute: .CenterX, relatedBy: .Equal, toItem: viewController.view, attribute: .CenterX, multiplier: 1, constant: 0).active = true
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|[leftView]->=8-[middleView]->=8-[rightView]|", options: [.AlignAllTop, .AlignAllBottom], metrics: nil, views: ["leftView": leftView, "middleView": middleView, "rightView": rightView]))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[leftView]", options: [], metrics: nil, views: ["leftView": leftView]))
results in something like this:
UPDATE (without embedding the labels in their own views):
import UIKit
import XCPlayground
let viewController = UIViewController()
XCPlaygroundPage.currentPage.liveView = viewController.view
viewController.view.backgroundColor = UIColor.whiteColor()
let leftLabel = UILabel()
leftLabel.translatesAutoresizingMaskIntoConstraints = false
leftLabel.backgroundColor = UIColor.greenColor()
leftLabel.text = "leftleftleftleftleftleftleftleftleftleftleftleftleftleft"
let middleLabel = UILabel()
middleLabel.translatesAutoresizingMaskIntoConstraints = false
middleLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)
middleLabel.backgroundColor = UIColor.redColor()
middleLabel.text = "middle"
let rightLabel = UILabel()
rightLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.backgroundColor = UIColor.greenColor()
rightLabel.text = "right"
viewController.view.addSubview(leftLabel)
viewController.view.addSubview(middleLabel)
viewController.view.addSubview(rightLabel)
NSLayoutConstraint(item: middleLabel, attribute: .CenterX, relatedBy: .Equal, toItem: viewController.view, attribute: .CenterX, multiplier: 1, constant: 0).active = true
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|[leftLabel]->=8-[middleLabel]->=8-[rightLabel]|", options: [.AlignAllTop, .AlignAllBottom], metrics: nil, views: ["leftLabel": leftLabel, "middleLabel": middleLabel, "rightLabel": rightLabel]))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[leftLabel]", options: [], metrics: nil, views: ["leftLabel": leftLabel]))
hope it helps :)

Set NSLayoutConstraint of view to top right

I'm at wits end trying to figure this out. In CSS, you can do something like this:
img {
position: absolute;
width: 50px;
height: 50px;
top: 0;
right: 0;
}
In Swift, I am using the programatic interface to set layout constraints of an image. It looks like this:
let img = UIImageView(image: UIImage(named: "my-image"))
img.setTranslatesAutoresizingMaskIntoConstraints(false)
mainScrollView.addSubview(img)
mainScrollView.addConstraint(NSLayoutConstraint(item: img, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 50))
mainScrollView.addConstraint(NSLayoutConstraint(item: img, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 50))
mainScrollView.addConstraint(NSLayoutConstraint(item: img, attribute: .Top, relatedBy: .Equal, toItem: mainScrollView, attribute: .Top, multiplier: 1, constant: 0))
mainScrollView.addConstraint(NSLayoutConstraint(item: img, attribute: .Trailing, relatedBy: .Equal, toItem: mainScrollView, attribute: .Trailing, multiplier: 1, constant: 0))
However for some bizarre reason it respects the width and height layout constraints, even the top one. but it's basically off screen to the left by 50. If I set the last .Trailing constant to 50, it actually moves it into view at the top left (yes I'm very confused by this).
The constraints should be applied to the view which is the nearest common ancestors of the view involved in the constraint. So I can see two errors in the code you posted:
if you are setting the height or the width of a view, the view itself is the nearest common ancestor view so the constraint should be applied directly to it. So your code should be:
item.addConstraint(NSLayoutConstraint(item: img, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 50))
item.addConstraint(NSLayoutConstraint(item: img, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 50))
Since the views involved in this constraint:
NSLayoutConstraint(item: img, attribute: .Top, relatedBy: .Equal, toItem: mainView, attribute: .Top, multiplier: 1, constant: 0))
are one the subview of the other, it should be applied to the parent view:
mainView.addConstraint(NSLayoutConstraint(item: img, attribute: .Top, relatedBy: .Equal, toItem: mainView, attribute: .Top, multiplier: 1, constant: 0)))
Regarding the problem with the Trailing attribute you should considering that when you are adding a constraint you are defining this formula:
item1.attribute = item2.attribute + constant
So if you are defining a constraint where both the attributes are Trailing you should to use a negative constant to have one view inside the other:
--------------------------
View1 |
------------- |
View2 | - (-50) - |
Have you tried doing it the way that doesn't involve code? Using the buttons on the bottom right of the story board view?
You just want to set it to top right with width and height both 50? Maybe you can do it like this:
let img = UIImageView(image: UIImage(named: "my-image"))
img.setTranslatesAutoresizingMaskIntoConstraints(false)
mainScrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[img(50)]-0-|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["img" : img]))
mainScrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[img(50)]", options: NSLayoutFormatOptions(0), metrics: nil, views: ["img" : img]))
The issue is that unlike standard views, constraints between a scroll view and its subviews don't dictate the size of the subviews, but rather the contentSize of the scroll view. This may seem curious, but it's actually a wonderful feature, getting us out of the business of calculating contentSize manually. For more information, see Technical Note TN2154.
But this means that in order to dictate the size of a subview within a scroll view, you actually have to reference something outside of the scroll view (e.g. the view controller's main view).
So you can place a "container" view inside the scroll view and put the image view in the upper right corner of that. And then you can define the container to be the width of the main view. That way you're accomplishing both goals, dictating the width of the contents of the the scroll view, and dictating the placement of the image view within that:
// define the container inside the scroll view
let containerView = UIView()
containerView.setTranslatesAutoresizingMaskIntoConstraints(false)
scrollView.addSubview(containerView)
// put the imageView in the container
let imageView = UIImageView(image: UIImage(named: "my-image"))
imageView.setTranslatesAutoresizingMaskIntoConstraints(false)
containerView.addSubview(imageView)
let views = ["containerView" : containerView, "view" : view, "imageView" : imageView]
// constraints for the container
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[containerView(==view)]|", options: nil, metrics: nil, views: views))
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[containerView]|", options: nil, metrics: nil, views: views))
// define the imageview constraints
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[imageView(==50)]|", options: nil, metrics: nil, views: views))
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[imageView(==50)]", options: nil, metrics: nil, views: views))

Swift constraints programmatically creates odd behavior

I'm attempting to layout all my constraints programmatically because I have to add and remove boxes based on user input. When I run a function which applies all my constraints in ViewDidLoad, it works great. But if I run it again, after removing them all, 2 of my labels disappear behind my navigation bar. I can't figure out why! Keep in mind if I never run the function pasted below, my screen is blank because I have no constraints on start up from the storyboard. My goal is to figure out why my label and button disappear when the same piece of code is run a second time.
Below is a picture when the code, which I'll post below, is run initially in ViewDidLoad:
Below is a picture when the same piece of code is run again afterwards when the next button is pressed. This is just a test, in the future I'll need to reset all my constraints after changing them:
And Below is a picture of the debug view hierarchy afterwards:
The code that creates the constraints is below. Keep in mind I have only 5 elements, my main view, scroll view, aglAltitudeLabel which is the label, nextButton which is the button, and aglAltitude which is the text input:
//Remove all pre-existing contraints
var constraints:NSArray = aglAltitudeLabel.constraints()
aglAltitudeLabel.removeConstraints(constraints)
constraints = aglAltitude.constraints()
aglAltitude.removeConstraints(constraints)
constraints = nextButton.constraints()
nextButton.removeConstraints(constraints)
constraints = scrollView.constraints()
scrollView.removeConstraints(constraints)
constraints = view.constraints()
self.view.removeConstraints(constraints)
constraints = calculateButton.constraints()
calculateButton.removeConstraints(constraints)
//Make scrollview fit normal View, accounting for Navigation bar
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|[first(\(self.view.frame.width))]|", options: nil, metrics: nil, views: ["first": scrollView]))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-64-[first(\(self.view.frame.height-64))]|", options: nil, metrics: nil, views: ["first": scrollView]))
//Set the heights for the label and button
let labelHeight = NSLayoutConstraint(item: aglAltitudeLabel, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 20)
scrollView.addConstraint(labelHeight)
let buttonHeight = NSLayoutConstraint(item: nextButton, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 30)
scrollView.addConstraint(buttonHeight)
//Setup the vertical layout
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-8-[first(30)]-8-|", options: nil, metrics: nil, views: ["first": aglAltitude]))
//Setup the horizontal layout
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-8-[first(97)]-8-[second(>=100)]-8-[third(46)]-8-|", options: nil, metrics: nil, views: ["first": aglAltitudeLabel, "second": aglAltitude, "third": nextButton]))
//Align baselines - this also occurs with the option above of AlignAllBaselines and leaving this code out
let aglLabelBaseline = NSLayoutConstraint(item: aglAltitudeLabel, attribute: NSLayoutAttribute.Baseline, relatedBy: NSLayoutRelation.Equal, toItem: aglAltitude, attribute: NSLayoutAttribute.Baseline, multiplier: 1.0, constant: 0)
scrollView.addConstraint(aglLabelBaseline)
let nextButtonBaseline = NSLayoutConstraint(item: nextButton, attribute: NSLayoutAttribute.Baseline, relatedBy: NSLayoutRelation.Equal, toItem: aglAltitude, attribute: NSLayoutAttribute.Baseline, multiplier: 1.0, constant: 0)
scrollView.addConstraint(nextButtonBaseline)
self.view.layoutSubviews()
Thank you for any help. I'm sure I'm doing 55 things incorrectly.
Edit: If I change the last section of code from this:
//Align baselines - this also occurs with the option above of AlignAllBaselines and leaving this code out
let aglLabelBaseline = NSLayoutConstraint(item: aglAltitudeLabel, attribute: NSLayoutAttribute.Baseline, relatedBy: NSLayoutRelation.Equal, toItem: aglAltitude, attribute: NSLayoutAttribute.Baseline, multiplier: 1.0, constant: 0)
scrollView.addConstraint(aglLabelBaseline)
let nextButtonBaseline = NSLayoutConstraint(item: nextButton, attribute: NSLayoutAttribute.Baseline, relatedBy: NSLayoutRelation.Equal, toItem: aglAltitude, attribute: NSLayoutAttribute.Baseline, multiplier: 1.0, constant: 0)
scrollView.addConstraint(nextButtonBaseline)
To this:
//Align baselines - this also occurs with the option above of AlignAllBaselines and leaving this code out
let aglLabelBaseline = NSLayoutConstraint(item: aglAltitudeLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 14)
self.view.addConstraint(aglLabelBaseline)
let nextButtonBaseline = NSLayoutConstraint(item: nextButton, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 8)
self.view.addConstraint(nextButtonBaseline)
it works exactly as I'd expect. Why the difference?
Without knowing what your requirements are, it's hard to give you good advice. Here is an example of how I might do it. I added all the views in code, and added background colors to the views so I could better see their layout. I'm assuming that the scroll view is only added once, and never removed, so it's constraints to self.view should only be added once in viewDidLoad. Same goes for the height of the the other views if you don't need to change them (notice that I add the height constraints to the view itself, not its superview). The only thing that needs to be removed and re-added are the constraints between the scroll view and its subviews.
class ViewController: UIViewController {
var aglAltitudeLabel = UILabel()
var aglAltitude = UITextField()
var nextButton = UIButton.buttonWithType(.System) as UIButton
var scrollView = UIScrollView()
var viewsDict: NSDictionary!
override func viewDidLoad() {
super.viewDidLoad()
aglAltitudeLabel.text = "AGL Altitude"
aglAltitudeLabel.backgroundColor = UIColor.lightGrayColor()
aglAltitudeLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
aglAltitude.setTranslatesAutoresizingMaskIntoConstraints(false)
aglAltitude.backgroundColor = UIColor.yellowColor()
aglAltitudeLabel.addConstraint(NSLayoutConstraint(item: aglAltitudeLabel, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 30))
scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
nextButton.setTranslatesAutoresizingMaskIntoConstraints(false)
nextButton.setTitle("Next", forState: .Normal)
nextButton.backgroundColor = UIColor.lightGrayColor()
nextButton.addConstraint(NSLayoutConstraint(item: nextButton, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 30))
self.view.addSubview(scrollView)
scrollView.addSubview(aglAltitude)
scrollView.addSubview(aglAltitudeLabel)
scrollView.addSubview(nextButton)
scrollView.backgroundColor = UIColor(red: 1, green: 1, blue: 0.8, alpha: 1)
viewsDict = ["scrollView": scrollView, "aglAltitudeLabel": aglAltitudeLabel, "aglAltitude":aglAltitude, "nextButton":nextButton] as NSDictionary
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|[scrollView]|", options: nil, metrics: nil, views: viewsDict))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-64-[scrollView]|", options: nil, metrics: nil, views: viewsDict))
self.redoConstraints()
}
#IBAction func redoConstraints() {
scrollView.removeConstraints(scrollView.constraints())
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-8-[aglAltitude(30)]-8-|", options: nil, metrics: nil, views: viewsDict))
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-8-[aglAltitudeLabel(97)]-8-[aglAltitude(>=100)]-8-[nextButton(46)]-8-|", options: .AlignAllBottom , metrics: nil, views: viewsDict))
}
}
Notice that I took the fixed width and height out of the constraints for the scroll view. There's no need for that, and it would cause it to not work correctly on rotation.

Resources