How to set variations of a NSLayoutConstraint constant in code? - ios

I have drag and dropped the constraint in my code and can access it.
#IBOutlet weak var betweenTextTerms: NSLayoutConstraint!
Howeever it seems all I can do is set the constant.
But how do I set the value for variations such as Compact Regular in code?

To do this programmatically you override traitCollectionDidChange: method in your view controller.
You can then look at self.traitCollection.horizontalSizeClass and self.traitCollection.verticalSizeClass to decide what to do. Use the reference that you have created to the layout constraint to set the constant accordingly.
After all of your layout constraints are set, call updateConstraints on your view to trigger a layout pass.
For example:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
let hCompact = (self.traitCollection.horizontalSizeClass == .compact)
let vRegular = (self.traitCollection.verticalSizeClass == .regular)
if hCompact && vRegular {
self.betweenTextTerms.constant = 45
}
self.updateViewConstraints()
}
For simple cases, it is much more convenient to do this in the Storyboard!

Related

Change Swift constraint on if statement

Here is my tableview row/cell:
there are constraints set in place - the imageview is below the label and the button is below the imageview.
here is my code:
if(row == 1) {
imageview.hidden = false
} else {
imageview.hidden = true
//how can i change the button constraint from below imageview to below label?
Adding and removing constraints is really bad example for that. I'll make your UI more complex.
Best way of solving these auto-layout problems is adding two constraints. One from imageView to button and second from imageView to label.
Now after setting these constraints, you need to set their priority levels. So, let's say button will be below the imageView first. In this case, you need to set imageView to button constraint's priority to something like 750 or UILayoutPriorityDefaultHigh and label to button constraint's priority to 250 or UILayoutPriorityDefaultLow.
Let's start creating a custom UITableViewCell
class YourTableViewCell: UITableViewCell {
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var label: UILabel!
#IBOutlet weak var button: UIButton!
#IBOutlet weak var buttonToLabelConstraint: NSLayoutConstraint!
#IBOutlet weak var buttonToImageViewConstraint: NSLayoutConstraint!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func shouldHideImageView(hidden: Bool) {
if(hidden == false) {
buttonToLabelConstraint.priority = UILayoutPriorityDefaultLow
buttonToImageViewConstraint.priority = UILayoutPriorityDefaultHigh
imageView.hidden = true
} else {
buttonToLabelConstraint.priority = UILayoutPriorityDefaultHigh
buttonToImageViewConstraint.priority = UILayoutPriorityDefaultLow
imageView.hidden = false
}
self.contentView.layoutIfNeeded()
}
}
After that, in your class where tableView is placed implement a logic like that:
if(row == 1) {
cell.shouldHideImageView(true)
} else {
cell.shouldHideImageView(false)
}
You should be all set.
You can try using a StackView, when you tell something to be hidden, the imageView the stack view will adjust the StackView as if the imageView was never a part of the view and it is an easy work around to not have to worry about constraints.
You can create IBOutlet on constraint and then just simply change the value like this:
buttonConstraint.constant = newValue
But i suggest you create for this a tableView. In this case you code and logic, i think, will be more accurate.
you could to this instead of hiding.
Make an outlet from the heights constraint of the imageview, call it constraint for now.
Set constraint.constant = 0 // effectively same as hiding.
Set constraint.constant = NON_ZERO_VALUE // effectively same as show.
hope it helps!
I see a couple of options. The first is a little easier to implement but a little less flexible if you decide to change your layout later.
Make the button's constraint to be below the label. Keep a reference to this constraint (you can connect it to your code via storyboard just like you do with the button itself, if you're using storyboard). When the imageView is visible, set myConstraint.constant += myImageView.frame.height. When the imageView is hidden, set myConstraint.constant -= myImageView.frame.height. Afterwards, call view.setNeedsLayout to update your constraints.
Make two constraints: one for below the image, and one for below the label ("constraintToImage" and "constraintToLabel"). Hook them both up to your controller like in option 1, and call view.addConstraint(constraintToImage) and view.removeConstraint(constraintToLabel) when the image becomes visible (and the opposite for when it's hidden). Again, call view.setNeedsLayout after.

Autolayout Constraint unselected for Regular-Regular shows up as conflicts on iPad

I have this constraint for Any-Any size class and I have unselected it for Regular-Regular with the hope that this constraint would not apply to iPad. Then I run the app on iPad simulator and get unsatisfiable constraints error on this constraint. Am I missing something? Isn't what I did supposed to disable this constraints for iPad?
#IBOutlet weak var const1Out: NSLayoutConstraint!
var const1: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
const1 = NSLayoutConstraint(item:... // Defining the complete constraint
orientationDidChange()
}
func orientationDidChange()
{
// Just iPad
if traitCollection.verticalSizeClass == .Regular && traitCollection.horizontalSizeClass == .Regular {
// Conditioning on iPad Portrait or Landscape
if(UIDeviceOrientationIsLandscape(UIDevice.currentDevice().orientation))
{
// activate to deactivate constraints either defined as vars or outlets
}
else
{
// activate to deactivate constraints either defined as vars or outlets
}
}
}

Keep autolayout constraints active status on device rotation

I noticed that when I update my autolayout constraints programmatically, all changes are reverted when I rotate the screen.
Reproduce the issue:
Basic Storyboard interface with UIView and 2 constraints:
width equal superview.width (multiplier 1) active
width equal superview.width (multiplier 1/2) disabled
Create and link these 2 constraints with IBOutlet
Programmatically disable the first constraint and enable the second one.
Rotate the device, the first constraint is active and the second one disabled.
Seems like a bug to me.
What do you think ?
Screenshots:
Storyboard:
Constraint #1:
Constraint #2:
Size Classes
Installed refers to Size Classes installation, not to active/inactive.
You must create another constraint programmatically, and activate/deactivate that one. This is because you cannot change the multiplier of a constraint (Can i change multiplier property for NSLayoutConstraint?), nor can you tinker with Size Classes (activateConstraints: and deactivateConstraints: not persisting after rotation for constraints created in IB).
There are a few ways to do so. In the example below, I create a copy of your x1 constraint, with a multiplier or 1/2. I then toggle between the two:
#IBOutlet var fullWidthConstraint: NSLayoutConstraint!
var halfWidthConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
halfWidthConstraint = NSLayoutConstraint(item: fullWidthConstraint.firstItem,
attribute: fullWidthConstraint.firstAttribute,
relatedBy: fullWidthConstraint.relation,
toItem: fullWidthConstraint.secondItem,
attribute: fullWidthConstraint.secondAttribute,
multiplier: 0.5,
constant: fullWidthConstraint.constant)
halfWidthConstraint.priority = fullWidthConstraint.priority
}
#IBAction func changeConstraintAction(sender: UISwitch) {
if sender.on {
NSLayoutConstraint.deactivateConstraints([fullWidthConstraint])
NSLayoutConstraint.activateConstraints([halfWidthConstraint])
} else {
NSLayoutConstraint.deactivateConstraints([halfWidthConstraint])
NSLayoutConstraint.activateConstraints([fullWidthConstraint])
}
}
Tested on iOS 9+, Xcode 7+.
I will describe how to simple switch between the two layouts.
When the screen rotates, Autolayout apply the installed default layout with the higher priority.
It does not matter whether Constraint is Active or not. Because, when rotating, the high priority layout on the storyboard is reinstalled and active = true.
Therefore, even if you change active, the default layout is applied when you rotate and you can not keep any layout.
Instead of switching active state, switching two layouts.
When switching between two layouts, use "priority" rather than "active".
This way does not need to worry about the state of active.
It's very simple.
First, create two layouts to switch, on Storyboard. Check both for Installed.
A conflict error occurs because two layouts have priority = 1000 (required).
Set the priority of the layout to be displayed first to High. And the priority of the other layout is set to Low, conflict error will be resolved.
Associate constraints of those layouts as IBOutlet of class.
Finally, just switch the priority between high and low at the timing you want to change the layout.
Note, please do not change priority to "required". Layout with priority set to required can not be changed it after that.
class RootViewController: UIViewController {
#IBOutlet var widthEqualToSuperView: NSLayoutConstraint!
#IBOutlet var halfWidthOfSuperview: NSLayoutConstraint!
override func viewDidLayoutSubviews() {
changeWidth()
}
func changeWidth() {
let orientation = UIApplication.shared.statusBarOrientation
if (orientation == .portrait || orientation == .portraitUpsideDown) {
widthEqualToSuperView.priority = UILayoutPriorityDefaultHigh;
halfWidthOfSuperview.priority = UILayoutPriorityDefaultLow;
}
else {
widthEqualToSuperView.priority = UILayoutPriorityDefaultLow;
halfWidthOfSuperview.priority = UILayoutPriorityDefaultHigh;
}
}
}
portrait with full width
landscape with half width
You can do the necessary switch with the constraints created via IB for size classes. The trick is to keep the collapsed state in a variable and update constraints as on your button's event as also on trait collection change event.
var collapsed: Bool {
didSet {
view.setNeedsUpdateConstraints()
}
}
#IBAction func onButtonClick(sender: UISwitch) {
view.layoutIfNeeded()
collapsed = !collapsed
UIView.animate(withDuration: 0.3) {
view.layoutIfNeeded()
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
view.setNeedsUpdateConstraints()
}
override func updateViewConstraints() {
constraint1.isActive = !collapsed
constraint2.isActive = collapsed
super.updateViewConstraints()
}

Swift : update of view constraints not visible

I do have a UIScrollView which holds a couple of UIViewController.
For the elements inside the UIViewController I used AutoLayout.
Now I'd like to change the constants of the view constraints if the current device is a iPhone 4.
So I override updateViewConstraints() of the UIViewControllers:
override func updateViewConstraints() {
if (self.view.frame.height == 480) {
self.imageMarginLeft.constant = 40
self.imageMarginRight.constant = 40
self.textMarginTop.constant = 10
println("updateViewConstraint \(self.imageMarginRight.constant)")
}
super.updateViewConstraints()
}
But even the println logs the correct new constant the update is not visible.
What do I have to change in my implementation?
As explained here I had to integrate self.view.layoutIfNeeded() before and after changing the view contstraint constant.

Getting Exception: "Impossible to set up layout with view hierarchy unprepared for constraint"

In my storyboard application I try to add extra UITextField to interface. But I get the exception that title says. I use SwiftAutoLayout library. Here is my code:
// MARK: - IBOutlets
#IBOutlet weak var passwordTextField: UITextField!
// MARK: - Properties
let userIdTextField = UITextField()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.passwordTextField.keyboardType = .NumberPad
if getCurrentUserId() == nil {
self.view.addSubview(userIdTextField)
userIdTextField.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addConstraints([userIdTextField.al_leading == passwordTextField.al_leading,
userIdTextField.al_trailing == passwordTextField.al_trailing,
userIdTextField.al_top == passwordTextField.al_bottom + 8])
userIdTextField.placeholder = "Enter User Id..."
}
}
Try adding passwordTextField to its superview before binding any constraints to it.
UPD (thanks #vmeyer and #MacMark): please notice that it's safer to add constraints not in viewDidLoad or viewWillAppear/viewDidAppear but in updateViewConstraints or viewWillLayoutSubviews since the view hierarchy might be unprepared for this operation.
I suggest you to not add constraints in viewDidLoad, because the view hierarchy is set, but not constraints.
You should prefer updateViewConstraints or viewWillLayoutSubviews
Please add the view to superview before adding constraints for the view in superview.
superView?.addSubview(view)
superView?.addConstraints([topConstraint,leftConstraint,bottomConstraint,rightConstraint])
You are trying to adding constraint to view which is not added/created in current view hierarchy. First add view then apply constraint.

Resources