Adding buttons (with constant width) programmatically to UIStackView - swift

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.

Related

Difference between using NSLayoutConstraints initializer compared to Anchors for setting constraints

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.

Swift Programatic constraint view.bottomAnchor causing issue

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.

Getting run time error when adding constraints

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.

Best way to programmatically layout UILabel and UIView subviews in UITableViewCell in Swift

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.

Swift auto layout programmatically and dynamic

I have a View where I load buttons dynamically. So I have a for loop to loop through all buttons. Since this is dynamically I want to create the auto layout programmatically. Right now I have the following code:
for var i = 0; i < data.count; i++ {
let button = UIButton.buttonWithType(UIButtonType.System) as! UIButton
button.backgroundColor = UIColor.greenColor()
button.setTitle("Button", forState: UIControlState.Normal)
button.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(button)
let centerXConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0)
let centerYConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant:15)
let widthConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 200)
let heightConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant:100)
scrollView.addConstraints([centerXConstraint, centerYConstraint, widthConstraint, heightConstraint])
}
This creates the first button and places it 15px under the top bar. The problem I have is how to place the next button 15px under the first one, the third button 15px under the second one etc. Anyone got any ideas?
Thats definitely possible, but firstly, I should mention it's not required that you add constraints for the buttons' width and height since they have an intrinsic content size (like UILabel) which depends on attributes such their text and font.
Back to your problem!
Here's the code, the explanation for each step is below:
override func viewDidLoad() {
super.viewDidLoad()
// 1.
var upperView: UIView = scrollView
for i in 0..<data.count {
let button = UIButton.buttonWithType(UIButtonType.System) as! UIButton
button.backgroundColor = UIColor.greenColor()
button.setTitle("Button", forState: UIControlState.Normal)
button.setTranslatesAutoresizingMaskIntoConstraints(false)
// 2.
scrollView.addSubview(button)
// 3.
let attribute: NSLayoutAttribute = i == 0 ? .Top : .Bottom
// 4.
let topEdgeConstraint = NSLayoutConstraint(item: button,
attribute: .Top,
relatedBy: .Equal,
toItem: upperView,
attribute: attribute,
multiplier: 1.0,
constant: 15.0)
let centerXConstraint = NSLayoutConstraint(item: button,
attribute: .CenterX,
relatedBy: .Equal,
toItem: scrollView,
attribute: .CenterX,
multiplier: 1.0,
constant: 0.0)
scrollView.addConstraint(topEdgeConstraint)
scrollView.addConstraint(centerXConstraint)
// 5.
if i == data.count - 1 {
let bottomConstraint = NSLayoutConstraint(item: button,
attribute: .Bottom,
relatedBy: .Equal,
toItem: scrollView,
attribute: .Bottom,
multiplier: 1.0,
constant: -15.0)
scrollView.addConstraint(bottomConstraint)
}
upperView = button
}
}
1. upperView is used to keep track of the view 'above' the current button. For example, when the first button is created, the upperView is the UIScrollView For the second button, upperView is the first button; for the third button, upperView is the second button and so on...
2. Your buttons should be added to the UIScrollView, not the self.view. Otherwise you'll get the error:
The view hierarchy is not prepared for the constraint...
3. This line selects the attribute on the upperView that will relate the button to the upperView. Here's a picture to demonstrate what I mean:
a) The .Top of the button is related to the .Top of the UIScrollView.
b) The .Top of the button is related to the .Bottom of the previous UIButton.
4. Making the top and centre X constraints - that's all pretty self explanatory.
5. For the UIScrollView to correctly calculate its contentSize it must have constraints in an unbroken chain from it top to bottom (the top to the bottom, in this case, because it needs to scroll vertically). Therefore, if it's the last UIButton a constraint from its bottom edge is added to the UIScrollView's bottom edge.
Hope that helps!