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

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.

Related

Adding a custom button to a view with constraints programatically

I want to programatically add a custom button to a view and also set the constraints for that button. Here is my code:
let button = SummaryButton(frame: CGRect(x: 0, y: 0, width: 36, height: 36))
button.setTitle("1", for: .normal)
button.addTarget(self, action: #selector(daysButtonPressed), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
daysView.addSubview(button)
NSLayoutConstraint(item: button, attribute: .leading, relatedBy: .equal, toItem: daysView, attribute: .leading, multiplier: 1.0, constant: 20).isActive = true
NSLayoutConstraint(item: button, attribute: .centerX, relatedBy: .equal, toItem: daysView, attribute: .centerX, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: button, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1.0, constant: 36).isActive = true
NSLayoutConstraint(item: button, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1.0, constant: 36).isActive = true
I don't see the button at all. If I remove the constraints I can see it, but the position is incorrect.
Is there an issue on how I am setting my constraints ? I want to added to the left side of the view.
Remove centerX constraint as you already have leading
NSLayoutConstraint(item: button, attribute: .centerX, relatedBy: .equal, toItem: daysView, attribute: .centerX, multiplier: 1.0, constant: 0).isActive = true
and add top say 50 pts from top of view
NSLayoutConstraint(item: button, attribute: .top, relatedBy: .equal, toItem: daysView, attribute: .top, multiplier: 1.0, constant: 50).isActive = true

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!

Auto set height of container view based on subviews

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

supplementary view remains briefly when viewcontroller is dismissed

I added a view to the top of my collection view, now when I dismiss the view remains for a split second. Does anyone have any idea to what might cause this?
let newView = UIView() newView.backgroundColor = UIColor.redColor()
newView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(newView)
let pinTop = NSLayoutConstraint(item: newView, attribute: .Top, relatedBy: .Equal, toItem:
self.topLayoutGuide, attribute: .Bottom, multiplier: 1.0, constant: 0)
let heightConstraint = newView.heightAnchor.constraintEqualToAnchor(nil, constant: 50)
let widthConstraint = NSLayoutConstraint(item: newView, attribute: .Width, relatedBy: .Equal, toItem: view, attribute: .Width, multiplier: 1.0, constant: 0)
self.topContentAdditionalInset = 50
NSLayoutConstraint.activateConstraints([pinTop, heightConstraint, widthConstraint])
If you are dismissing it with the viewController, try to add newView .removeFromSuperview() in viewWillDissapear and re-adding it in viewWillAppear
I was just missing a constraint, stupid me!
let centerX = NSLayoutConstraint(item: newView, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0)

Constraints are not expanding my View on orientation change

I created my view as so :
self.scrollView = UIScrollView()
self.scrollView.delegate = self
self.scrollView.contentSize = CGSizeMake(UIScreen.mainScreen().bounds.width, 1000)
containerView = UIView()
containerView.backgroundColor = UIColor.redColor()
scrollView.translatesAutoresizingMaskIntoConstraints = false
containerView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(containerView)
view.addSubview(scrollView)
And then I added these constraints :
// Constraint ScrollView
view.addConstraint(NSLayoutConstraint(item: scrollView, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1.0, constant: 0.0))
view.addConstraint(NSLayoutConstraint(item: scrollView, attribute: .Bottom, relatedBy: .Equal, toItem: view, attribute: .Bottom, multiplier: 1.0, constant: 0.0))
view.addConstraint(NSLayoutConstraint(item: scrollView, attribute: .Leading, relatedBy: .Equal, toItem: view, attribute: .Leading, multiplier: 1.0, constant: 0.0))
view.addConstraint(NSLayoutConstraint(item: scrollView, attribute: .Trailing, relatedBy: .Equal, toItem: view, attribute: .Trailing, multiplier: 1.0, constant: 0.0))
// Constraint ContainerView
scrollView.addConstraint(NSLayoutConstraint(item: containerView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 0.0))
scrollView.addConstraint(NSLayoutConstraint(item: containerView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 0.0))
scrollView.addConstraint(NSLayoutConstraint(item: containerView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Leading, multiplier: 1.0, constant: 0.0))
scrollView.addConstraint(NSLayoutConstraint(item: containerView, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Trailing, multiplier: 1.0, constant: 0.0))
And this :
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.frame = view.bounds
containerView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height)
}
But when I change the orientation, the width of the containerView remains the same. How can I ensure that the width expands to the width of the scrollView to it's new layout?
You should replace
scrollView.addConstraint(NSLayoutConstraint(item: containerView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 0.0))
scrollView.addConstraint(NSLayoutConstraint(item: containerView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Leading, multiplier: 1.0, constant: 0.0))
with
view.addConstraint(NSLayoutConstraint(item: containerView, attribute: .Height, relatedBy: .Equal, toItem: scrollView, attribute: .Height, multiplier: 1.0, constant: 0.0))
view.addConstraint(NSLayoutConstraint(item: containerView, attribute: .Width, relatedBy: .Equal, toItem: scrollView, attribute: .Width, multiplier: 1.0, constant: 0.0))
This uses the width and height of the scroll view instead of stretching the container view to the right side and bottom of the scroll view. It seems to work much better. I also added the constraint to a common ancestor, as per the docs but it also works by adding to scrollView, so take your choice whether you want to add all constraints to the view instead of the scrollView or not.
Further note
In iOS 8 and later you can simply activate your constraints instead of adding them to your view.
NSLayoutConstraint(item: containerView, attribute: .Top, relatedBy: .Equal, toItem: scrollView, attribute: .Top, multiplier: 1.0, constant: 0.0).active = true
In iOS 9, which I like even more you can use anchors, e.g.
scrollView.topAnchor.constraintEqualToAnchor(topLayoutGuide.bottomAnchor).active = true
You don't need to do anything in layoutSubviews. The translatesAutoresizingMaskIntoConstraints property should indeed be set to false NOT true.
you should also add these constraints
containerView.addConstraint(NSLayoutConstraint(item: containerView, attribute: .Top, relatedBy: .Equal, toItem: scrollView, attribute: .Top, multiplier: 1.0, constant: 0.0))
containerView.addConstraint(NSLayoutConstraint(item: containerView, attribute: .Bottom, relatedBy: .Equal, toItem: scrollView, attribute: .Bottom, multiplier: 1.0, constant: 0.0))
containerView.addConstraint(NSLayoutConstraint(item: containerView, attribute: .Leading, relatedBy: .Equal, toItem: scrollView, attribute: .Leading, multiplier: 1.0, constant: 0.0))
containerView.addConstraint(NSLayoutConstraint(item: containerView, attribute: .Trailing, relatedBy: .Equal, toItem: scrollView, attribute: .Trailing, multiplier: 1.0, constant: 0.0))

Resources