Resize superview with autolayout depending on largest subview - swift

What is the best practice to resize superview with autolayout if we have inner NSView columns with dynamic heights?
For example. If we have two column layout, where left column height is bigger than right column, the superview height should be as right column height. Than, if we change right column height to be bigger than left column height, superview height should change to height of right column. How to accomplish this?
I made sample project to test this:
Initially we have layout with two columns, where .Bottom constraint of left NSView is attached to bottom of superview.
If we press Make Right Bigger button, I make height of right NSView bigger than left one.
So I want here superview to change height depending on bigger column (right column). Is there a good practice to do so?
Code:
import Cocoa
class ViewController: NSViewController {
let leftView = NSView()
let rightView = NSView()
let button = NSButton()
var rightViewHeightConstraint: NSLayoutConstraint?
override func loadView() {
self.view = TestView()
}
override func viewDidLoad() {
super.viewDidLoad()
leftView.backgroundColor = NSColor.redColor()
rightView.backgroundColor = NSColor.orangeColor()
layoutLeft(view, insertView: leftView)
layoutRight(view, insertView: rightView)
button.title = "Make Right Bigger"
button.target = self
button.action = "makeBigger:"
ViewControllerLayout.layoutBotton(view, insertView: button, bottom: -20)
}
func makeBigger(sender: AnyObject) {
rightViewHeightConstraint?.animator().constant = 150.0
}
func layoutLeft(containerView: NSView, insertView: NSView) {
insertView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(insertView)
let c1 = NSLayoutConstraint(item: insertView, attribute: .Left, relatedBy: .Equal, toItem: containerView, attribute: .Left, multiplier: 1.0, constant: 0.0)
let c2 = NSLayoutConstraint(item: insertView, attribute: .Width, relatedBy: .Equal, toItem: containerView, attribute: .Width, multiplier: 0.5, constant: 0.0)
let c3 = NSLayoutConstraint(item: insertView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: 100.0)
let c4 = NSLayoutConstraint(item: insertView, attribute: .Top, relatedBy: .Equal, toItem: containerView, attribute: .Top, multiplier: 1.0, constant: 0.0)
let c5 = NSLayoutConstraint(item: insertView, attribute: .Bottom, relatedBy: .Equal, toItem: containerView, attribute: .Bottom, multiplier: 1.0, constant: -60.0)
containerView.addConstraint(c1)
containerView.addConstraint(c2)
containerView.addConstraint(c3)
containerView.addConstraint(c4)
containerView.addConstraint(c5)
}
func layoutRight(containerView: NSView, insertView: NSView) {
insertView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(insertView)
let c1 = NSLayoutConstraint(item: insertView, attribute: .Right, relatedBy: .Equal, toItem: containerView, attribute: .Right, multiplier: 1.0, constant: 0.0)
let c2 = NSLayoutConstraint(item: insertView, attribute: .Width, relatedBy: .Equal, toItem: containerView, attribute: .Width, multiplier: 0.5, constant: 0.0)
let c3 = NSLayoutConstraint(item: insertView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: 50.0)
let c4 = NSLayoutConstraint(item: insertView, attribute: .Top, relatedBy: .Equal, toItem: containerView, attribute: .Top, multiplier: 1.0, constant: 0.0)
let c5 = NSLayoutConstraint(item: insertView, attribute: .Bottom, relatedBy: .Equal, toItem: containerView, attribute: .Bottom, multiplier: 1.0, constant: -60.0)
containerView.addConstraint(c1)
containerView.addConstraint(c2)
containerView.addConstraint(c3)
containerView.addConstraint(c4)
// containerView.addConstraint(c5) // Cant add .Bottom constraint here, because of different column sizes.
rightViewHeightConstraint = c3
}
}
struct ViewControllerLayout {
static func layoutBotton(containerView: NSView, insertView: NSView, bottom: Double) {
insertView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(insertView)
containerView.addConstraint(NSLayoutConstraint(item: insertView, attribute: .CenterX, relatedBy: .Equal, toItem: containerView, attribute: .CenterX, multiplier: 1.0, constant: 0.0))
containerView.addConstraint(NSLayoutConstraint(item: insertView, attribute: .Bottom, relatedBy: .Equal, toItem: containerView, attribute: .Bottom, multiplier: 1.0, constant: CGFloat(bottom)))
}
}
Download test project: GitHub

Managed to accomplish this with constraints only. Just added the container view for columns and set its height as GreaterThanOrEqual to left column and right column.
view.addConstraint(NSLayoutConstraint(item: containerView, attribute: .Height, relatedBy: .GreaterThanOrEqual, toItem: leftView, attribute: .Height, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: containerView, attribute: .Height, relatedBy: .GreaterThanOrEqual, toItem: rightView, attribute: .Height, multiplier: 1, constant: 0))

First create a constraint on superview with height
let heightConstraint = NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: greaterHeightAmongTwoColumns)
As discussed in the comment, you can post a notification and then let the super view know the updated frame. Super view would check if the updated height of any of the two columns is greater than its current height and update its heightConstraint declared above and then call
[self layoutIfNeeded]
on the superview's superview so that it is laid out with the new frame.

Related

A layout issue (NSLayoutConstraint) in an iOS app

In an iOS app I have an autolayout issue.
The 2 following screenshots show the problem.
The switch (UISwitch object) on the right is displaced horizontally, when it should be fixed. Can anyone see what is happening?
It is true that the string on the left is changing length, but I think (according to the way I have set the constraints up) the font should be resized or the string split in 2 lines; but not the switch displaced.
Here is the relevant swift code:
import UIKit
class My_ViewController: UIViewController {
let xPanel = UILabel(), yPanel = UILabel(),
khToggle = UISwitch(), khLabel = UILabel()
....
override func viewDidLoad() {
super.viewDidLoad()
layOutUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
.....
toggleKeepHide(khToggle)
}
func layOutUI() {
for component in [xPanel,yPanel,khLabel,khToggle] {
component.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(component)
}
...........
khLabel.numberOfLines = 0
khLabel.adjustsFontSizeToFitWidth = true
khToggle.addTarget(self,
action: #selector(toggleKeepHide(_:)),
for: .valueChanged)
view.addConstraints([
.........
NSLayoutConstraint(item: khToggle, attribute: .right, relatedBy: .equal,
toItem: view, attribute: .right, multiplier: 1.0, constant: -30.0),
NSLayoutConstraint(item: khToggle, attribute: .top, relatedBy: .equal,
toItem: yPanel, attribute: .bottom, multiplier: 1.0,
constant: 50.0),
NSLayoutConstraint(item: khLabel, attribute: .right, relatedBy: .equal,
toItem: khToggle, attribute: .left, multiplier: 1.0, constant: -23.0),
NSLayoutConstraint(item: khLabel, attribute: .centerY, relatedBy: .equal,
toItem: khToggle, attribute: .centerY, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: khLabel, attribute: .left, relatedBy: .equal,
toItem: view, attribute: .left, multiplier: 1.0, constant: 30.0)])
}
#objc func toggleKeepHide(_ sender: UISwitch) {
if sender.isOn {khLabel.text = "Hide this object from the wyxoug list."}
else {khLabel.text = "Keep this object in the wyxoug list."}
}
}
You haven't provided sufficient information to reproduce the problem. Here's a reduction of your code, in my view controller's viewDidLoad (I eliminated everything but the label and the switch, fixed your left and right (you should never use those), and changed the alignment between the two views to top instead of center):
khToggle = UISwitch()
khToggle.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(khToggle)
khLabel = UILabel()
khLabel.text = String(repeating: "word ", count: 40)
khLabel.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(khLabel)
// Do any additional setup after loading the view.
khLabel.numberOfLines = 0
view.addConstraints([
NSLayoutConstraint(item: khToggle!, attribute: .trailing, relatedBy: .equal,
toItem: view, attribute: .trailing, multiplier: 1.0, constant: -30.0),
NSLayoutConstraint(item: khToggle!, attribute: .top, relatedBy: .equal,
toItem: view, attribute: .top, multiplier: 1.0,
constant: 50.0),
NSLayoutConstraint(item: khLabel!, attribute: .trailing, relatedBy: .equal,
toItem: khToggle, attribute: .leading, multiplier: 1.0, constant: -23.0),
NSLayoutConstraint(item: khLabel!, attribute: .top, relatedBy: .equal,
toItem: khToggle, attribute: .top, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: khLabel!, attribute: .leading, relatedBy: .equal,
toItem: view, attribute: .leading, multiplier: 1.0, constant: 30.0)])
The result displays fine; there are no constraint conflicts or ambiguities, and it looks as one would expect:
The horizontal constraints for the label and the toggle are competing against each other.
(If you step in to Xcode's visual debugger, you'll find that there is a warning: the UISwitch instance has an ambiguous width and horizontal position.)
Solution
You have provided absolute values to AutoLayout, and it cannot resolve the requirements. To fix this, introduce some flexibility in to the requirements by setting the compression resistance of the label to a lower value:
khLabel.setCompressionResistance(.defaultLow, for: .horizontal)

Changing position of UIProgressView?

I added a progress bar to my screen. I want it to be centered horizontally in my container, but I want to move it to the bottom of my screen. How do I edit the third line to change its position?
func addControls() {
progressView = UIProgressView(progressViewStyle: UIProgressViewStyle.Default)
progressView?.center = self.view.center
view.addSubview(progressView!)
}
You could use NSLayoutConstraints and do something like this. The 3rd line being where you put the progress bar on top or beneath the other object.
var otherObject = UIView()
self.view.addSubview(progressView)
let topConstraint = NSLayoutConstraint(item: progressView, attribute: .topMargin, relatedBy: .equal, toItem: otherObject, attribute: .bottomMargin, multiplier: 1, constant: 0)
let leftConstraint = NSLayoutConstraint(item: progressView, attribute: .leftMargin, relatedBy: .equal, toItem: self.view, attribute: .leftMargin, multiplier: 1, constant: 0)
let rightConstraint = NSLayoutConstraint(item: progressView, attribute: .rightMargin, relatedBy: .equal, toItem: self.view, attribute: .rightMargin, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: progressView, attribute: .bottomMargin, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 50)
self.view.addConstraints([topConstraint, leftConstraint, rightConstraint, bottomConstraint])
That's really simple.
If you want to add a subview above another one, use this function:
func insertSubview(_ view: UIView, aboveSubview siblingSubview: UIView)
To add a subview below another one, use this function:
func insertSubview(_ view: UIView, belowSubview siblingSubview: UIView)

Formatting UICollectionViewCell inner items with constraints

I have a UICollectionViewCell and I want to be able to format the items inside of it more freely. That means - I want to be able to set constraints relative to the cell itself.
This is my cell:
And this is my code:
//image View Constraints
let productImageTopConstraint = NSLayoutConstraint(item: ProductImageView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 1) // constant was 10
let productImageBottomConstraint = NSLayoutConstraint(item: ProductImageView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: -30)
let productImageLeadingConstraint = NSLayoutConstraint(item: ProductImageView, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 10)
let productImageTrailingConstraint = NSLayoutConstraint(item: ProductImageView, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: -10)
//product name field constraints
let productNameTopConstraint = NSLayoutConstraint(item: ProductName, attribute: .top, relatedBy: .equal, toItem: ProductImageView, attribute: .bottom, multiplier: 1, constant: 10)
let productNameBottomConstraint = NSLayoutConstraint(item: ProductName, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0)
let productNameLeadingConstraint = NSLayoutConstraint(item: ProductName, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 10)
let productNameTrailingConstraint = NSLayoutConstraint(item: ProductName, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0)
What I want:
The ImageView to be closer to the cell's top edge
The product name label to be in the center
To be able to add another label between the product name label and the cell's bottom edge
How do I do that ? How do I take into account the cell's edges ?
You can use Layout Anchors to achieve this.
first get the margins of your UICollectionViewCell contentView like below
let margins = self.contentView.layoutMarginsGuide
1. The ImageView to be closer to the cell's top edge
Add following constraints relative to cell's content view margins like below
//Add top, left, right constraint relative to cell content view like below
yourImageView.topAnchor.constraint(equalTo: margins.topAnchor,constant:5).isActive = true
yourImageView.leftAnchor.constraint(equalTo: margins.leftAnchor,constant:5).isActive = true
yourImageView.rightAnchor.constraint(equalTo: margins.rightAnchor,constant:5).isActive = true
2. The product name label to be in the center
//To center align
yourLabel.centerXAnchor.constraint(equalTo: margins.centerXAnchor).isActive = true
//Now set your label top anchor to display it below your image view
yourLabel.topAnchor.constraint(equalTo: yourImageView.bottomAnchor,constant:5).isActive = true
3. To be able to add another label between the product name label and the cell's bottom edge
anotherLabel.topAnchor.constraint(equalTo: yourLabel.bottomAnchor,constant:5).isActive = true
anotherLabel.bottomAnchor.constraint(equalTo: margins.bottomAnchor).isActive = true
//To center align
anotherLabel.centerXAnchor.constraint(equalTo: margins.centerXAnchor).isActive = true
UPDATE
Make sure you have added your control as subview and set translatesAutoresizingMaskIntoConstraints = false
The order in which you should add constraint programmatically is as follows
Initialise your controls like let yourLabel = UILabel()
Set yourLabel.translatesAutoresizingMaskIntoConstraints = false
Add your label as subview self.addSubView(yourLabel)
Add constraints to your label
1
let productImageTopConstraint = NSLayoutConstraint(item: ProductImageView,
attribute: .top,
relatedBy: .equal,
toItem: self,
attribute: .top,
multiplier: 1,
constant: 1) <- make it 0 so it will be pinned to top edge
2 Set ProductName.textAlignment = .center
3 a) Remove productNameBottomConstraint so ProductName's height will be calculated from text and font automatically
b) Add another label with layout
let productName2TopConstraint = NSLayoutConstraint(item: ProductName2, attribute: .top, relatedBy: .equal, toItem: ProductName, attribute: .bottom, multiplier: 1, constant: 10)
let productName2BottomConstraint = NSLayoutConstraint(item: ProductName2, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0)
let productName2LeadingConstraint = NSLayoutConstraint(item: ProductName2, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 10)
let productName2TrailingConstraint = NSLayoutConstraint(item: ProductName2, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0)

UIScrollView with NSLayoutConstraint

UIScrollView is not scrolling the subviews but it does show a scroll-bar.
Here is what I am trying to do
class SetupViewController: UIViewController {
let scrollView = UIScrollView()
let pageLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.setupViews()
self.setupConstraints()
self.setText()
}
func setupViews() {
self.scrollView.backgroundColor = .red
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
// Page Label
self.pageLabel.font = UIFontLocalized(englishFontSize: 22, arabicFontSize: 22)
self.pageLabel.textAlignment = .center
self.pageLabel.translatesAutoresizingMaskIntoConstraints = false
}
func setupConstraints() {
// Add To Sub Views
self.view.addSubview(self.pageLabel)
self.view.addSubview(self.scrollView)
// Page Label
NSLayoutConstraint(item: self.pageLabel, attribute: .top, relatedBy: .equal, toItem: self.topLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: 30.0).isActive = true
NSLayoutConstraint(item: self.pageLabel, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1.0, constant: 20.0).isActive = true
NSLayoutConstraint(item: self.pageLabel, attribute: .width, relatedBy: .equal, toItem: self.view, attribute: .width, multiplier: 1.0, constant: -40.0).isActive = true
NSLayoutConstraint(item: self.pageLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 36.0).isActive = true
// Scroll View
NSLayoutConstraint(item: self.scrollView, attribute: .top, relatedBy: .equal, toItem: self.line1, attribute: .bottom, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: self.scrollView, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: self.scrollView, attribute: .width, relatedBy: .equal, toItem: self.view, attribute: .width, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: self.scrollView, attribute: .height, relatedBy: .equal, toItem: self.view, attribute: .height, multiplier: 1.0, constant: -100.0).isActive = true
self.scrollView.contentSize.height = 2000
// NSLayoutConstraint for rest of elements are removed in this example code.
}
}
I can confirm UIScrollView is added in the view because I checked by giving background red color to scrollview and it does show the background red color in correct position. My issue is the subviews does not move only the scroll bar is moving.
What could be the possible issue here?
Note: NSLayoutConstraint exist for all UIKit elements, I have not added it in the code.
I got this issue sorted our. As pointed out in the comment, the issue was related to superview in constraint which was set to self.view changing it to self.scrollView solved the issue.

Auto layout issue in swift code

want to create UI such that constraint should be such that the space between the two button and the distance from the side edge should be same.
I am using the following code
func addConstraintsToBottomButtons()
{
let viewFrame = self.view.frame
let availableWidth:CGFloat = viewFrame.width - 60
let buttonDistance:CGFloat = availableWidth/3
let buttonWidth:CGFloat = 30.0
let buttonHeight:CGFloat = 30.0
var BtnOne = UIButton()
BtnOne.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(BtnOne)
BtnOne.backgroundColor = UIColor.redColor()
var btnTwo = UIButton()
btnTwo.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(btnTwo)
btnTwo.backgroundColor = UIColor.yellowColor()
//Views to add constraints to
//Metrics for Visual Format string
// Button One Constraint..
self.view .addConstraint(NSLayoutConstraint(item: BtnOne, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: bottonView, attribute: NSLayoutAttribute.Leading, multiplier: 1, constant: buttonDistance))
self.view .addConstraint(NSLayoutConstraint(item: BtnOne, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: bottonView, attribute: NSLayoutAttribute.CenterY, multiplier: 1, constant: 0))
self.view .addConstraint(NSLayoutConstraint(item: BtnOne, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 0, constant: buttonWidth))
self.view .addConstraint(NSLayoutConstraint(item: BtnOne, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 0, constant: buttonHeight))
// Button Two Constraint...
self.view .addConstraint(NSLayoutConstraint(item: btnTwo, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: bottonView, attribute: NSLayoutAttribute.Trailing, multiplier: 1, constant: -(buttonDistance)))
self.view .addConstraint(NSLayoutConstraint(item: btnTwo, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: bottonView, attribute: NSLayoutAttribute.CenterY, multiplier: 1, constant: 0))
self.view .addConstraint(NSLayoutConstraint(item: btnTwo, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 0, constant: buttonWidth))
self.view .addConstraint(NSLayoutConstraint(item: btnTwo, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 0, constant: buttonHeight))
}
let me suggest a workaround that will do just the thing you want:
Create three views in storyboard / pull them out of the same place where you get buttons - and use them as cushions between the edges of your screen and your buttons: anchor to edges, equal width, and lower horizontal compression than the actual buttons and it should behave as desired.