2

I've had a very frustrating time working with constraints programatically in Swift 3. At a very basic level, my application displays a number of views with initial constraints, and then applies new constraints upon rotation, to allow the views to resize and reposition as needed. Unfortunately this has been far from easy as I am still new to iOS development and Swift. I've spent a lot of time trying many different solutions offered on StackOverflow and elsewhere, but I keep reaching the same outcome (detailed at the end).

Storyboard image

I have a view controller (let's call it "Main View Controller") whose root view contains two subviews, View A and View B Container. The root view has a pink background color.

View A contains a single label inside, centered vertically and horizontally, as well as an orange background color. View A has 4 constraints - [Leading Space to Superview], [Top Space to Top Layout Guide], [Trailing Space to Superview] and [Bottom Space to Bottom Layout Guide].

View B Container initially has no content. It has 4 constraints - [Width Equals 240], [Height Equals 128], [Leading Space to Superview] and [Leading Space to Superview].


I also have another view controller (let's call it "View B View Controller") that drives the content for the View B Container. For the sake of simplicity, this is just a default view controller with no custom logic. The root view of View B View Controller contains a single subview, View B.

View B is almost identical to View A above - single label centered vertically and horizontally and a blue background color. View B has 4 constraints - [Leading Space to Superview], [Top Space to Superview], [Trailing Space to Superview] and [Bottom Space to Superview].


In the Main View Controller class, I've maintained IBOutlet references to View A and View B Container, as well as their respective constraints mentioned above. In the below code, the Main View Controller instantiates the View B View Controller and adds the subsequent view to the View B Container, applying a flexible width/height auto-resizing mask to ensure it fills the available space. Then it fires a call to the internal _layoutContainers() function which performs a number of constraint-modifying operations depending on the device's orientation. The current implementation does the following:

  • removes the known constraints from View A
  • removes the known constraints from View B Container
  • depending on device orientation, activate new constraints for both View A and View B Container according to a specific design (detailed in code comments below)
  • fire off updateConstraintsIfNeeded() and layoutIfNeeded() against all views

When a resize event occurs, the code allows the viewWillTransition() to fire and then calls the _layoutContainers() function in the completion callback, so that the device is in a new state and can follow the necessary logic path.

The entire Main View Controller unit is below:

import UIKit

class ViewController: UIViewController {

    // MARK: Variables

    @IBOutlet weak var _viewAView: UIView!

    @IBOutlet weak var _viewALeadingConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewATopConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewATrailingConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewABottomConstraint: NSLayoutConstraint!

    @IBOutlet weak var _viewBContainerView: UIView!

    @IBOutlet weak var _viewBContainerWidthConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewBContainerHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewBContainerTopConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewBContainerLeadingConstraint: NSLayoutConstraint!

    // MARK: UIViewController Overrides

    override func viewDidLoad() {
        super.viewDidLoad()

        // Instantiate View B's controller
        let viewBViewController = self.storyboard!.instantiateViewController(withIdentifier: "ViewBViewController")
        self.addChildViewController(viewBViewController)

        // Instantiate and add View B's new subview 
        let view = viewBViewController.view
        self._viewBContainerView.addSubview(view!)
        view!.frame = self._viewBContainerView.bounds
        view!.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        viewBViewController.didMove(toParentViewController: self)

        self._layoutContainers()
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

        coordinator.animate(alongsideTransition: nil, completion: { _ in
            self._layoutContainers()
        })
    }

    // MARK: Internal

    private func _layoutContainers() {

        // Remove View A constraints
        self._viewAView.removeConstraints([
            self._viewALeadingConstraint,
            self._viewATopConstraint,
            self._viewATrailingConstraint,
            self._viewABottomConstraint,
        ])

        // Remove View B Container constraints
        var viewBContainerConstraints: [NSLayoutConstraint] = [
            self._viewBContainerTopConstraint,
            self._viewBContainerLeadingConstraint,
        ]

        if(self._viewBContainerWidthConstraint != nil) {
            viewBContainerConstraints.append(self._viewBContainerWidthConstraint)
        }
        if(self._viewBContainerHeightConstraint != nil) {
            viewBContainerConstraints.append(self._viewBContainerHeightConstraint)
        }

        self._viewBContainerView.removeConstraints(viewBContainerConstraints)


        // Portrait:
        // View B - 16/9 and to bottom of screen
        // View A - anchored to top and filling the remainder of the vertical space

        if(UIDevice.current.orientation != .landscapeLeft && UIDevice.current.orientation != .landscapeRight) {

            let viewBWidth = self.view.frame.width
            let viewBHeight = viewBWidth / (16/9)
            let viewAHeight = self.view.frame.height - viewBHeight

            // View A - anchored to top and filling the remainder of the vertical space
            NSLayoutConstraint.activate([
                self._viewAView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
                self._viewAView.topAnchor.constraint(equalTo: self.view.topAnchor),
                self._viewAView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
                self._viewAView.bottomAnchor.constraint(equalTo: self._viewBContainerView.topAnchor),
            ])

            // View B - 16/9 and to bottom of screen
            NSLayoutConstraint.activate([
                self._viewBContainerView.widthAnchor.constraint(equalToConstant: viewBWidth),
                self._viewBContainerView.heightAnchor.constraint(equalToConstant: viewBHeight),
                self._viewBContainerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: viewAHeight),
                self._viewBContainerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            ])
        }

        // Landscape:
        // View B - 2/3 of screen on left
        // View A - 1/3 of screen on right
        else {
            let viewBWidth = self.view.frame.width * (2/3)

            // View B - 2/3 of screen on left
            NSLayoutConstraint.activate([
                self._viewBContainerView.widthAnchor.constraint(equalToConstant: viewBWidth),
                self._viewBContainerView.heightAnchor.constraint(equalToConstant: self.view.frame.height),
                self._viewBContainerView.topAnchor.constraint(equalTo: self.view.topAnchor),
                self._viewBContainerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            ])

            // View A - 1/3 of screen on right
            NSLayoutConstraint.activate([
                self._viewAView.leadingAnchor.constraint(equalTo: self._viewBContainerView.trailingAnchor),
                self._viewAView.topAnchor.constraint(equalTo: self.view.topAnchor),
                self._viewAView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
                self._viewAView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
            ])
        }

        // Fire off constraints and layout update functions

        self.view.updateConstraintsIfNeeded()
        self._viewAView.updateConstraintsIfNeeded()
        self._viewBContainerView.updateConstraintsIfNeeded()

        self.view.layoutIfNeeded()
        self._viewAView.layoutIfNeeded()
        self._viewBContainerView.layoutIfNeeded()
    }
}

My problem is that, although the initial load of the application displays the expected result (View B maintaining a 16/9 ratio and sitting at the bottom of the screen, View A taking up the remaining space):

Successful display

Any subsequent rotation breaks the views completely and doesn't recover:

Failed display in landscape

Failed display in portrait

Additionally, the following constraints warnings are thrown once the application loads:

TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<_UILayoutSupportConstraint:0x600000096c60 _UILayoutGuide:0x7f8d4f414110.height == 0   (active)>",
    "<_UILayoutSupportConstraint:0x600000090ae0 V:|-(0)-[_UILayoutGuide:0x7f8d4f414110]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>",
    "<NSLayoutConstraint:0x600000096990 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f413e60]   (active)>",
    "<NSLayoutConstraint:0x608000094e10 V:|-(456.062)-[UIView:0x7f8d4f413e60]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000096990 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f413e60]   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.



TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x600000096940 UIView:0x7f8d4f413e60.leading == UIView:0x7f8d4f40f9e0.leadingMargin   (active)>",
    "<NSLayoutConstraint:0x608000094e60 H:|-(0)-[UIView:0x7f8d4f413e60]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000096940 UIView:0x7f8d4f413e60.leading == UIView:0x7f8d4f40f9e0.leadingMargin   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.



TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<_UILayoutSupportConstraint:0x600000096d50 _UILayoutGuide:0x7f8d4f40f4b0.height == 0   (active)>",
    "<_UILayoutSupportConstraint:0x600000096d00 _UILayoutGuide:0x7f8d4f40f4b0.bottom == UIView:0x7f8d4f40f9e0.bottom   (active)>",
    "<NSLayoutConstraint:0x600000092e30 V:[UIView:0x7f8d4f40fd90]-(0)-[_UILayoutGuide:0x7f8d4f40f4b0]   (active)>",
    "<NSLayoutConstraint:0x608000092070 UIView:0x7f8d4f40fd90.bottom == UIView:0x7f8d4f413e60.top   (active)>",
    "<NSLayoutConstraint:0x608000094e10 V:|-(456.062)-[UIView:0x7f8d4f413e60]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>",
    "<NSLayoutConstraint:0x600000096e40 'UIView-Encapsulated-Layout-Height' UIView:0x7f8d4f40f9e0.height == 667   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000092e30 V:[UIView:0x7f8d4f40fd90]-(0)-[_UILayoutGuide:0x7f8d4f40f4b0]   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.



TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<_UILayoutSupportConstraint:0x600000096c60 _UILayoutGuide:0x7f8d4f414110.height == 20   (active)>",
    "<_UILayoutSupportConstraint:0x600000090ae0 V:|-(0)-[_UILayoutGuide:0x7f8d4f414110]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>",
    "<NSLayoutConstraint:0x600000096850 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f40fd90]   (active)>",
    "<NSLayoutConstraint:0x608000093b50 V:|-(0)-[UIView:0x7f8d4f40fd90]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000096850 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f40fd90]   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

Thank you for reading if you got this far! Surely someone has encountered (and hopefully solved) this or a similar issue. Any help would be immensely appreciated!

2 Answers2

1

Instead of trying to add and remove constraints consider just adjusting a priority to transform your view instead.

So for you default layout have a constraint with priority 900. Then add a second conflicting constraint with priority 1. Now to toggle the display mode just move that second constraint priority up above 900, and then back below to reverse. Easy to test it all in Interface Builder by just changing the priority too.

Also you can put the change in an animation block to get a nice smooth transition.

-

One other thing to consider using is size classes. Using this you can specify that particular constraints only apply for certain orientations so you could probably get your desired behaviour entirely 'for free', just set it all up in IB.

trapper
  • 11,716
  • 7
  • 38
  • 82
  • That did the trick! Thank you greatly for the suggestion @trapper, I rewrote the code (see https://pastebin.com/7qJ3FGnv) to initialise and store all the appropriate constraints, and then toggle the priority of all constraints based on which set of constraints should be displaying (I used 999 and 1 for high/low). One gotcha I encountered was setting the desired constraints to a priority of 1000 - as mentioned in [another StackOverflow post](https://stackoverflow.com/a/31186261/8306004), you can't change between a required and non-required value. – Spaghetti Robonaut Jul 17 '17 at 07:41
  • Yeah you can't use 1000 if you want to toggle it. Also by using less than 999 for your 'default' layout you can still put something above it, thats why I said use 900. So you would only need to toggle priority on one constraint, rather than changing two. – trapper Jul 17 '17 at 08:12
  • btw, regarding your pastebin, you could do this with size classes and won't need any of that code at all – trapper Jul 17 '17 at 08:15
0

Part of the issue is that in _layoutContainers you remove the constaints from the storyboard and add now ones, but on subsequent rotations you don't remove the previous ones you added. You should store the new constraints that you create so that the next time the screen rotates, you can get the old constraints and remove them.

Also, calling _layoutContainers from viewDidLoad is too early in the VCs lifecycle since the views frame won't have the correct value yet. You can crate aspect ratio constraints so you don't have to calculate the size manually.

For example, the portrait constraint for

// View B - 16/9 and to bottom of screen
NSLayoutConstraint.activate([
    self._viewBContainerView.heightAnchor.constraint(equalToConstant: self._viewBContainerView.widthAnchor, multiplier: 16.0 / 9.0),
    self._viewBContainerView.topAnchor.constraint(equalTo: self.view.bottomAnchor),
    self._viewBContainerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
    // should there be a constraint for self._viewBContainerView.trailingAnchor?
])
Craig Siemens
  • 12,942
  • 1
  • 34
  • 51
  • Thanks for taking the time to read/reply :) I revised my code to store each of the new constraints and also moved the call to _layoutConstraints() into viewDidAppear (https://pastebin.com/eDaar909) but unfortunately I still get the same behaviour after the first rotation. – Spaghetti Robonaut Jul 17 '17 at 05:06
  • From debugging and stepping through the code, I've noticed that the constraints warnings in the logs are first generated after the View A constraints are activated (line 115 in https://pastebin.com/eDaar909). I thought this may be an issue of precedence, given that View A references a View B constraint anchor, but changing the order of activation doesn't seem to change that. – Spaghetti Robonaut Jul 17 '17 at 05:12