3

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:

    1. width equal superview.width (multiplier 1) active
    2. 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: enter image description here

Constraint #1:

enter image description here

Constraint #2:

enter image description here

Tobi Nary
  • 4,566
  • 4
  • 30
  • 50
Salah
  • 501
  • 1
  • 5
  • 18
  • Do you use storyboard? It looks like size classes behaviour. – kedzia Feb 01 '16 at 13:01
  • Yes, I use storyboard but the constraints are enabled for all size classes. I don't have any size class specific configuration. – Salah Feb 01 '16 at 13:54
  • I would try to set them as you want them to be for rotated size class – kedzia Feb 01 '16 at 14:28
  • @Salah if possible post screenshot of your constraints – Kishore Kumar Feb 01 '16 at 14:30
  • @KishoreKumar Just added screenshots :) – Salah Feb 01 '16 at 14:52
  • @kedzia The constraints does not depend on size class at all. FYI, in my specific case, I have a view with 2 buttons (50% width each) and in some cases I want to hide one button and display the other one with 100% width. – Salah Feb 01 '16 at 14:55

3 Answers3

7

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+.

Community
  • 1
  • 1
SwiftArchitect
  • 47,376
  • 28
  • 140
  • 179
  • Thank you, I didn't know that "installed" was only meant for size classes. Just to be clear, when I create a constraint programmatically, it is assigned by default to all size classes ? – Salah Feb 02 '16 at 09:31
  • 1
    Programmatically created constraints are applied to all Size Classes. Enabling/Disabling them requires extra effort: http://stackoverflow.com/a/27289658/218152 – SwiftArchitect Feb 02 '16 at 13:21
3

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 portrait landscape with half width landscape

yaslam
  • 108
  • 7
0

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()
}
IvanRublev
  • 784
  • 9
  • 10