Tuesday, August 21, 2018

Auto Layout constraint breaks when set active

Leave a Comment

I have a set of AL constraints positioning a child vc that has two positions, expanded and collapsed.

I found that when I add the collapsed constraint, a top anchor to bottom anchor constraint with a constant, when the vc is first created, there seems to be additional spacing when I activate it. Seemingly because the actual height isn't available at the time.

When I add the constraint in viewDidLayoutSubviews there additional spacing is gone and the constraint behaves properly. Except the issue that now when I switch between the constraints in an animation, I cannot deactivate the collapsed constraint as I switch to the expanded constraint and the constraint breaks. Possibly because viewDidLayoutSubviews is called throughout the transition animation.

Here's an abstract of vc setup.

var foregroundExpandedConstraint: NSLayoutConstraint! var foregroundCollapsedConstraint: NSLayoutConstraint!  var foregroundViewController: UIViewController? {     didSet {          setupforegroundViewController(foregroundViewController: foregroundViewController!)     } }  func setupforegroundViewController(foregroundViewController: UIViewController) {      addChildViewController(foregroundViewController)     foregroundViewController.didMove(toParentViewController: self)      guard let foregroundView = foregroundViewController.view else { return }     foregroundView.translatesAutoresizingMaskIntoConstraints = false     view.addSubview(foregroundView)      foregroundExpandedConstraint = foregroundView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 15)      let height =  view.safeAreaLayoutGuide.layoutFrame.height - 50 - 15     let cellHeight = ((height) / 6)             foregroundCollapsedConstraint = NSLayoutConstraint(item: foregroundView, attribute: .top, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .bottom, multiplier: 1, constant: (-cellHeight) * 2 - 50)      let foregroundViewControllerViewConstraints = [         foregroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),         foregroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),         foregroundView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, constant: -50 - 15),         foregroundExpandedConstraint!         ]       NSLayoutConstraint.activate(foregroundViewControllerViewConstraints) } 

And here the animations are preformed using UIViewPropertyAnimator.

func animateTransitionIfNeeded(state: ForegroundState, duration: TimeInterval) {      let containerFrameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {         [unowned self] in          switch state {         case .expanded:             self.foregroundCollapsedConstraint?.isActive = false             self.foregroundExpandedConstraint?.isActive = true             self.view.layoutIfNeeded()         case .collapsed:             self.foregroundExpandedConstraint?.isActive = false             self.foregroundCollapsedConstraint?.isActive = true             self.view.layoutIfNeeded()         }     }      containerFrameAnimator.addCompletion {  [weak self] (position) in          if position == .start {             switch state {             case .collapsed:                 self?.foregroundCollapsedConstraint?.isActive = false                 self?.foregroundExpandedConstraint?.isActive = true                 self?.foregroundIsExpanded = true                 self?.view.layoutIfNeeded()             case .expanded:                 self?.foregroundExpandedConstraint?.isActive = false                 self?.foregroundCollapsedConstraint?.isActive = true                 self?.foregroundIsExpanded = false                 self?.view.layoutIfNeeded()             }         } else if position == .end {             switch state {             case .collapsed:                 self?.foregroundExpandedConstraint?.isActive = false                 self?.foregroundCollapsedConstraint?.isActive = true                 self?.foregroundIsExpanded = false             case .expanded:                 self?.foregroundExpandedConstraint?.isActive = false                 self?.foregroundCollapsedConstraint?.isActive = true                 self?.foregroundIsExpanded = true             }         }         self?.runningAnimations.removeAll()     } 

Again to reiterate, when I use the following code, setting the constraint as the vc is added to the view hierarchy, it doesn't layout properly. Checking the constraints I see they change after view did layout subviews is called. Each constraint changes appropriately except for the collapsed constraint.

When I add the collapsed constraint in view did layout subviews it behaves properly however I am unable to deactivate it going forwards and the constraint breaks.

override func viewDidLayoutSubviews() {     super.viewDidLayoutSubviews()      let height =  view.safeAreaLayoutGuide.layoutFrame.height - 50 - 15     let cellHeight = ((height) / 6)      if let v = foregroundViewController?.view {         foregroundCollapsedConstraint = NSLayoutConstraint(item: v, attribute: .top, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .bottom, multiplier: 1, constant: (-cellHeight) * 2 - 50)     } } 

Edit: I've created a repo demonstrating the issue: https://github.com/louiss98/UIViewPropertyAnimator-Layout-Test

Any suggestions?

1 Answers

Answers 1

You can eliminate the "broken" constraint by changing the constant instead of creating a new constraint.

In your viewDidLayoutSubviews() func,

change:

override func viewDidLayoutSubviews() {     super.viewDidLayoutSubviews()      let height =  view.safeAreaLayoutGuide.layoutFrame.height - 50 - 15     let cellHeight = ((height) / 6)      foregroundCollapsedConstraint = NSLayoutConstraint(item: testViewController.view, attribute: .top, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .bottom, multiplier: 1, constant: (-cellHeight) * 2 - 50) } 

to:

override func viewDidLayoutSubviews() {     super.viewDidLayoutSubviews()      let height =  view.safeAreaLayoutGuide.layoutFrame.height - 50 - 15     let cellHeight = ((height) / 6)      foregroundCollapsedConstraint.constant = (-cellHeight) * 2 - 50 } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment