skabob11
skabob11

Reputation: 1306

Adding View, Removing it, and Adding it Again Breaks AutoLayout Constraints

I have a very simple Custom Container View Controller that swaps between two views. When the content view (a UIViewController with a .xib that includes one button with AutoLayout constraints) is added it lays out fine with no conflicting constraints. Pressing the button swaps that view for another view (another instance of the same type of view) which also lays out fine with no conflicting constraints.

When I swap the views again to reinsert the first view (which has been stored and is the same view that was removed earlier) iOS is "unable to simultaneously satisfy constraints." Every time after that second swap iOS throws the same warning of unsatisfying constraints.

Code for displaying a view controller:

func displayController(controller:UIViewController) {

    self.addChildViewController(controller)
    controller.view.setTranslatesAutoresizingMaskIntoConstraints(false)
    controller.view.frame = CGRectMake(0.0, 0.0, self.view.bounds.width, self.view.bounds.height)
    self.view.addSubview(controller.view)

    self.view.addConstraint(NSLayoutConstraint(item: controller.view, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 0))
    self.view.addConstraint(NSLayoutConstraint(item: controller.view, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Leading, multiplier: 1.0, constant: 0))
    self.view.addConstraint(NSLayoutConstraint(item: controller.view, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Width, multiplier: 1.0, constant: 0))
    self.view.addConstraint(NSLayoutConstraint(item: controller.view, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Height, multiplier: 1.0, constant: 0))

    controller.didMoveToParentViewController(self)
    self.currentViewController = controller;
}

Code for removing the view controller

func hideController(controller:UIViewController) {

    controller.willMoveToParentViewController(nil)
    controller.view.removeFromSuperview()
    controller.removeFromParentViewController()

    if self.currentViewController == controller {

        self.currentViewController = nil
    }
}

And the code for swapping the views just calls both of those methods:

func switchToViewController(controller:UIViewController) {

    if self.currentViewController != nil {

        self.hideController(self.currentViewController!)
    }

    self.displayController(controller)
}

Both child view controllers use the same .xib with a large button that has constraints set in InterfaceBuilder.

The first time these child views are added and removed they display fine with no warnings.

First child view controller Second child view controller

Once that first view is added again the height of the button is wrong and I get an "unable to simultaneously satisfy constraints" warning.

First child view controller added a second time

2015-02-23 21:40:17.223 Swift Container View Controller[27976:832141] Unable to simultaneously satisfy constraints.
(
"<NSLayoutConstraint:0x7fa57a41eed0 'UIView-Encapsulated-Layout-Height' V:[UIView:0x7fa57a715b40(667)]>",
"<NSLayoutConstraint:0x7fa57a71d1c0 V:[UIButton:0x7fa57a71bce0'Switch to Yellow View']-(413)-|   (Names: '|':UIView:0x7fa57a71cff0 )>",
"<NSLayoutConstraint:0x7fa57a71d260 V:|-(262)-[UIButton:0x7fa57a71bce0'Switch to Yellow View']   (Names: '|':UIView:0x7fa57a71cff0 )>",
"<NSLayoutConstraint:0x7fa57a71d300 V:[UIButton:0x7fa57a71bce0'Switch to Yellow View'(125)]>",
"<NSLayoutConstraint:0x7fa57a48ff10 UIView:0x7fa57a71cff0.height == UIView:0x7fa57a715b40.height>"
)

I'm fairly certain that the constraints on the button are correct as they lay out correctly the first time but then break on subsequent uses.

Upvotes: 2

Views: 1747

Answers (2)

Sam Clewlow
Sam Clewlow

Reputation: 4351

The problem is that you have the height of the child view controller defined in more than one way. These three lines are important

"<NSLayoutConstraint:0x7fa57a71d1c0 V:[UIButton:0x7fa57a71bce0'Switch to Yellow View']-(413)-|   (Names: '|':UIView:0x7fa57a71cff0 )>",

This tells us the bottom of the button is constrained to the bottom of the view with a constant (distance) of 413.

"<NSLayoutConstraint:0x7fa57a71d260 V:|-(262)-[UIButton:0x7fa57a71bce0'Switch to Yellow View']   (Names: '|':UIView:0x7fa57a71cff0 )>",

This tells us the top of the button is constrained to the top of the view with a constant (distance) of 262.

"<NSLayoutConstraint:0x7fa57a71d300 V:[UIButton:0x7fa57a71bce0'Switch to Yellow View'(125)]>",

This tells us the button is constrained to a fixed height with a constant (distance) of 125.

In order to satisfy these 3 constraints simultaneously. The view controllers view must have a height of 800 (413 + 262 + 125), no more, no less.

When you are adding the view controllers view to the container, you are attempting to again define the height with a new constraint

self.view.addConstraint(NSLayoutConstraint(item: controller.view, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Height, multiplier: 1.0, constant: 0))

Shown here in the logs:

"<NSLayoutConstraint:0x7fa57a41eed0 'UIView-Encapsulated-Layout-Height' V:[UIView:0x7fa57a715b40(667)]>"

As the view cannot be both 667pts and 800pts in height simultaneously, some of the constraints must be broken, and your interface is appearing incorrectly.

To fix this, we need to rethink the the constraints around the button. The answer is to not use top and bottom constraints for the button. Instead define the width and the height of the button, and then match the buttons centre x and y to the view controllers centre x and y.

Remember that if you have required (priority 1000) constraints chained from edge to edge (ie top to bottom or leading to trailing), this will define the size of the superview. Better to only constrain 2 sides and width and height, or match with the a relative point of the parent (eg center).

Upvotes: 2

lassej
lassej

Reputation: 6494

The problem is the constraint 'UIView-Encapsulated-Layout-Height'. It's a constraint that is added by the SDK for unknown reasons. It happens very rarely, but when it does, the only workaround I found was to give one of my own constraints a priority of 999. In your case:

let heightConstraint = NSLayoutConstraint(item: controller.view, attribute: .Height, relatedBy: .Equal, toItem: self.view, attribute: .Height, multiplier: 1.0, constant: 0)
heightConstraint.priority = 999
self.view.addConstraint( heightConstraint)

The SDK constraint is added only temporarily so your layout should work as intended.

Upvotes: 0

Related Questions