Programatically setting auto layout constraints for a UIView's subviews - ios

Below I have included some code for you to checkout. I am trying to take a custom UIView and add another custom subview to it. This subview should be constrained to the parent view in such a way it essentially just lays on top with the same dimensions and the parent just acts as a wrapper.
I have tried using the NSLayoutConstraint and failed miserable so far. The view never actually shows up. I have a left, right, bottom, and top constraint which should line up with the parent view.
The first ask I have is that someone please explain and or correct my logic when using the following method. The item param I figured is the actual view you want to set a constraint for (customViewChild). The attribute is to say that I want the left edge of my customViewChild to be used for this constraint. The relatedBy seems pretty straight forward although I could be wrong, and then finally the toItem points to self which is my CustomViewParent which also has a .left attribute to say that I want the left edge of my child and parent to line up. Is this logic flawed or am I doing something else wrong?
NSLayoutConstraint(item: customViewChild!,
attribute: .left,
relatedBy: .equal,
toItem: self,
attribute: .left,
multiplier: 1.0,
constant: 0.0)
I know the following example could very easily be done with IB, but I am trying to understand NSLayoutConstraint, so please provide answers regarding that. And lastly, if anyone could actually correct this code so I have a working example, that would be awesome.
class CustomViewParent: UIView {
var customViewChild: UIView?
override init(frame: CGRect) {
super.init(frame: frame)
setConstraints()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setConstraints()
}
func setConstraints() {
customViewChild = UIView()
addSubview(customViewChild!)
customViewChild?.translatesAutoresizingMaskIntoConstraints = false
let leftConstraint = NSLayoutConstraint(item: customViewChild!,
attribute: .left,
relatedBy: .equal,
toItem: self,
attribute: .left,
multiplier: 1.0,
constant: 0.0).isActive = true
let rightConstraint = NSLayoutConstraint(item: customViewChild!,
attribute: .right,
relatedBy: .equal,
toItem: self,
attribute: .right,
multiplier: 1.0,
constant: 0.0).isActive = true
let topConstraint = NSLayoutConstraint(item: customViewChild!,
attribute: .top,
relatedBy: .equal,
toItem: self,
attribute: .top,
multiplier: 1.0,
constant: 0.0).isActive = true
let bottomConstraint = NSLayoutConstraint(item: customViewChild!,
attribute: .bottom,
relatedBy: .equal,
toItem: self,
attribute: .bottom,
multiplier: 1.0,
constant: 0.0).isActive = true
customViewChild.addConstraint([leftConstraint, rightConstraint, topConstraint, bottomConstraint]);
}
}

You may find this a bit easier, and more readable...
func setConstraints() {
if customViewChild == nil {
customViewChild = UIView()
addSubview(customViewChild!)
customViewChild?.translatesAutoresizingMaskIntoConstraints = false
customViewChild?.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
customViewChild?.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
customViewChild?.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
customViewChild?.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
}

Three things:
You don't need to use addConstraint. Just set isActive to true for the constraints.
It doesn't make sense to both set isActive to true and to assign the result of that to a constant. Setting the isActive property doesn't return the NSLayoutConstraint. It returns ().
You should use .leading and .trailing instead of .left and .right.
With these changes, the following should work:
func setConstraints() {
customViewChild = UIView()
addSubview(customViewChild!)
customViewChild?.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: customViewChild!,
attribute: .leading,
relatedBy: .equal,
toItem: self,
attribute: .leading,
multiplier: 1.0,
constant: 0.0).isActive = true
NSLayoutConstraint(item: customViewChild!,
attribute: .trailing,
relatedBy: .equal,
toItem: self,
attribute: .trailing,
multiplier: 1.0,
constant: 0.0).isActive = true
NSLayoutConstraint(item: customViewChild!,
attribute: .top,
relatedBy: .equal,
toItem: self,
attribute: .top,
multiplier: 1.0,
constant: 0.0).isActive = true
NSLayoutConstraint(item: customViewChild!,
attribute: .bottom,
relatedBy: .equal,
toItem: self,
attribute: .bottom,
multiplier: 1.0,
constant: 0.0).isActive = true
}

Related

SideMenu with AutoLayout

I'am trying to make a side menu and i have some problems with setting it with auto layout.
I have a rootViewController that i add to it the leftMenuVC as childVC then i set the constraints.
class RootVC: UIViewController, NavigationBarDelegate {
var leftMenuVC: UIViewController?
var navigationBar = NavigationBar()
var isMenuCollapsed = true
override func viewDidLoad() {
leftMenuVC = leftVC()
addChildViewController(leftMenuVC!)
view.addSubview(leftMenuVC!.view)
leftMenuVC!.didMove(toParentViewController: self)
}
override func viewDidLayoutSubviews() {
if let v = leftMenuVC?.view {
v.translatesAutoresizingMaskIntoConstraints = false
v.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
v.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
v.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -140).isActive = true
v.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
}
func menuButtonClicked(){
}
}
So my question is how to change constraints to hide/show the menu with support of orientations
What I usually do when I want to hide a view outside the screen with constraints is:
1 Set all constraints so that the sideview is visible (in active state)
2 Keep in reference the constraint that stick your sideview on one side (here the left one)
leftAnchor = NSLayoutConstraint(item: v, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0)
view.addConstraint(leftAnchor)
view.addConstraint(NSLayoutConstraint(item: v, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: v, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: v, attribute: .width, relatedBy: .equal, toItem: view, attribute: .width, multiplier: 0.6, constant: 0))
3 Set one more constraint so that the view will be hidden. Usually it's something like that. Note that the priority is set to 999 to avoid constraint conflicts.
var hiddingConstraint = NSLayoutConstraint(item: v, attribute: .right, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0)
hiddingConstraint.priority = 999
view.addConstraint(hiddingConstraint)
4 Animate by activating or not your leftAnchor
UIView.animate(withDuration: 0.3) {
self.leftAnchor.active = false
self.view.layoutIfNeeded()
}
So you should end up with a code like this:
class RootVC: UIViewController, NavigationBarDelegate {
var leftMenuVC: UIViewController?
var navigationBar = NavigationBar()
var isMenuCollapsed = true {
didSet {
UIView.animate(withDuration: 0.3) {
self.leftAnchor?.isActive = self.isMenuCollapsed
self.view.layoutIfNeeded()
}
}
}
var leftAnchor : NSLayoutConstraint?
override func viewDidLoad() {
leftMenuVC = leftVC()
addChildViewController(leftMenuVC!)
view.addSubview(leftMenuVC!.view)
leftMenuVC!.didMove(toParentViewController: self)
}
override func viewDidLayoutSubviews() {
if let v = leftMenuVC?.view {
leftAnchor = NSLayoutConstraint(item: v, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0)
view.addConstraint(leftAnchor!)
view.addConstraint(NSLayoutConstraint(item: v, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: v, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: v, attribute: .width, relatedBy: .equal, toItem: view, attribute: .width, multiplier: 0.6, constant: 0))
var hiddingConstraint = NSLayoutConstraint(item: v, attribute: .right, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0)
hiddingConstraint.priority = 999
view.addConstraint(hiddingConstraint)
}
}
func menuButtonClicked(){
isMenuCollapsed = !isMenuCollapsed
}
}
PS: I won't put the constraints setting in viewDidLayoutSubviews, maybe in viewWillAppear, as you don't have to set them every time the device is being rotated. That's the purpose of constraints
Instead of writing this code by yourself, save yourself the trouble.
Here is MMDrawerController to your rescue. I am using it myself. It's super easy to implement and offers lots of customization options. Hope you find it useful. :-)

NSLayoutConstraints not working in reference to view

I want to create a container view based on the view size. The code speaks for itself but it doesn't work. I've tried using override viewWillAppear to no avail.
import UIKit
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UITextFieldDelegate {
let searchBarContainer: UIView = {
let sBarContainer = UIView()
sBarContainer.backgroundColor = UIColor.gray
return sBarContainer
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Search Bar"
collectionView?.backgroundColor = UIColor.white
view.addSubview(searchBarContainer)
NSLayoutConstraint(item: searchBarContainer, attribute: .width, relatedBy: .equal, toItem: view, attribute: .width, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: searchBarContainer, attribute: .height, relatedBy: .equal, toItem: view, attribute: .height, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: searchBarContainer, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0).isActive = true
}
}
Any suggestions? Thanks
Creating NSLayoutConstraints like that will just return the constraint. You either need to add them to the Parent view, or if you're iOS 8+ you can use the much superior Layout Anchors.
So, for example:
NSLayoutConstraint(item: searchBarContainer, attribute: .width, relatedBy: .equal, toItem: view, attribute: .width, multiplier: 1, constant: 0).isActive = true
becomes
label.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
edit: and to be clear, the anchor route means you don't have to deal with manually adding them after creating them.

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.

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

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