I am using a Stack view to create a kind of table UI, I have 6 views in a StackView 0,2,4 are visible and 1,3,5 are hidden. When tapping one of the visible views I wish to "open" one of the views that are hidden.
I have this code that works great on iOS 10 but from some reason I can not understand it is not working well on iOS 9.
Note that if I load the views all open, the close animation will work but it won't open when setting the hidden property to false.
Here is my code -
EDIT
After some debugging looks like the view height constraint is nor recovering from the hiding, and it's frame is still height is 0.
import UIKit
class DeckView: UIView {
}
class ViewController: UIViewController {
var scrollView: UIScrollView!
var stackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[scrollView]|", options: .alignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[scrollView]|", options: .alignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 0
stackView.alignment = .center
stackView.distribution = .fillProportionally
stackView.axis = .vertical
scrollView.addSubview(stackView)
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[stackView]|", options: NSLayoutFormatOptions.alignAllCenterX, metrics: nil, views: ["stackView": stackView]))
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[stackView]|", options: NSLayoutFormatOptions.alignAllCenterX, metrics: nil, views: ["stackView": stackView]))
for i in 0 ..< 8 {
let view = DeckView()
view.tag = i
view.translatesAutoresizingMaskIntoConstraints = false
view.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
view.isUserInteractionEnabled = true
if i%2 == 0 {
view.backgroundColor = UIColor.magenta
let constriant = view.heightAnchor.constraint(equalToConstant:160)
constriant.priority = 999
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.openDeck(_:))))
view.addConstraint(constriant)
} else {
view.backgroundColor = UIColor.red
let constriant = view.heightAnchor.constraint(equalToConstant:160)
constriant.priority = 999
view.addConstraint(constriant)
view.isHidden = false
}
stackView.addArrangedSubview(view)
}
}
func openDeck(_ sender:UIGestureRecognizer) {
if let view = sender.view as? DeckView,
let childView = stackView.viewWithTag(view.tag + 1) {
UIView.animate(withDuration: 0.4, animations: {
childView.isHidden = !childView.isHidden
})
}
}
}
keep the view's height priority lower than 1000(go for 999).
Do not set setHidden:true if it is already hidden(This is UIStackView's bug)
If any one stumble on this issue.
I was able to solve this issue by removing the -
stackView.distribution = .fillProportionally
I am not sure why this happened but I found that Autolayout added a height constraint named 'UISV-fill-proportionally' with a constant of 0 and greater priority then my height constraint. removing the fillProportionally fixed the issue.
Related
I'm trying to programmatically create a 3x33 grid of UIButtons using nested UIStackViews inside a UIScrollView. The outer UIStackView has a vertical axis, while the inner UIStackViews have horizontal axes and distribution = .fillEqually.
If I don't have the enclosing UIScrollView, the buttons properly fill the width of the screen. With the scroll view, however, the buttons fill only half the width of the screen.
Here is a code example. In IB, ViewController's view property has been set to a UIScrollView instance.
class ViewController: UIViewController {
var stackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
title = "No Filling :("
stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 5
view.addSubview(stackView)
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[stackView]-|", options: .alignAllCenterX, metrics: nil, views: ["stackView": stackView!]))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[stackView]|", options: .alignAllCenterY, metrics: nil, views: ["stackView": stackView!]))
for _ in 1...33 {
let hView = UIStackView()
hView.translatesAutoresizingMaskIntoConstraints = false
hView.axis = .horizontal
hView.distribution = .fillEqually
hView.spacing = 5
stackView.addArrangedSubview(hView)
stackView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[hView]|", options: .alignAllCenterX, metrics: nil, views: ["hView": hView]))
for i in 1...3 {
let button = UIButton(type: .system)
button.setTitle("Button \(i)", for: .normal)
hView.addArrangedSubview(button)
button.layer.borderWidth = 1
button.layer.cornerRadius = 7
}
}
}
}
You want your scrollView's content width to be the width of the scrollView itself so that it fills the screen and scrolls vertically.
In order to do that, you need to establish the width of the scrollView's content. Right now, it is getting its width from the intrinsic size of the three buttons.
You need to add a constraint that explicitly makes the scrollView's content (the stackView + left and right offsets) equal to the width of the scrollView:
view.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "H:|-20-[stackView]-20-|", options: .alignAllCenterX,
metrics: nil, views: ["stackView": stackView!]))
view.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "V:|[stackView]|", options: .alignAllCenterY,
metrics: nil, views: ["stackView": stackView!]))
stackView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -40).isActive = true
Note: The adjustment constant -40 is the sum of the left and right offsets (20 each). In order for the scrollView to not scroll horizontally: stackView.width + 20 + 20 == scrollView.width, or equivalently, stackView.width == scrollView.width - 40 which is what the constraint specifies.
I was messing around with this example of a programatically created scroll view and stack view, and I decided to experiment and change the UIButtons to UILabels. I replaced the code inside of the for loop with this code:
let label = UILabel()
label.text = "Label"
stackView.addArrangedSubview(label)
When I re-ran the app, however, I found that the scroll view could no longer be scrolled. After debugging, I found that the stack view's frame had zero width and zero height, which I presume to be the source of the problem. I've been unable to figure out why the stack view has no width or height or how to make the scroll view scroll once again.
Here's the full view controller, with my modifications to compile it for Swift 4, use UILabel, and print the frame size:
import UIKit
class ViewController: UIViewController {
var scrollView: UIScrollView!
var stackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[scrollView]|", options: .alignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[scrollView]|", options: .alignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
scrollView.addSubview(stackView)
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[stackView]|", options: NSLayoutFormatOptions.alignAllCenterX, metrics: nil, views: ["stackView": stackView]))
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[stackView]", options: NSLayoutFormatOptions.alignAllCenterX, metrics: nil, views: ["stackView": stackView]))
for _ in 1 ..< 100 {
let label = UILabel()
label.text = "Label"
label.sizeToFit()
stackView.addArrangedSubview(label)
// let vw = UIButton(type: UIButtonType.system)
// vw.setTitle("Button", for: .normal)
// stackView.addArrangedSubview(vw)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentSize = CGSize(width: stackView.frame.width, height: stackView.frame.height)
print("stack view frame: \(stackView.frame)")
}
}
You should:
• Pin the bottom of the stackView to your scrollView's content bottom edge by updating your last VFL constraint like this:
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[stackView]|", options: NSLayoutFormatOptions.alignAllCenterX, metrics: nil, views: ["stackView": stackView]))
• Avoid mixing autolayout with 'manual' layout and remove that line:
scrollView.contentSize = CGSize(width: stackView.frame.width, height: stackView.frame.height)
I have Set scrollview and add constraints to scrollview. But it is showing blank screen. Please check below code.
override func loadView()
{
super.loadView()
scrollView = UIScrollView(frame:CGRectZero)
scrollView.backgroundColor = UIColor.whiteColor()
scrollView.sizeToFit()
self.view.addSubview(scrollView)
scrollView.backgroundColor = UIColor.blueColor()
contentView = UIView()
// contentView.setTranslatesAutoresizingMaskIntoConstraints(false)
contentView.backgroundColor = UIColor.redColor()
scrollView.addSubview(contentView)
var viewBindingsDictBoth = [String: AnyObject]()
viewBindingsDictBoth["scrollView"] = scrollView
viewBindingsDictBoth["contentView"] = contentView
viewBindingsDictBoth["mainView"] = self.view
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[scrollView]-0-|",options: [], metrics: nil, views:viewBindingsDictBoth))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[scrollView]-0-|",options: [], metrics: nil, views:viewBindingsDictBoth))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[contentView]|",options: [], metrics: nil, views:viewBindingsDictBoth))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[contentView]|",options: [], metrics: nil, views:viewBindingsDictBoth))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[contentView(==mainView)]",options: [], metrics: nil, views:viewBindingsDictBoth))
self.view.contentMode = UIViewContentMode.Redraw
}
Thanks in Advance..
The most important thing you are missing is
scrollView.translatesAutoresizingMaskIntoConstraints = false
and
contentView.translatesAutoresizingMaskIntoConstraints = false
I think that some of your constraints are not perfect but the biggest problem is that every view is using its autoresizing properties for positioning by default. Those will then collide with your constraints.
From UIView.translatesAutoresizingMaskIntoConstraints documentation:
Note that the autoresizing mask constraints fully specify the view’s size and position; therefore, you cannot add additional constraints to modify this size or position without introducing conflicts. If you want to use Auto Layout to dynamically calculate the size and position of your view, you must set this property to false, and then provide a non ambiguous, nonconflicting set of constraints for the view.
(emphasis mine)
I usually find it very unnecessary to override loadView unless I'm really trying to get the base view (self.view) to be of a different subclass of UIView. loadView is for initializing the value of self.view, which you have forgotten to do.
In your case, for legibility I would recommend moving view initialization to viewDidLoad.
override func viewDidLoad() {
// Do your init here
}
In loadView() method of your viewcontroller you need create a root view. Your root view right now is nil view = UIView()
You have a few mistakes:
override func loadView()
{
super.loadView()
scrollView = UIScrollView(frame:CGRectZero)
scrollView.backgroundColor = UIColor.whiteColor()
scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
// No need for sizeToFit with autoLayout
//scrollView.sizeToFit()
self.view.addSubview(scrollView)
scrollView.backgroundColor = UIColor.blueColor()
contentView = UIView()
//Always set TranslatesAutoresizingMaskIntoConstraints to false, on every view that users AutoLayout
contentView.setTranslatesAutoresizingMaskIntoConstraints(false)
contentView.backgroundColor = UIColor.redColor()
scrollView.addSubview(contentView)
var viewBindingsDictBoth = [String: AnyObject]()
viewBindingsDictBoth["scrollView"] = scrollView
viewBindingsDictBoth["contentView"] = contentView
viewBindingsDictBoth["mainView"] = self.view
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[scrollView]-0-|",options: [], metrics: nil, views:viewBindingsDictBoth))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[scrollView]-0-|",options: [], metrics: nil, views:viewBindingsDictBoth))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[contentView]|",options: [], metrics: nil, views:viewBindingsDictBoth))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[contentView]|",options: [], metrics: nil, views:viewBindingsDictBoth))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[contentView(==mainView)]",options: [], metrics: nil, views:viewBindingsDictBoth))
// self.view.contentMode = UIViewContentMode.Redraw
}
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.
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: