Auto set height of container view based on subviews - ios

I know this question has been asked numerous times, but I can't quite seem to get to the bottom of this problem.
Using Auto Layout, I would like to automatically set the height of my container UIView based on its subviews. I have looked at using sizeToFit and other various methods of summing up the height of my subviews, however from what I've read the height of my container height should be automatic when using Auto Layout because of the subviews "intrinsic" content size.
Below is a reduced case of what I'm experiencing. I would really appreciate any guidance!
Overview:
Create container UIView, pin to left and right sides of superview, no explicit height, align its centerY with its superview centerY
Create a 300 width by 100 height UIView, add it as a subview to container view, align its centerX with container view's centerX, pin to container view's top edge
Repeat step #2, except this time pin its top to #2's bottom edge
The expected height of the container view is 200, except its height is actually still 0 (therefor centerY alignment is off)
Code:
class ViewController: UIViewController {
let redView = RedView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(redView)
view.setNeedsUpdateConstraints()
}
}
class RedView: UIView {
let greenView = GreenView()
let blueView = BlueView()
init() {
super.init(frame: CGRect.zero)
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = UIColor.red()
addSubview(greenView)
addSubview(blueView)
setNeedsUpdateConstraints()
}
override func updateConstraints() {
super.updateConstraints()
NSLayoutConstraint(item: greenView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: blueView, attribute: .top, relatedBy: .equal, toItem: greenView, attribute: .bottom, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: self, attribute: .left, relatedBy: .equal, toItem: superview, attribute: .left, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: self, attribute: .right, relatedBy: .equal, toItem: superview, attribute: .right, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: superview, attribute: .centerY, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 200).isActive = true
}
}
class GreenView: UIView {
init() {
super.init(frame: CGRect.zero)
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = UIColor.green()
}
override func updateConstraints() {
super.updateConstraints()
NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 300).isActive = true
NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100).isActive = true
NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: superview, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
}
}
class BlueView: UIView {
init() {
super.init(frame: CGRect.zero)
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = UIColor.blue()
}
override func updateConstraints() {
super.updateConstraints()
NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 300).isActive = true
NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100).isActive = true
NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: superview, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
}
}

You need to pin blueView's bottom to redView's bottom, just add this line to redView's updateConstraints:
NSLayoutConstraint(item: blueView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0).active = true

Related

How do I create a robust ScrollView with both horizontal and vertical paging using autoLayout?

Situation
So I'm trying to make a UIScrollView that is basically a menu navigation bar that a user can navigate by swiping between menu items, where there are pages vertically laid out, and sub-pages horizontally laid out. Mockup:
The way I'm doing this is by creating a UIScrollView whose frame is the size of one UILabel, and I have isPagingEnabled set to true. I then tried adding a UIStackView, with each row indicating a page, and the contents of each row being a sub-page. I set the scrollView.contentSize to be the size of the UIStackView. The problem is that all my label's frames are zeros all through and the UIScrollView doesn't work.
:/
I really wanted to avoid getting help as I felt like I could do this by myself but I've spent two days on this and I've lost all hope.
Code
Here is the code where I add the labels. It's called upon the scrollview's superview's init (because the UIScrollView is in a custom UIView I call crossNavigation View).
private func addScrollViewLabels() {
//Get Max Number of Items in a Single Row
var maxRowCount = -1
for item in items {
if (item.contents.count > maxRowCount) {maxRowCount = item.contents.count}
}
self.rowsStackView.axis = .vertical
self.rowsStackView.distribution = .fillEqually
self.rowsStackView.alignment = .fill
self.rowsStackView.translatesAutoresizingMaskIntoConstraints = false
self.scrollView.addSubview(rowsStackView)
for i in 0 ..< items.count {
let row = items[i].contents
let rowView = UIView()
self.rowsStackView.addArrangedSubview(rowView)
var rowLabels : [UILabel] = []
//First Label
rowLabels.append(UILabel())
rowView.addSubview(rowLabels[0])
NSLayoutConstraint(item: rowLabels[0], attribute: .leading, relatedBy: .equal, toItem: rowView, attribute: .leading, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: rowLabels[0], attribute: .top, relatedBy: .equal, toItem: rowView, attribute: .top, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: rowLabels[0], attribute: .bottom, relatedBy: .equal, toItem: rowView, attribute: .bottom, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: rowLabels[0], attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: 0.55, constant: 0.0).isActive = true
//Middle Labels
for j in 1 ..< row.count {
rowLabels.append(UILabel())
rowView.addSubview(rowLabels[j])
//Stick it to it's left
NSLayoutConstraint(item: rowLabels[j], attribute: .leading, relatedBy: .equal, toItem: rowLabels[j-1], attribute: .trailing, multiplier: 1.0, constant: 0.0).isActive = true
//Stick top to rowView
NSLayoutConstraint(item: rowLabels[j], attribute: .top, relatedBy: .equal, toItem: rowView, attribute: .top, multiplier: 1.0, constant: 0.0).isActive = true
//Row Height is equal to rowView's Height
NSLayoutConstraint(item: rowLabels[j], attribute: .height, relatedBy: .equal, toItem: rowView, attribute: .height, multiplier: 1.0, constant: 0.0).isActive = true
//rowLabels[j].width = containerView.width * 0.55 (so other labels can peek around it)
NSLayoutConstraint(item: rowLabels[j], attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: 0.55, constant: 0.0).isActive = true
}
//lastLabel.trailing = rowView.trailing
NSLayoutConstraint(item: rowLabels[row.count-1], attribute: .trailing, relatedBy: .equal, toItem: rowView, attribute: .trailing, multiplier: 1.0, constant: 0.0).isActive = true
}
//Constraints for stack view:
//rowsStackView.height = scrollView.height * items.count
NSLayoutConstraint(item: rowsStackView, attribute: .height, relatedBy: .equal, toItem: scrollView, attribute: .height, multiplier: CGFloat(self.items.count), constant: 0.0).isActive = true
//rowsStackView.height = scrollView.height * items.count
NSLayoutConstraint(item: rowsStackView, attribute: .leading, relatedBy: .equal, toItem: containerView, attribute: .leading, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: rowsStackView, attribute: .top, relatedBy: .equal, toItem: scrollView, attribute: .top, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: rowsStackView, attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: 1.0, constant: 0.0).isActive = true
self.scrollView.contentSize = rowsStackView.frame.size
}
Take a look at this demo project I made based off your mock up.
It sets up the framework for everything you want to do.
Examine the view hierarchy and you'll understand where to configure the layout to make it exactly how you want.
The last step is to tweak the paging so you get the snap behavior you desire. There are many ways to do this but it is a bit involved.
Good Luck!

NSLayoutConstraint - can't set subview frame to parent view bounds

I have a testView UIView and subview named testViewSub. The testView is constrained by using NSLayoutConstraint. And i set subView frame to testView.bounds. But it doesn't work. Here is the code
let testView = UIView()
testView.backgroundColor = UIColor.red
self.view.addSubview(testView)
testView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: testView, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1.0, constant: 30).isActive = true
NSLayoutConstraint(item: testView, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1.0, constant: -30).isActive = true
NSLayoutConstraint(item: testView, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1.0, constant: 200).isActive = true
NSLayoutConstraint(item: testView, attribute: .height, relatedBy: .equal, toItem: self.view, attribute: .height, multiplier: 0.15, constant: 0).isActive = true
let testViewSub = UIView()
testViewSub.backgroundColor = UIColor.green
testViewSub.frame = testView.bounds
self.testView.addSubview(testViewSub)
testViewSub.translatesAutoresizingMaskIntoConstraints = false
But if i set testView's frame using CGRect. It works.
Where is the layout happening? I've run into issues before where the constraints don't take effect until the view appears, so relying on frames to be the correct size in viewDidLoad or viewWillAppear causes problems.
In this case, adding testViewSub in viewDidAppear worked for me, though I'm not sure it's the way I would recommend. Using constraints to lay it out, just as with testView, will also work from viewDidLoad or viewWillAppear:
// layout constraints however you want - in this case they are such that green view's frame = red view's bounds
let testViewSub = UIView()
testViewSub.backgroundColor = UIColor.green
self.testView.addSubview(testViewSub)
NSLayoutConstraint(item: testViewSub, attribute: .leading, relatedBy: .equal, toItem: testView, attribute: .leading, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: testViewSub, attribute: .trailing, relatedBy: .equal, toItem: testView, attribute: .trailing, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: testViewSub, attribute: .top, relatedBy: .equal, toItem: testView, attribute: .top, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: testViewSub, attribute: .height, relatedBy: .equal, toItem: testView, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
testViewSub.translatesAutoresizingMaskIntoConstraints = false
This will also deal with rotation better than simply setting the frame.

Adding subview with constraints makes view black

I am trying to add a overlay over for all my viewcontrollers by adding this code to my "BaseViewController". However it result in all ViewControllers turning black and behaving oddly.
override public func viewDidLoad()
{
super.viewDidLoad()
overlayView = UIView()
overlayView.backgroundColor = UIColor.redColor() //For testing
view.addSubviewWithMatchingConstraints(overlayView)
...
}
And in UIView extension:
func addSubviewWithMatchingConstraints(subView: UIView)
{
translatesAutoresizingMaskIntoConstraints = false
addSubview(subView)
addConstraint(NSLayoutConstraint(item: subView, attribute: .Width, relatedBy: .Equal, toItem: self, attribute: .Width, multiplier: 1, constant: 0))
addConstraint(NSLayoutConstraint(item: subView, attribute: .Height, relatedBy: .Equal, toItem: self, attribute: .Height, multiplier: 1, constant: 0))
addConstraint(NSLayoutConstraint(item: subView, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .CenterX, multiplier: 1.0, constant: 0))
addConstraint(NSLayoutConstraint(item: subView, attribute: .CenterY, relatedBy: .Equal, toItem: self, attribute: .CenterY, multiplier: 1.0, constant: 0))
}
The issue was that I set translatesAutoresizingMaskIntoConstraints on the parent rather than the child.
childView.translatesAutoresizingMaskIntoConstraints = false
fixed it

Auto layout completes animation in much less than the time provided

I want to animate a container width in 10 seconds (for example). I have two views and I assign size constraints to both of them, and call the empty init() method. The problem I'm having - the "animation" is over before you know it, even though I have 10 seconds in for the duration. What gives?
This is a reproducible example of the issue.
class ContainerView: UIView {}
class ChildView: UIView {}
class ViewController: UIViewController {
var containerView: ContainerView!
var childView: ChildView!
var widthConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
containerView = ContainerView()
containerView.backgroundColor = UIColor.blueColor()
childView = ChildView()
childView.backgroundColor = UIColor.redColor()
containerView.translatesAutoresizingMaskIntoConstraints = false
childView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(childView)
view.addSubview(containerView)
// Constraints
let containerWidthNumber: CGFloat = 200
let childWidthNumber: CGFloat = 100
let containerWidth = NSLayoutConstraint(item: containerView, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: containerWidthNumber)
let containerHeight = NSLayoutConstraint(item: containerView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: containerWidthNumber)
containerView.addConstraints([
containerWidth,
containerHeight,
NSLayoutConstraint(item: childView, attribute: .CenterX, relatedBy: .Equal, toItem: containerView, attribute: .CenterX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: childView, attribute: .Top, relatedBy: .Equal, toItem: containerView, attribute: .Top, multiplier: 1, constant: 0)
])
widthConstraint = NSLayoutConstraint(item: childView, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: childWidthNumber)
let childHeight = NSLayoutConstraint(item: childView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: childWidthNumber)
childView.addConstraints([
widthConstraint,
childHeight,
])
view.addConstraints([
NSLayoutConstraint(item: containerView, attribute: .CenterX, relatedBy: .Equal, toItem: view, attribute: .CenterX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: containerView, attribute: .CenterY, relatedBy: .Equal, toItem: view, attribute: .CenterY, multiplier: 1, constant: 0),
])
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
self.widthConstraint.constant = 50
UIView.animateWithDuration(10, animations: {
self.childView.layoutIfNeeded()
}, completion: { (success: Bool) in
if success {
print("good")
}
})
}
}

Have view centered and fill available height without going offscreen

I've got a situation where I would like a view to be centered in its superview, remain square, but fill as much height as possible without going off the edge, i.e., it should look at the available vertical and horizontal space, choosing the smallest between the 2.
There are 2 other views, one below and one above, that will both be either a button or label. The bottom/top of these views should be attached to the top/bottom of the central view. I can get this to work, to an extent, but I'll explain my issue below, and what I've got so far:
Top label has:
.Top >= TopLayoutGuide.Bottom
.Top = TopLayoutGuide.Bottom (priority 250)
.Right = CentralView.Right
Central view has:
Center X and Y = Superview Center X and Y
.Height <= Superview.Width * 0.9
.Width = self.Height
.Top = TopLabel.Bottom
Bottom button has:
.Right = CentralView.Right
.Top = CentralView.Bottom
.Bottom <= (BottomLayoutGuide.Top - 16)
Running this seems fine, and produces the desired results:
However, if I make the view an instance of my custom class and add a UIButton subview, it all goes wrong. In this class I perform:
self.topLeftButton = CustomButtonClass()
self.topLeftButton.setTranslatesAutoresizingMaskIntoConstraints(false)
self.addSubview(self.topLeftButton)
self.addConstraints([
NSLayoutConstraint(item: self.topLeftButton, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.topLeftButton, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.topLeftButton, attribute: .Height, relatedBy: .Equal, toItem: self, attribute: .Height, multiplier: 0.5, constant: 0),
NSLayoutConstraint(item: self.topLeftButton, attribute: .Width, relatedBy: .Equal, toItem: self.topLeftButton, attribute: .Width, multiplier: 1, constant: 0)
])
Using this code the view collapses down to the following:
I can't figure out why this is. I've made a few small tweaks here and there, but not managed to get it to work as desired. If I add the same button in IB the view wants to collapse again, and it's as if the button will not grow in height.
In real life I wouldn't subclass UIButton, but have done in my answer, as that is what the question indicated. UIButton works best through composition. So maybe better to create a UIButton, then modify its properties.
class FooViewController: UIViewController {
override func viewDidLoad() {
var view = CustomView()
view.backgroundColor = UIColor.darkGrayColor()
var label = UILabel()
label.text = "Label"
var button = UIButton.buttonWithType(.System) as UIButton
button.setTitle("Button", forState: .Normal)
view.setTranslatesAutoresizingMaskIntoConstraints(false)
label.setTranslatesAutoresizingMaskIntoConstraints(false)
button.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(view)
self.view.addSubview(label)
self.view.addSubview(button)
// The width should be as big as possible...
var maxWidthConstraint = NSLayoutConstraint(item: view, attribute: .Width, relatedBy: .Equal, toItem: view.superview, attribute: .Width, multiplier: 1, constant: 0);
// ... but not at the expense of other constraints
maxWidthConstraint.priority = 1
self.view.addConstraints([
// Max width, if possible
maxWidthConstraint,
// Width and height can't be bigger than the container
NSLayoutConstraint(item: view, attribute: .Width, relatedBy: .LessThanOrEqual, toItem: view.superview, attribute: .Width, multiplier: 1, constant: 0),
NSLayoutConstraint(item: view, attribute: .Width, relatedBy: .LessThanOrEqual, toItem: view.superview, attribute: .Height, multiplier: 1, constant: 0),
// Width and height are equal
NSLayoutConstraint(item: view, attribute: .Height, relatedBy: .Equal, toItem: view, attribute: .Width, multiplier: 1, constant: 0),
// View is centered
NSLayoutConstraint(item: view, attribute: .CenterX, relatedBy: .Equal, toItem: view.superview, attribute: .CenterX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: view, attribute: .CenterY, relatedBy: .Equal, toItem: view.superview, attribute: .CenterY, multiplier: 1, constant: 0),
])
// Label above view
self.view.addConstraints([
NSLayoutConstraint(item: label, attribute: .Top, relatedBy: .GreaterThanOrEqual, toItem: label.superview, attribute: .Top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: label, attribute: .Bottom, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: label, attribute: .Right, relatedBy: .LessThanOrEqual, toItem: view, attribute: .Right, multiplier: 1, constant: 0),
])
// Button below view
self.view.addConstraints([
NSLayoutConstraint(item: button, attribute: .Bottom, relatedBy: .LessThanOrEqual, toItem: button.superview, attribute: .Bottom, multiplier: 1, constant: 0),
NSLayoutConstraint(item: button, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Bottom, multiplier: 1, constant: 0),
NSLayoutConstraint(item: button, attribute: .Right, relatedBy: .LessThanOrEqual, toItem: view, attribute: .Right, multiplier: 1, constant: 0),
])
}
}
class CustomView: UIView {
required init(coder: NSCoder) {
super.init(coder: coder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override init() {
super.init()
var button = CustomButton()
button.setTitle("Custom Button", forState: UIControlState.Normal)
button.setTranslatesAutoresizingMaskIntoConstraints(false)
self.addSubview(button)
// Custom button in the top left
self.addConstraints([
NSLayoutConstraint(item: button, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: button, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: 0),
])
}
}
class CustomButton: UIButton {
required init(coder: NSCoder) {
super.init(coder: coder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override init() {
super.init()
self.backgroundColor = UIColor.greenColor()
}
}

Resources