I am trying to get better at all programatic constraints.
I created an interface representation of what I wanted to do to make sure everything turned blue and gave no errors when ran.
In interface builder I created a UIView, set the ratio to 1:1, top anchor 100, and bottom anchor 450, and horizontally constrained it.
Works great.
Now I deleted all of that and do it via code
I think this should be identical to the IB version...
let testView = UIView()
view.addSubview(testView)
testView.backgroundColor = .red
let testViewTopConstraint = testView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100)
let testViewBottomConstraint = testView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 450)
let testViewCenterXConstraint = testView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let testViewAspectConstraint = NSLayoutConstraint(item: testView,
attribute: .height,
relatedBy: .equal,
toItem: testView,
attribute: .width,
multiplier: (1.0 / 1.0),
constant: 0)
NSLayoutConstraint.activate([testViewTopConstraint, testViewBottomConstraint, testViewCenterXConstraint, testViewAspectConstraint])
testView.translatesAutoresizingMaskIntoConstraints = false
When I run it, the red square becomes a rectangle extending to the end (possible beyond).
I have played around with programatic constraints. If I were to add a leading constraint I am perfectly fine, it just seems to be my bottom constraint that always messes me up.
What is wrong with my bottom constraint?
Or am I doing something else wrong?
There are a few things but to answer your particular issue here you just have to add a - to the 450 constant
let testViewBottomConstraint = testView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -450)
You can also omit the NSLayoutConstraint by adding a few more anchors.
Related
Why do some developers add constraints like this:
NSLayoutConstraint(item: myView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1.0, constant: 20.0).isActive = true
And some like this:
myView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 20).isActive = true
They basically do the same thing... right? So what is the difference between them? Why should one be used instead of the other? Is there a performance difference for using one over the other?
At the place I worked our iOS lead was using the NSLayoutConstraint initialization way exclusively and everyone was forced to do the same to have more consistency and readability throughout the entire code, I like both ways, I just want to know if there was/is ever any benefit of using one over the other? Or are the differences just based on preference?
In large part, it is new syntax and readability, but there are still some things that you can do with NSLayoutConstraint(...) that you cannot do the "new" way.
For example, let's take a simple task of adding a UILabel centered horizontally, 40-pts from the bottom. Each of the following examples will begin with this:
override func viewDidLoad() {
super.viewDidLoad()
let myView = UILabel()
myView.backgroundColor = .green
myView.text = "Hello"
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
// add constraints....
}
So, our first method looks like this:
// center horizontally
NSLayoutConstraint(item: myView,
attribute: .centerX,
relatedBy: .equal,
toItem: view,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0).isActive = true
// bottom = 40-pts from view Bottom
NSLayoutConstraint(item: myView,
attribute: .bottom,
relatedBy: .equal,
toItem: view,
attribute: .bottom,
multiplier: 1.0,
constant: -40.0).isActive = true
We can get the exact same results using this syntax, which would generally be considered more "readable":
// center horizontally
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
// bottom = 40-pts from view Bottom
myView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -40.0).isActive = true
Now, we'll usually have many more constraints to set, so we can make it even more readable with this (eliminates the .isActive = true at the end of every line):
NSLayoutConstraint.activate([
// center horizontally
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
// bottom = 40-pts from view Bottom
myView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -40.0),
])
So... What happens if we throw in just a little complexity, such as saying "Keep the Bottom of the Label 20% from the bottom of the view"?
To stick with the "new, more readable" syntax, we have a couple options...
1 - constrain the bottom of the Label to the bottom of the view, wait until layout is finished - so we know the height of the view - and then set the .constant on the bottom anchor to -(view height * 0.2). That will work, but we have to re-calculate every time the label's superview changes (such as on device rotation).
2 - add a UIView as a "bottom spacer":
// add a hidden UIView for bottom "space"
let spacerView = UIView()
spacerView.isHidden = true
view.addSubview(spacerView)
spacerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// spacerView at bottom, height = 20% of view height
spacerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
spacerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2),
// center horizontally
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
// bottom = spacerView Top
myView.bottomAnchor.constraint(equalTo: spacerView.topAnchor, constant: 0.0),
])
That works, and handles superview size changes, but we've added another view to the view hierarchy. For this simple example, no big deal, but we probably don't want to add a bunch of them for a complex layout.
3 - add a UILayoutGuide as a "bottom spacer":
// add a UILayoutGuide for bottom "space"
let spacerGuide = UILayoutGuide()
view.addLayoutGuide(spacerGuide)
NSLayoutConstraint.activate([
// spacerGuide at bottom, height = 20% of view height
spacerGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
spacerGuide.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2),
// center horizontally
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
// bottom = spacerGuide Top
myView.bottomAnchor.constraint(equalTo: spacerGuide.topAnchor, constant: 0.0),
])
Accomplishes the same thing, but now we're using a non-rendering UI element so we're not weighing down the view hierarchy.
4 - Use the NSLayoutConstraint(...) syntax, and avoid all of that:
// center horizontally
NSLayoutConstraint(item: myView,
attribute: .centerX,
relatedBy: .equal,
toItem: view,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0).isActive = true
// bottom = 80% of view bottom (leaves 20% space at bottom)
NSLayoutConstraint(item: myView,
attribute: .bottom,
relatedBy: .equal,
toItem: view,
attribute: .bottom,
multiplier: 0.8,
constant: 0.0).isActive = true
}
So, for most situations, it is a matter of preference and/or consistency, but you will find the occasional case where there is a difference.
Anchors were added later. They look cleaner and you can more easily constrain to the safe area as it has its own anchors.
I created a UIButton in the middle of a TabBarController but it is not correctly displayed on the iPhone X because of its safe areas in the bottom of the phone.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//Frame mic button
micButton.frame = CGRect.init(x: self.tabBar.center.x - 62, y: self.view.bounds.height - 94, width: 124, height: 124)
micButton.layer.cornerRadius = 62
}
What is the correct way for drawing this UIButton without touching the safe areas.
Safe zones, specifically a safeAreaLayoutGuide, relates to constraints.
There are various ways to code auto layout constraints (Visual Format Layout or VFL, explicit - and wordy - NSLayoutConstraints) but my preference is to use "layout anchors".
The basic idea for any layout is position and size. Give something a width/height, and x/y axis value and you've got it. Pretty much like frames.
So the basic "anchor" way of laying out this:
micButton.frame = CGRect.init(x: self.tabBar.center.x - 62, y: self.view.bounds.height - 94, width: 124, height: 124)
Would be this:
let micButton = UIButton()
micButton.translatesAutoresizingMaskIntoConstraints = false
micButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -94).isActive = true
micButton.widthAnchor.constraint(equalToConstant: 124).isActive = true
micButton.heightAnchor.constraint(equalToConstant: 124).isActive = true
micButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: -62).isActive = true
Some notes:
Yes, you do not dictate a frame. In fact, you just initialize things when possible.
Since you aren't using IB, you need to set the auto resize mask flag to false. This is something even the most experienced developers sometimes forget. (Doing so usually means you have "unexpected" results ranging from not seeing something to seeing it misplaced.)
I've set the bottom or Y axis, the width and height, and finally the X axis.
While it's more code than using a frame, what you gain is consistency across screen sizes.
But you want more - you need to code for iPhone X safe zones. You have two tools Apple has given you: layoutMarginsGuide and safeAreaLayoutGuide. The former was introduced in iOS 9 (along with the more simple layoutGuide and layout anchors) while the latter was introduced in iOS 11.
[My assumption, probably a safe one, is that all iPhone X devices will run iOS 11 or later. Since a "safe area" is really only needed for this device, the following is what you need.]
Margins work with all devices for leading/trailing (or horizontal) edges. They also work for top/bottom (or vertical) edges too. But for iPhone X you need to be concerned with a different top/bottom, thus the "safe area".
let layoutGuideTop = UILayoutGuide()
let layoutGuideBottom = UILayoutGuide()
view.addLayoutGuide(layoutGuideTop)
view.addLayoutGuide(layoutGuideBottom)
let margins = view.layoutMarginsGuide
view.addLayoutGuide(margins)
if #available(iOS 11, *) {
let guide = view.safeAreaLayoutGuide
layoutGuideTop.topAnchor.constraintEqualToSystemSpacingBelow(guide.topAnchor, multiplier: 1.0).isActive = true
layoutGuideBottom.bottomAnchor.constraintEqualToSystemSpacingBelow(guide.bottomAnchor, multiplier: 1.0).isActive = true
} else {
layoutGuideTop.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
layoutGuideBottom.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
}
The code snippet above will yield the correct top/bottom margin variables (layoutGuideTop and layoutGuideBottom) based on the iOS version. From there you can adjust your bottom anchor to:
micButton.bottomAnchor.constraint(equalTo: laytouGuideBottom, constant: -94).isActive = true
Which should set micButton to be 94 points above the bottom safe area.
Here's a few links that should help you with layout anchors and guides:
Layout Anchors
Layout Guides
Safe Area Layout Guides
EDIT:
One last note about constraints. Since you aren't relying on frame values, all of this code is best placed in viewDidLoad, because the layout engine will determine everything appropriately (and viewDidLayoutSubviews may be called more than once).
Try this :
Add in UITabViewController's class
micButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(micButton)
if #available(iOS 11, *) {
let guide = view.safeAreaLayoutGuide
micButton.centerXAnchor.constraint(equalTo: guide.centerXAnchor).isActive = true
micButton.bottomAnchor.constraint(equalTo: guide.bottomAnchor).isActive = true
micButton.heightAnchor.constraint(equalToConstant: 64).isActive = true
micButton.widthAnchor.constraint(equalToConstant: 64).isActive = true
} else {
NSLayoutConstraint(item: micButton, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: micButton, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0).isActive = true
micButton.heightAnchor.constraint(equalToConstant: 64).isActive = true
micButton.widthAnchor.constraint(equalToConstant: 64).isActive = true
}
Thanks to #dfd guidance this was the way i did it as a work around. I just checked if the phone using the app has the same screen size as iPhoneX and i just changed in the constraints.
override func viewDidLoad() {
super.viewDidLoad()
//Frame mic button
micButton.frame = CGRect.init(x: self.tabBar.center.x - 62, y: self.view.bounds.height - 94, width: 124, height: 124)
micButton.layer.cornerRadius = 62
micButton.translatesAutoresizingMaskIntoConstraints = false
micButton.setBackgroundImage(#imageLiteral(resourceName: "micIcon"), for: .normal)
//Add to tabbar view
self.view.insertSubview(micButton, aboveSubview: self.tabBar)
if UIDevice().userInterfaceIdiom == .phone {
if UIScreen.main.nativeBounds.height == 2436 {
//iPhoneX Device
micButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
micButton.widthAnchor.constraint(equalToConstant: 124).isActive = true
micButton.heightAnchor.constraint(equalToConstant: 124).isActive = true
micButton.centerXAnchor.constraint(equalTo: self.tabBar.centerXAnchor, constant: 0).isActive = true
} else {
//Not an iPhoneX Device
micButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 25).isActive = true
micButton.widthAnchor.constraint(equalToConstant: 124).isActive = true
micButton.heightAnchor.constraint(equalToConstant: 124).isActive = true
micButton.centerXAnchor.constraint(equalTo: self.tabBar.centerXAnchor, constant: 0).isActive = true
}
}
// Do any additional setup after loading the view.
}
This code help you to solve your problem.
micButton.bottomAnchor.constraint(equalTo:view.safeAreaLayoutGuide.bottomAnchor).isActive = true
I'm trying to center an image to a view but I'm getting error..this is my code:
let markerimage = #imageLiteral(resourceName: "ic_new_mark_icon")
let size = CGSize(width: 60, height: 60)
var newimage = imageWithImage(image: markerimage, scaledToSize: size)
var imageview = UIImageView(image: newimage)
self.view.addSubview(imageview)
let xConstraint = NSLayoutConstraint(item: imageview, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0)
let yConstraint = NSLayoutConstraint(item: imageview, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 0)
imageview.addConstraint(xConstraint)
imageview.addConstraint(yConstraint)
and my error is
2016-12-22 16:29:11.305417 Googlemappractice[21426:362773] [LayoutConstraints] The view hierarchy is not prepared for the constraint: <NSLayoutConstraint:0x600000285f00 UIImageView:0x7ff772f15560.centerX == UIView:0x7ff772c0bd30.centerX (inactive)>
When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled. Break on -[UIView(UIConstraintBasedLayout)
The constraints need to be added to the earliest common ancestor of the two views affected by the constraint. In this case, self.view is earlier in the hierarchy so the constraints should be added to it instead of to imageview.
Since iOS 8, there is an easier way. Once you've added your view to the view hierarchy (with addSubview), you can activate the constraints instead of adding them to views.
You can do this by setting the isActive property to true:
xConstraint.isActive = true
yConstraint.isActive = true
or by using the activate class function of NSLayoutConstraint to active multiple constraints:
NSLayoutConstraint.activate([xConstraint, yConstraint])
As mentioned by #dfd in the comments, you should also set this flag:
imageview.translatesAutoresizingMaskIntoConstraints = false
This tells iOS to not create constraints for imageview based upon its frame. If you don't do this, you'll end up with conflicting constraints.
You are going to need width and height constraints for your imageview as well, because you've only specified its position so far.
I created StackView on main.storyboard and my idea was to add some buttons of constant size 40x40 with images on them (that are originally 400x400px but they should scale down to fill that 40x40) and they should stack up like this:
(stack view is invisible, but i marked it red on pic,its 170x50 space in the top right where the animals are)
so in short: all buttons(with pictures of animals) should be same size 40x40 and populated to the left(or to the right) with some spacing of like 2px in between them.
In code i wrote this:
func setupIcon(iconNumber:Int) -> UIButton{
myImageButton = UIButton()
myImageButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
myImageButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
myImageButton.isUserInteractionEnabled = false
//myImageButton.backgroundColor = UIColor.gray
let widthConstraint = NSLayoutConstraint(item: myImageButton, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1, constant: 40)
let heightConstraint = NSLayoutConstraint(item: myImageButton, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1, constant: 40)
NSLayoutConstraint.activate([widthConstraint, heightConstraint])
switch(iconNumber){
case 0:
myImageButton.setImage(UIImage(named: "hamster")!, for: .normal)
return myImageButton
case 1:
myImageButton.setImage(UIImage(named: "dog")!, for: .normal)
return myImageButton
...
default:
myImageButton.setImage(UIImage(named: "hamster")!, for: .normal)
return myImageButton
}
}
to return UIButton which i then add to my StackView by calling:
cell.iconStackView.addArrangedSubview(setupIcon(iconNumber: pett.pic))
and on my StackView settings I have:
Axis: Horizontal
Alignment: Center
Distribution: Equal Spacing
Content mode: Left
Semantic: Unspecified
but that gives me this kind of behavior of those buttons:
and as you can see there are several problems:
1) the constrains i added no longer work when there is only 1 button present
2) i cannot figure out what should i set my StackView settings to make them all go closer to each other and to the right with rest of the space to the left blank
can you help me out what to do to get my desired effect on these buttons?
Thanks.
note: every cell with those numbers "13:00" etc have different number of buttons present. My goal is to have all animals to the right, no matter if 1 or 3-4. Images shouldn't stretch like the dog, or be spaced like those 2 in the second or third row
Don't use a stack view in this situation. Simple autolayout constraints are all you need to line up three buttons on the right side like that.
Using Swift and autolayout, what's the best way to put a label and custom view in a cell? I want the label on the left and the view on the right. The view could be any subclass of UIView. One problem I'm having is sizing the label's width to fit the text. I want the custom view to take up the remainder of the space. I prefer to not use the visual format but instead NSLayoutConstraint():
func tableView(aTableView: UITableView, cellForRowAtIndexPath anIndexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(frame: CGRect(x: 0.0, y: 0.0, width: aTableView.bounds.size.width, height: aTableView.estimatedRowHeight))
let titleLabel = UILabel()
titleLabel.adjustsFontSizeToFitWidth = true
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.numberOfLines = 0
titleLabel.text = "some text"
titleLabel.sizeToFit()
cell.contentView.addSubview(titleLabel)
titleLabel.layer.borderColor = UIColor.redColor().CGColor
titleLabel.layer.borderWidth = 1.0
cell.contentView.addConstraint(NSLayoutConstraint(item: titleLabel, attribute: .Left, relatedBy: .Equal, toItem: cell.contentView, attribute: .Left, multiplier: 1.0, constant: 0.0))
let customView = UIView()
cell.contentView.addSubview(customView)
cell.contentView.addConstraint(NSLayoutConstraint(item: customView, attribute: .Left, relatedBy: .Equal, toItem: titleLabel, attribute: .Right, multiplier: 1.0, constant: 0.0))
cell.contentView.addConstraint(NSLayoutConstraint(item: customView, attribute: .Right, relatedBy: .Equal, toItem: cell.contentView, attribute: .Right, multiplier: 1.0, constant: 0.0))
return cell
}
Try adding the following line:
customView.translatesAutoresizingMaskIntoContraints = false
you might also need an additional constraint to define maybe the max size of the UILabel or the min size of the UIView.
Since you have numberOfLines = 0, the UILabel doesn't know exactly when to break into a new line, since there are no constraints specifying that.
Hope it helps
Create the label and give these constraints
horizontal spacing constraint between custom view and the label
trailing space constraint to content view
Top space and bottom space for the label and custom view
Leading space for the custom view
Here the label automatically grows with the content and the custom view will occupy the remaining space.