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))
Related
I can't seem to find a proper answer on SO. I'm trying to rotate a subclassed UISlider and apply constraints to position it properly, but can't seem to get it to work correctly. In the XIB it has constraints so that I don't have any issues with error showing. (Boss hates errors showing). So I remove the constraints first.
My code looks like this:
removeConstraints([sliderHeight, sliderWidth, sliderLeading, sliderBottom])
let rotation = CATransform3DMakeRotation(-CGFloat(M_PI_2), 0.0, 0.0, 1.0)
self.layer.transform = rotation
let views = ["slider":slider, "deviceIcon":deviceIcon]
var constraints = NSLayoutConstraint.constraintsWithVisualFormat("H:[deviceIcon]-8-[slider]", options: .DirectionLeadingToTrailing, metrics: nil, views: views)
constraints.append(NSLayoutConstraint(item: deviceIcon, attribute: .Bottom, relatedBy: .Equal, toItem: slider, attribute: .Bottom, multiplier: 1, constant: 0))
constraints.append(NSLayoutConstraint(item: slider, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 30))
constraints.append(NSLayoutConstraint(item: slider, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 115))
addConstraints(constraints)
Once it runs, the frames look like this (white area between grey and orange vertical dashes is the thumb) :
It doesn't have the proper width/height and it isn't aligned to the bottom of the deviceIcon on its left. (I think it might be the proper distance from the deviceIcon, I can't tell.)
How can I do this properly?
So the answer is: Don't. Simply adjust the existing leading and bottom constraints so that the rotated slider is in the proper position.
I simply want to set constraints which allows the UIWebView to fill the screen. (Screen has a UINavigationbar and a UIToolBar), I've tried following the examples given in the apple documentation but always get a warning and my view doesnt show up. Below is what I've tried.
private func addSubviews(){
navigationController?.setToolbarHidden(false, animated: false)
// webView = UIWebView(frame: CGRect(x: 0, y: 0, width: UIScreen.mainScreen().bounds.width, height: UIScreen.mainScreen().bounds.height))
webView = UIWebView(frame: CGRectZero)
webView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(webView)
let views = ["myView" : webView]
let formatString = "|-[myView]-|"
let constraints = NSLayoutConstraint.constraintsWithVisualFormat(formatString, options:[] , metrics: nil, views: views)
NSLayoutConstraint.activateConstraints(constraints)
}
Oofh, visual constraints make my head hurt. I never was a fan of the visual language, never made any sense to me. This may be a bit more code but I think it's much easier to read as a developer and far more expressive as to intent.
self.view.addSubview(webView)
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .Left, relatedBy: .Equal, toItem: webView, attribute: .Left, multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .Top, relatedBy: .Equal, toItem: webView, attribute: .Top, multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .Bottom, relatedBy: .Equal, toItem: webView, attribute: .Bottom, multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .Right, relatedBy: .Equal, toItem: webView, attribute: .Right, multiplier: 1, constant: 0))
These constraints pin all edges of a view to the respective edge with a distance of 0.
Then at the end of your constraints, don't forget:
self.view.layoutIfNeeded()
My problem with the visual language is I can't look at it and immediately be clear as to what it does. Here, it's quite explicit. Just for fun, here's a nice function you can use whenever your constant and multiplier is 0:
/**
Adds an edge constraint between the superview and subview with a constant of 0.
- parameter attribute: Layout attribute.
- parameter subview: Subview.
*/
func addEdgeConstraintWithAttribute(attribute: NSLayoutAttribute, withSubview subview: UIView) {
self.addConstraint(NSLayoutConstraint(item: subview, attribute: attribute, relatedBy: .Equal, toItem: self, attribute: attribute, multiplier: 1, constant: 0))
}
Used like:
self.addEdgeConstraint(.Top, withSubview: webView)
where the function is an extension on UIView
Im my opinion, your view didn't show up because you didn't set up enough constraints for it.
Basically, with a view, you should add constraints to let it know at least the position and the size. In some cases, you dont need to setup size constraints because that view has intrinsic content size like UILabel, UIButton
Back to your example, you should add constraints like this:
private func addSubviews(){
navigationController?.setToolbarHidden(false, animated: false)
// webView = UIWebView(frame: CGRect(x: 0, y: 0, width: UIScreen.mainScreen().bounds.width, height: UIScreen.mainScreen().bounds.height))
let webView = UIWebView(frame: CGRectZero)
webView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(webView)
let views = ["myView" : webView]
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[myView]-|", options:[] , metrics: nil, views: views))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[myView]-|", options:[] , metrics: nil, views: views))
}
H in H:|-[myView]-| stands for horizontal.
V in V:|-[myView]-| stands for vertical.
I've got an issue with a custom UIView implementation. What I am trying to do is animate a UICollectionViewCell, and creating a new view after the cell scales and 'flips' on the other side. (Think of the UICollectionViewCells as cards).
I successfully manage to achieve the desired animation. However, when I create a view in code from the XIB file, the constraints seem to be applied but everything is blown up in size. I set the minimum font scale for the labels to 0.2, but it doesn't seem to be working.
This is the view designed in IB:
View designed in IB with constraints
Now here is what I get when the animation proceeds to add a new CardBackView to the cell's view:
The horrendous result
This is the code that does the animation and adds the card back view:
// now flip card and show other side of card
UIView.transitionWithView(cell, duration: duration, options: [.BeginFromCurrentState, .TransitionFlipFromRight, .CurveEaseInOut], animations: { () -> Void in
let cardBackView = NSBundle.mainBundle().loadNibNamed("CardBackView", owner: self, options: nil).first as! CardBackView
cardBackView.frame = cell.bounds
cardBackView.titleLabel.text = movie.title
cardBackView.titleLabel.textColor = UIColor.whiteColor()
cardBackView.overviewLabel.text = movie.overview
cardBackView.backgroundColor = UISettings.TabBarColor
cell.addSubview(cardBackView)
}, completion: nil)
When you're adding your CardBackView to your Cell it assumes that your translating your autoresizing mask into constraints, so it might behave weirdly. To fix this you need to create constraints for your CardBackView and add them to your Cell (it's parent).
If your CardBackView should use the same size as your Cell, something like this should work:
cardBackView.translatesAutoresizingMaskIntoConstraints = false
cell.addConstraint(NSLayoutConstraint(item: cardBackView, attribute: .Top, relatedBy: .Equal, toItem: cell, attribute: .Top, multiplier: 1, constant: 0))
cell.addConstraint(NSLayoutConstraint(item: cardBackView, attribute: .Bottom, relatedBy: .Equal, toItem: cell, attribute: .Bottom, multiplier: 1, constant: 0))
cell.addConstraint(NSLayoutConstraint(item: cardBackView, attribute: .Left, relatedBy: .Equal, toItem: cell, attribute: .Left, multiplier: 1, constant: 0))
cell.addConstraint(NSLayoutConstraint(item: cardBackView, attribute: .Right, relatedBy: .Equal, toItem: cell, attribute: .Right, multiplier: 1, constant: 0))
I have constraint code that lays out a number of UILabels (4+) vertically from top to bottom inside a container view (a regular UIView). I now want my container view to be sized so that its height matches the bottom of that last label that I've added.
I have tried this:
let constraintPanelHeight = NSLayoutConstraint(item: cell.panelOptions,
attribute: .Height, relatedBy: .Equal, toItem: priorLabel!,
attribute: .Bottom, multiplier: 1, constant: 0)
cell.contentView.addConstraint(constraintPanelHeight)
but this generates an Invalid pairing of layout attributes error since I'm matching .Height of one view with .Bottom of a subview (I'm guess that's why).
How can I auto-size my containing view like this?
I don't know what your trying to achieve there exactly. If you want to pin the one cell to the bottom of the other, you'll have to use attribute .Top and another constraint for the height. For example: you have 10 labels in your view, then set your height to one tenth of the superview:
let heightConstraint = NSLayoutConstraint(item: cell.panelOptions, attribute: .Height, relatedBy: .Equal, toItem: superview, attribute: .Height, multiplier: 0.1, constant: 0)
But since iOS9 there's also the UIStackView which makes laying out subviews in a view (vertically or horizontally) very easy. Have a look at that if you want to spread your labels evenly in your superview.
If you are fixed number of views the easiest and most readable way is the use the visual layout format and constraints like this:
NSLayoutConstraint.constraints
WithVisualFormat("V:|[view1][view2][view3]|",
options: NSLayoutFormatOptions.AlignAllCenterX,
metrics: nil,
views: ["view1": view1, "view2": view2,"view3": view3])
If you want to add a variable number the principle is the same but achieved with a loop. You shouldn't set the height of anything. Let the intrinsicContentSize of each label size the container from the inside. You might need to set the priority of contentCompressionResistance of each label to 1000 just to be sure the labels don't get squashed. Keep in mind the you will need horizontal constraints as well but they should be simpler to work out.
And here's the version for a variable number of subviews:
var prevView : UIView?
for view in views{
container.addSubview(view)
//
// Add horizontal constraints for each view to fit the container
// exclude for simplicity
// Add vertical constraints
if prevView == nil{
container.addConstraint(NSLayoutConstraint(item: container,
attribute: .Top,
relatedBy: .Equal,
toItem: view,
attribute: .Top,
multiplier: 1, constant: 0)
)
} else if view == views.last!{
container.addConstraint(NSLayoutConstraint(item: container,
attribute: .Bottom,
relatedBy: .Equal,
toItem: view,
attribute: .Bottom,
multiplier: 1,
constant: 0))
} else if let prev = prevView {
container.addConstraint(NSLayoutConstraint(item: prev,
attribute: .Bottom,
relatedBy: .Equal,
toItem: view,
attribute: .Top,
multiplier: 1,
constant: 0))
}
prevView = view
}
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.