ios: UIImageView with Aspect Fill not fitting the constraint - ios

I have a following design. Using Storyboard
I have tried multiple methods, but all efforts in vain.
1 - Main Default View of View Controller
2 - View to contain Image view and 2 equal widths buttons
3 - Image view with Aspect Fill (Need to take up as much space based on iPhone size)
Everything I got right except 1 constraint. Constraint between the ImageView and View containing the 2 buttons. (Using Stack view for buttons)
In certain screens sizes, image view with Aspect fill overlaps the stackview.
Aspect Fit leaves lot of white space.
Image View needs to scale for all iPhones based on size ! (That seems to be solved by Aspect Fill, not aspect fit)
How to solve this problem of keeping Image + 2 buttons vertically centered (Rectangle 2 to be vertically and horizontally centered) while ImageView scales ?
Edit: I have tried "Clip to Bound" as well, but it clips the image on the sides. I would like to know the full setup, just what should be done with each of views or how each view should be organized to get the effect ?

maybe your constraints are set up correctly. try to set the "Clip To Bounds" flag for the imageview.
except of that you could add an aspect ratio constraint to your imageview that matches the aspect ratio of the image it contains.
UPDATE
this is how you would set up the constraints programmatically (in viewDidLoad for example):
let containerView: UIView = {
let cv = UIView()
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = UIColor.lightGrayColor()
return cv
}()
let imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
iv.backgroundColor = UIColor.darkGrayColor()
iv.image = UIImage(named: "Forest") // YOUR IMAGE HERE
return iv
}()
let leftButton: UIButton = {
let lb = UIButton(type: .System)
lb.translatesAutoresizingMaskIntoConstraints = false
lb.setTitle("Left Button", forState: .Normal)
return lb
}()
let rightButton: UIButton = {
let rb = UIButton(type: .System)
rb.translatesAutoresizingMaskIntoConstraints = false
rb.setTitle("Right Button", forState: .Normal)
return rb
}()
let stackView: UIStackView = {
let sv = UIStackView()
sv.translatesAutoresizingMaskIntoConstraints = false
sv.axis = .Horizontal
sv.distribution = .FillEqually
return sv
}()
stackView.addArrangedSubview(leftButton)
stackView.addArrangedSubview(rightButton)
view.addSubview(containerView)
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-[cv]-|", options: [], metrics: nil, views: ["cv": containerView]))
containerView.centerYAnchor.constraintEqualToAnchor(view.centerYAnchor).active = true
containerView.addSubview(imageView)
containerView.addSubview(stackView)
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-[iv]-|", options: [], metrics: nil, views: ["iv": imageView]))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[iv]-[sv]-|", options: [.AlignAllLeading, .AlignAllTrailing], metrics: nil, views: ["iv": imageView, "sv": stackView]))
let image = imageView.image!
imageView.addConstraint(NSLayoutConstraint(item: imageView, attribute: .Width, relatedBy: .Equal, toItem: imageView, attribute: .Height, multiplier: image.size.width / image.size.height, constant: 0))
and this is the result:

Related

Center multiple independent items in swift

As you can see, two's a label , two images and two labels, all independent. I need to center all of these items,
i am doing this job programmatically. please suggest
try with way but failed :
func setupViews(){
view.addSubview(orderLocation)
orderLocation.addSubview(ownOrderTagImage)
orderLocation.addSubview(ownOrderTagLabel)
orderLocation.addSubview(ownOrderLocationImage)
orderLocation.addSubview(ownOrderLocationLabel)
//Change the width and height accordingly
view.addConstraintsWithFormat(format: "H:|[v0]|", views: orderLocation)
view.addConstraintsWithFormat(format: "V:|-50-[v0(20)]|", views: orderLocation)
orderLocation.addConstraintsWithFormat(format: "H:|[v0(18)][v1(70)]-20-[v2(18)][v3(80)]|", views: ownOrderTagImage, ownOrderTagLabel,ownOrderLocationImage,ownOrderLocationLabel)
orderLocation.addConstraintsWithFormat(format: "V:|[v0(20)]", views: ownOrderTagImage)
orderLocation.addConstraintsWithFormat(format: "V:|[v0(20)]", views: ownOrderTagLabel)
orderLocation.addConstraintsWithFormat(format: "V:[v0(20)]|", views: ownOrderLocationImage)
orderLocation.addConstraintsWithFormat(format: "V:[v0(20)]|", views: ownOrderLocationLabel)
}
You have a group of four views that you want to lay out in a row, and you want to center the group as a whole within a superview.
There is no anchor at the center of the group for you to use in a constraint. All of the anchors available are at the centers or edges of the individual subviews.
You cannot constrain “distance from leftmost item to superview” to be equal to “distance from rightmost item to superview”, because that constraint would involve four anchors, and an auto layout constraint can only involve two anchors.
One solution is to add a helper view that tightly surrounds the group of views. Then you can constrain the helper view's center anchor to the superview's center anchor.
The simplest way to do it is to put the group of views in a stack view, and center the stack view. If you use a stack view, you'll also need a “spacer” view to separate your “tag” views from your “location” views.
Here's my test result:
Here's my test playground:
import UIKit
import PlaygroundSupport
let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 100))
rootView.backgroundColor = #colorLiteral(red: 0.9568627477, green: 0.6588235497, blue: 0.5450980663, alpha: 1)
PlaygroundPage.current.liveView = rootView
let tagImageView = UIImageView(image: UIImage(named: "Toggle"))
tagImageView.contentMode = .scaleAspectFit
let tagLabel = UILabel()
tagLabel.text = "#YUIO9N"
let locationImageView = UIImageView(image: UIImage(named: "Gear"))
locationImageView.contentMode = .scaleAspectFit
let locationLabel = UILabel()
locationLabel.text = "Garmany"
let spacer = UIView()
let rowView = UIStackView(arrangedSubviews: [tagImageView, tagLabel, spacer, locationImageView, locationLabel])
rowView.axis = .horizontal
rowView.translatesAutoresizingMaskIntoConstraints = false
rootView.addSubview(rowView)
NSLayoutConstraint.activate([
tagImageView.widthAnchor.constraint(equalToConstant: 18),
spacer.widthAnchor.constraint(equalToConstant: 20),
locationImageView.widthAnchor.constraint(equalToConstant: 18),
rowView.heightAnchor.constraint(equalToConstant: 20),
rowView.centerXAnchor.constraint(equalTo: rootView.centerXAnchor),
rowView.topAnchor.constraint(equalTo: rootView.topAnchor, constant: 20)])
addConstraintsWithFormat is not inherently a part of swift, it's a an extension on the Visual Format Language.
I believe you are using the following code as your extension.
extension UIView {
func addConstraintsWithFormat(_ format: String, views : UIView...) {
var viewsDictionary = [String: UIView]()
for(index, view) in views.enumerated(){
let key = "v\(index)"
viewsDictionary[key] = view
view.translatesAutoresizingMaskIntoConstraints = false
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
You must remember to include information like that in future questions.
Anyways, you are missing the view you are adding the views to.
In your case you should have an ImageView as your superview
First create the ImageView object which in your case is going to be that bar
let imageView: UIImageView = {
let imgV = UIImageView()
//put the name of the image you are using in the quotes
imgV.image = UIImage(named: "")
imgV.layer.masksToBounds = true
imgV.contentMode = .scaleAspectFill
return imgV
}()
Make sure the photo you use is properly formatted and add barView as the super view of your sub views and their constraints.
Try
//here you add the view
view.addSubview(imageView)
imageView.addSubview(ownOrderTagImage)
imageView.addSubview(ownOrderTagLabel)
imageView.addSubview(ownOrderLocationImage)
imageView.addSubview(ownOrderLocationLabel)
//Change the width and height accordingly
view.addConstraintsWithFormat(format: "H:|[v0(100)]|", views: imageView)
view.addConstraintsWithFormat(format: "V:|[v0(10)]|", views: imageView)
imageView.addConstraintsWithFormat(format: "H:|[v0][v1]|", views: ownOrderTagImage, ownOrderTagLabel)
imageView.addConstraintsWithFormat(format: "H:|[v0][v1]|", views: ownOrderLocationImage, ownOrderLocationLabel)
imageView.addConstraintsWithFormat(format: "V:|[v0(44)]", views: ownOrderTagImage)
imageView.addConstraintsWithFormat(format: "V:|[v0(44)]", views: ownOrderTagLabel)
imageView.addConstraintsWithFormat(format: "V:[v0(30)]|", views: ownOrderLocationImage)
imageView.addConstraintsWithFormat(format: "V:[v0(30)]|", views: ownOrderLocationLabel)

UIStackView: add arranged views with the same width

I have UIStackView and want add to this view some arranged views... sometimes one, sometimes two, three... etc. UIStackView has full width. I want arranged views to have the same width with alignment left, so that they do not fill full width of UIStackView.
This is what I have:
This is what I want:
Is it possible to do it somehow or do I need to change width of UIStackVIew depending on how many arranged I added?
My code:
// creating views
stackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .center
v.distribution = .fillEqually
v.spacing = 5
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
if let img = chosenImage {
let imageView: UIImageView = {
let l = UIImageView()
l.translatesAutoresizingMaskIntoConstraints = false
l.image = img
return l
}()
self.stackView?.addArrangedSubview(imageView)
imageView.addConstraint(NSLayoutConstraint(item: imageView, attribute: .width, relatedBy: .equal, toItem: imageView, attribute: .height, multiplier: 1, constant: 1))
imageView.setNeedsUpdateConstraints()
} else {
print("Something went wrong")
}
on storyboard, Select stackview, Attributes inspector -> Stackview -> Distribution, select Fill Equally.
You can add an empty clear view to your StackView at the end and set the hugging and resistance priority for this View. It should resize to available space so hugging priority of "fill view" has to be higher and resistance for "fill view" should be low, on your real items set resistance to higher values

Having problems with NSLayoutConstraints and using Auto Layout programmatically, UIViews stuck at (0,0)

I'm trying to programmatically generate a 'score page' where by I have a UILabel and a UISlider for each attribute's score. Since there isn't a fixed number of attributes, I've decided to do this programmatically (as opposed to in story board)
My idea of going about doing this was to create a UIView for each attribute and then insert one UILabel and one UISlider into the UIView, and then setting up constraints after.
However, I'm running into a problem whereby I'm unable to set up the constraints properly, or another huge error that I might have missed out due to inexperience in doing such things. As a result, all the UIViews are stuck to the top left of the screen (0,0) and are on top of one another.
Here's my code so far :
func addLabels(attributesArray: [String], testResultsDictionary: [String:Double]){
var viewsDictionary = [String:AnyObject]()
//let numberOfAttributes = attributesArray.count //(vestigial, please ignore)
let sliderHeight = Double(20)
let sliderWidth = Double(UIScreen.mainScreen().bounds.size.width)*0.70 // 70% across screen
let labelToSliderDistance = Float(10)
let sliderToNextLabelDistance = Float(30)
let edgeHeightConstraint = Float(UIScreen.mainScreen().bounds.size.height)*0.10 // 10% of screen height
for attribute in attributesArray {
let attributeView = UIView(frame: UIScreen.mainScreen().bounds)
attributeView.backgroundColor = UIColor.blueColor()
attributeView.translatesAutoresizingMaskIntoConstraints = false
attributeView.frame.size = CGSize(width: Double(UIScreen.mainScreen().bounds.size.width)*0.80, height: Double(80))
self.view.addSubview(attributeView)
var attributeViewsDictionary = [String:AnyObject]()
let attributeIndex = attributesArray.indexOf(attribute)! as Int
let attributeLabel = UILabel()
attributeLabel.translatesAutoresizingMaskIntoConstraints = false
attributeLabel.text = attribute.stringByReplacingOccurrencesOfString("_", withString: " ")
attributeLabel.sizeToFit()
let attributeSlider = UISlider()
attributeSlider.translatesAutoresizingMaskIntoConstraints = false
attributeSlider.setThumbImage(UIImage(), forState: .Normal)
attributeSlider.frame.size = CGSize(width: sliderWidth, height: sliderHeight)
attributeSlider.userInteractionEnabled = false
if let sliderValue = testResultsDictionary[attribute] {
attributeSlider.value = Float(sliderValue)
}
else {
attributeSlider.value = 0
}
attributeView.addSubview(attributeLabel)
attributeView.addSubview(attributeSlider)
//attributeView.sizeToFit()
attributeViewsDictionary["Label"] = attributeLabel
attributeViewsDictionary["Slider"] = attributeSlider
viewsDictionary[attribute] = attributeView
print(viewsDictionary)
let control_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:|-[\(attribute)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
var control_constraint_V = [NSLayoutConstraint]()
if attributeIndex == 0 {
control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:|-\(edgeHeightConstraint)-[\(attribute)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
}
else if attributeIndex == attributesArray.indexOf(attributesArray.last!){
control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[\(attribute)]-\(edgeHeightConstraint)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
}
else {
control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[\(attributesArray[attributeIndex-1])]-\(sliderToNextLabelDistance)-[\(attribute)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
}
self.view.addConstraints(control_constraint_H)
self.view.addConstraints(control_constraint_V)
let interAttributeConstraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[Label]-\(labelToSliderDistance)-[Slider]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: attributeViewsDictionary)
//let interAttributeConstraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:[Label]-5-[Slider]", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: attributeViewsDictionary)
attributeView.addConstraints(interAttributeConstraint_V)
//attributeView.addConstraints(interAttributeConstraint_H)
//attributeView.sizeToFit()
}
}
Extra Notes:
- An attributeArray looks something like this: ["Happiness", "Creativity", "Tendency_To_Slip"]
The code is extremely messy and unnecessarily long as it is a prototype, so sorry! Please bear with it!
The issue is that these views do not have their constraints fully defined (notably, there were a lot of missing vertical constraints). I also note that you've attempted to set the size of the frame of various views, but that is for naught because when you use auto layout, all frame values will be discarded and recalculated by the auto layout process. Instead, make sure the views dimensions are fully defined entirely by the constraints.
For example:
let spacing: CGFloat = 10
func addLabels(attributesArray: [String], testResultsDictionary: [String: Float]) {
var previousContainer: UIView? // rather than using the index to look up the view for the previous container, just have a variable to keep track of the previous one for you.
for attribute in attributesArray {
let container = UIView()
container.backgroundColor = UIColor.lightGrayColor() // just so I can see it
container.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(container)
// set top anchor for container to superview if no previous container, otherwise link it to the previous container
if previousContainer == nil {
container.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: spacing).active = true
} else {
container.topAnchor.constraintEqualToAnchor(previousContainer!.bottomAnchor, constant: spacing).active = true
}
previousContainer = container
// set leading/trailing constraints for container to superview
NSLayoutConstraint.activateConstraints([
container.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor, constant: spacing),
view.trailingAnchor.constraintEqualToAnchor(container.trailingAnchor, constant: spacing),
])
// create label
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = attribute
container.addSubview(label)
// create slider
let slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
slider.value = testResultsDictionary[attribute]!
container.addSubview(slider)
// constraints for label and slider
NSLayoutConstraint.activateConstraints([
label.topAnchor.constraintEqualToAnchor(container.topAnchor, constant: spacing),
slider.topAnchor.constraintEqualToAnchor(label.bottomAnchor, constant: spacing),
container.bottomAnchor.constraintEqualToAnchor(slider.bottomAnchor, constant: spacing),
label.leadingAnchor.constraintEqualToAnchor(container.leadingAnchor, constant: spacing),
slider.leadingAnchor.constraintEqualToAnchor(container.leadingAnchor, constant: spacing),
container.trailingAnchor.constraintEqualToAnchor(label.trailingAnchor, constant: spacing),
container.trailingAnchor.constraintEqualToAnchor(slider.trailingAnchor, constant: spacing)
])
}
}
Now, I happen to be using the iOS 9 syntax for defining constraints (it is expressive and concise), but if you want/need to use VFL you can do that, too. Just make sure that you define an equivalent set of constraints which are unambiguously defined (top, bottom, leading and trailing). Also note that rather than hardcoding the size of these container views, I let it infer it from the size of its subviews and the container views will resize accordingly.
Having said all of this, I look at this UI and I might be inclined to do this with a table view, which gets you out of the business of having to define all of these constraints, but also gracefully handles the scenario where there are so many of these that you want to enjoy scrolling behavior, too. Or, if I knew that these were always going to be able to fit on a single screen, I might use a UIStackView. But if you want to do it with constraints, you might do something like above.

iOS 9 StackView inside ScrollView does not fill the width of the screen

I'm trying to make a UIScrollView containing a UIStackView with several layers of stack views nested inside. I would like to use AutoLayout, but there is something wrong with what I'm doing, and my brain is getting scrambled trying to figure it out:
import UIKit
class HomeView: UIView {
let looks = sampleLooks
let user = sampleUser
let streamView: UIStackView = UIStackView(arrangedSubviews: [])
let scrollView: UIScrollView = UIScrollView()
func makeButtonWithTitle(title: String, image: UIImage?, selector: String, tag: Int) -> UIButton {
let button = UIButton(type: .System)
button.setImage(image, forState: .Normal)
button.tintColor = UIColor.blackColor()
switch tag {
case 0...10:
button.backgroundColor = UIColor(white: 0.98, alpha: 0.8)
button.titleLabel?.font = UIFont(name: "HelveticaNeue-Thin", size: 30)
default:
button.backgroundColor = UIColor(white: 0.90, alpha: 1.0)
button.titleLabel?.font = UIFont(name: "HelveticaNeue-Thin", size: 40)
}
button.setTitle(title, forState: .Normal)
button.tag = tag
return button
}
func makeMessageView(senderImage: UIImage, senderHandle: String, text: String) -> UIView {
let textView = UILabel()
textView.text = text
textView.font = UIFont(name: "HelveticaNeue-Thin", size: 20)
let senderLabel = UILabel()
senderLabel.text = senderHandle
textView.font = UIFont(name: "HelveticaNeue-Thin", size: 20)
let textStackView = UIStackView(arrangedSubviews:[senderLabel, textView])
textStackView.axis = .Horizontal
textStackView.alignment = .Fill
textStackView.distribution = .Fill
let postView = UIView()
postView.addSubview(textStackView)
postView.backgroundColor = UIColor(white: 0.98, alpha: 0.8)
return postView
}
required init?(coder:NSCoder) {
super.init(coder:coder)
self.contentMode = .ScaleToFill
self.backgroundColor = UIColor(patternImage: UIImage(named: "background")!)
self.streamView.spacing = 20.0
self.streamView.translatesAutoresizingMaskIntoConstraints = false
self.streamView.axis = .Vertical
self.streamView.alignment = .Fill
self.streamView.distribution = .FillEqually
for look in self.looks {
let lookImageView = UIImageView(image: look.photo.photo)
lookImageView.contentMode = .ScaleAspectFit
lookImageView.clipsToBounds = true
let postView = self.makeMessageView(
look.user.photo.photo, senderHandle: look.user.handle, text: look.post)
let likeButton = self.makeButtonWithTitle(
" Like", image: UIImage(named: "like"), selector: "", tag: 0)
let commentButton = self.makeButtonWithTitle(
" Comment", image: UIImage(named: "SMS"), selector: "", tag: 1)
let shareButton = self.makeButtonWithTitle(
" Share", image: UIImage(named: "upload"), selector: "", tag: 2)
let buttonsView = UIStackView(arrangedSubviews: [likeButton, commentButton, shareButton])
buttonsView.distribution = .FillEqually
let lookView = UIStackView(arrangedSubviews:[lookImageView, postView, buttonsView])
lookView.axis = .Vertical
lookView.distribution = .Fill
lookView.alignment = .Fill
self.streamView.addArrangedSubview(lookView)
}
self.scrollView.addSubview(self.streamView)
self.scrollView.frame = UIScreen.mainScreen().bounds
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.addSubview(self.scrollView)
}
}
So, what I would like this code to do is to give me a scrollable stack of nested stacks that covers the width of the screen (it's ok if the images are clipped, but I'd like them to cover the screen without distortion). What it actually gives me is a scrollable set of images where the width of the first image (seemingly) determines the width of the stack. I'm actually not sure if that's what's going on, but the top level UIStackView is not covering the width of the screen.
I think it has something to do with the UIScrollView has no intrinsic width, so the stack view inside it decides its own size. I say this because if I put the stack view directly in the parent view, it covers the display, but then as you might expect, there is no scrolling...
You need to set some constraints between the scrollview and the UIStackView it contains, at the moment you don't have any.
These three are enough to make the inner UIStackView the same size of the scrollview:
Horizontal constraint between the scrollview and the UIStackView, no space between them
Vertical constraint between the scrollview and the UIStackView, no space between them, this way you'll be able to scroll for the full height of the UIStackView
Same width for the scrollview and the UIStackView, this way the UIStackView will match the scrollview width
And this is the code:
//You already have these two lines:
//scrollView.addSubview(streamView)
//streamView.translatesAutoresizingMaskIntoConstraints = false;
//Add this one too:
scrollView.translatesAutoresizingMaskIntoConstraints = false;
scrollView.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat("V:|[innerView]|",
options: NSLayoutFormatOptions(rawValue:0),
metrics: nil,
views: ["innerView":streamView]))
scrollView.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat("H:|[innerView]|",
options: NSLayoutFormatOptions(rawValue:0),
metrics: nil,
views: ["innerView":streamView]))
scrollView.addConstraint(
NSLayoutConstraint(item: scrollView,
attribute: .Width,
relatedBy: .Equal,
toItem: streamView,
attribute: .Width,
multiplier: 1.0,
constant: 0))
Alternatively, you can extend UIView with a method that embed your view in a scrollview:
extension UIView {
func embedInScrollView()->UIView{
let cont=UIScrollView()
self.translatesAutoresizingMaskIntoConstraints = false;
cont.translatesAutoresizingMaskIntoConstraints = false;
cont.addSubview(self)
cont.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[innerView]|", options: NSLayoutFormatOptions(rawValue:0),metrics: nil, views: ["innerView":self]))
cont.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[innerView]|", options: NSLayoutFormatOptions(rawValue:0),metrics: nil, views: ["innerView":self]))
cont.addConstraint(NSLayoutConstraint(item: self, attribute: .Width, relatedBy: .Equal, toItem: cont, attribute: .Width, multiplier: 1.0, constant: 0))
return cont
}
}
And use it this way:
let scrollView = streamView.embedInScrollView()
Edit: Fixed last constraint in the first snippet.

Mimic UIStackView `fill proportionally` layout approach on iOS version prior to 9.0

The iOS 9.0 comes with UIStackView which makes it easier to layout views according to their content size. For example, to place 3 buttons in a row in accordance with their content width you can simply embed them in stack view, set axis horizontal and distribution - fill proportionally.
The question is how to achieve the same result in older iOS versions where stack view is not supported.
One solution I came up with is rough and doesn't look good. Again, You place 3 buttons in a row and pin them to nearest neighbors using constraints. After doing that you obviously will see content priority ambiguity error because auto layout system has no idea which button needs to grow / shrink before others.
Unfortunately, the titles are unknown before app's launch so you just might arbitrary pick a button. Let's say, I've decreased horizontal content hugging priority of middle button from standard 250 to 249. Now it'll grow before other two. Another problem is that left and right buttons strictly shrink to their content width without any nice looking paddings as in Stack View version.
It seems over complicated for a such simple thing. But the multiplier value of a constraint is read-only, so you'll have to go the hard way.
I would do it like this if I had to:
In IB: Create a UIView with constraints to fill horizontally the superView (for example)
In IB: Add your 3 buttons, add contraints to align them horizontally.
In code: programmatically create 1 NSConstraint between each UIButton and the UIView with attribute NSLayoutAttributeWidth and multiplier of 0.33.
Here you will get 3 buttons of the same width using 1/3 of the UIView width.
Observe the title of your buttons (use KVO or subclass UIButton).
When the title changes, calculate the size of your button content with something like :
CGSize stringsize = [myButton.title sizeWithAttributes:
#{NSFontAttributeName: [UIFont systemFontOfSize:14.0f]}];
Remove all programmatically created constraints.
Compare the calculated width (at step 4) of each button with the width of the UIView and determine a ratio for each button.
Re-create the constraints of step 3 in the same way but replacing the 0.33 by the ratios calculated at step 6 and add them to the UI elements.
Yes we can get the same results by using only constraints :)
source code
Imagine, I have three labels :
firstLabel with intrinsic content size equal to (62.5, 40)
secondLabel with intrinsic content size equal to (170.5, 40)
thirdLabel with intrinsic content size equal to (54, 40)
Strucuture
-- ParentView --
-- UIView -- (replace here the UIStackView)
-- Label 1 --
-- Label 2 --
-- Label 3 --
Constraints
for example the UIView has this constraints :
view.leading = superview.leading, view.trailing = superview.trailing, and it is centered vertically
UILabels constraints
SecondLabel.width equal to:
firstLabel.width * (secondLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth)
ThirdLabel.width equal to:
firstLabel.width * (thirdLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth)
I will back for more explanations
You may want to consider a backport of UIStackView, there are several open source projects. The benefit here is that eventually if you move to UIStackView you will have minimal code changes. I've used TZStackView and it has worked admirably.
Alternatively, a lighter weight solution would be to just replicate the logic for a proportional stack view layout.
Calculate total intrinsic content width of the views in your stack
Set the width of each view equal to the parent stack view multiplied by its proportion of the total intrinsic content width.
I've attached a rough example of a horizontal proportional stack view below, you can run it in a Swift Playground.
import UIKit
import XCPlayground
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.grayColor().CGColor
view.backgroundColor = UIColor.whiteColor()
XCPlaygroundPage.currentPage.liveView = view
class ProportionalStackView: UIView {
private var stackViewConstraints = [NSLayoutConstraint]()
var arrangedSubviews: [UIView] {
didSet {
addArrangedSubviews()
setNeedsUpdateConstraints()
}
}
init(arrangedSubviews: [UIView]) {
self.arrangedSubviews = arrangedSubviews
super.init(frame: CGRectZero)
addArrangedSubviews()
}
convenience init() {
self.init(arrangedSubviews: [])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConstraints() {
removeConstraints(stackViewConstraints)
var newConstraints = [NSLayoutConstraint]()
for (n, subview) in arrangedSubviews.enumerate() {
newConstraints += buildVerticalConstraintsForSubview(subview)
if n == 0 {
newConstraints += buildLeadingConstraintsForLeadingSubview(subview)
} else {
newConstraints += buildConstraintsBetweenSubviews(arrangedSubviews[n-1], subviewB: subview)
}
if n == arrangedSubviews.count - 1 {
newConstraints += buildTrailingConstraintsForTrailingSubview(subview)
}
}
// for proportional widths, need to determine contribution of each subview to total content width
let totalIntrinsicWidth = subviews.reduce(0) { $0 + $1.intrinsicContentSize().width }
for subview in arrangedSubviews {
let percentIntrinsicWidth = subview.intrinsicContentSize().width / totalIntrinsicWidth
newConstraints.append(NSLayoutConstraint(item: subview, attribute: .Width, relatedBy: .Equal, toItem: self, attribute: .Width, multiplier: percentIntrinsicWidth, constant: 0))
}
addConstraints(newConstraints)
stackViewConstraints = newConstraints
super.updateConstraints()
}
}
// Helper methods
extension ProportionalStackView {
private func addArrangedSubviews() {
for subview in arrangedSubviews {
if subview.superview != self {
subview.removeFromSuperview()
addSubview(subview)
}
}
}
private func buildVerticalConstraintsForSubview(subview: UIView) -> [NSLayoutConstraint] {
return NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
}
private func buildLeadingConstraintsForLeadingSubview(subview: UIView) -> [NSLayoutConstraint] {
return NSLayoutConstraint.constraintsWithVisualFormat("|-0-[subview]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
}
private func buildConstraintsBetweenSubviews(subviewA: UIView, subviewB: UIView) -> [NSLayoutConstraint] {
return NSLayoutConstraint.constraintsWithVisualFormat("[subviewA]-0-[subviewB]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subviewA": subviewA, "subviewB": subviewB])
}
private func buildTrailingConstraintsForTrailingSubview(subview: UIView) -> [NSLayoutConstraint] {
return NSLayoutConstraint.constraintsWithVisualFormat("[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
}
}
let labelA = UILabel()
labelA.text = "Foo"
let labelB = UILabel()
labelB.text = "FooBar"
let labelC = UILabel()
labelC.text = "FooBarBaz"
let stack = ProportionalStackView(arrangedSubviews: [labelA, labelB, labelC])
stack.translatesAutoresizingMaskIntoConstraints = false
labelA.translatesAutoresizingMaskIntoConstraints = false
labelB.translatesAutoresizingMaskIntoConstraints = false
labelC.translatesAutoresizingMaskIntoConstraints = false
labelA.backgroundColor = UIColor.orangeColor()
labelB.backgroundColor = UIColor.greenColor()
labelC.backgroundColor = UIColor.redColor()
view.addSubview(stack)
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-0-[stack]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["stack": stack]))
view.addConstraint(NSLayoutConstraint(item: stack, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: 0))
Use autolayout to your advantage. It can do all the heavy lifting for you.
Here is a UIViewController that lays out 3 UILabels, as you have in your screen shot, with no calculations. There are 3 UIView subviews that are used to give the labels "padding" and set the background color. Each of those UIViews has a UILabel subview that just shows the text and nothing else.
All of the layout is done with autolayout in viewDidLoad, which means no calculating ratios or frames and no KVO. Changing things like padding and compression/hugging priorities is a breeze. This also potentially avoids a dependency on an open source solution like TZStackView. This is just as easily setup in interface builder with absolutely no code needed.
class StackViewController: UIViewController {
private let leftView: UIView = {
let leftView = UIView()
leftView.translatesAutoresizingMaskIntoConstraints = false
leftView.backgroundColor = .blueColor()
return leftView
}()
private let leftLabel: UILabel = {
let leftLabel = UILabel()
leftLabel.translatesAutoresizingMaskIntoConstraints = false
leftLabel.textColor = .whiteColor()
leftLabel.text = "A medium title"
leftLabel.textAlignment = .Center
return leftLabel
}()
private let middleView: UIView = {
let middleView = UIView()
middleView.translatesAutoresizingMaskIntoConstraints = false
middleView.backgroundColor = .redColor()
return middleView
}()
private let middleLabel: UILabel = {
let middleLabel = UILabel()
middleLabel.translatesAutoresizingMaskIntoConstraints = false
middleLabel.textColor = .whiteColor()
middleLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
middleLabel.textAlignment = .Center
return middleLabel
}()
private let rightView: UIView = {
let rightView = UIView()
rightView.translatesAutoresizingMaskIntoConstraints = false
rightView.backgroundColor = .greenColor()
return rightView
}()
private let rightLabel: UILabel = {
let rightLabel = UILabel()
rightLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.textColor = .whiteColor()
rightLabel.text = "OK"
rightLabel.textAlignment = .Center
return rightLabel
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(leftView)
view.addSubview(middleView)
view.addSubview(rightView)
leftView.addSubview(leftLabel)
middleView.addSubview(middleLabel)
rightView.addSubview(rightLabel)
let views: [String : AnyObject] = [
"topLayoutGuide" : topLayoutGuide,
"leftView" : leftView,
"leftLabel" : leftLabel,
"middleView" : middleView,
"middleLabel" : middleLabel,
"rightView" : rightView,
"rightLabel" : rightLabel
]
// Horizontal padding for UILabels inside their respective UIViews
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[leftLabel]-(16)-|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[middleLabel]-(16)-|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[rightLabel]-(16)-|", options: [], metrics: nil, views: views))
// Vertical padding for UILabels inside their respective UIViews
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[leftLabel]-(6)-|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[middleLabel]-(6)-|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[rightLabel]-(6)-|", options: [], metrics: nil, views: views))
// Set the views' vertical position. The height can be determined from the label's intrinsic content size, so you only need to specify a y position to layout from. In this case, we specified the top of the screen.
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][leftView]", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][middleView]", options: [], metrics: nil, views: views))
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][rightView]", options: [], metrics: nil, views: views))
// Horizontal layout of views
NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[leftView][middleView][rightView]|", options: [], metrics: nil, views: views))
// Make sure the middle view is the view that expands to fill up the extra space
middleLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)
middleView.setContentHuggingPriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)
}
}
Resulting view:

Resources