3

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?

Stefan
  • 908
  • 1
  • 11
  • 33
  • If you show your code (see: [mcve]) you're likely to get more help. Without seeing what you're actually doing, you may be better off changing the `priority` of your two constraints, rather than trying to activate/deactivate. – DonMag Aug 14 '18 at 13:02
  • @DonMag I've included relevant code, could you take a look at it and see if changing priority is what's needed. Also I always thought that setting a constraint equal to active was the same as setting it's priority to required (999) and vice versa. – Stefan Aug 14 '18 at 19:23
  • I don't know. But take a look [here](https://stackoverflow.com/questions/47823639/why-calling-setneedsupdateconstraints-isnt-needed-for-constraint-changes-or-ani) for some general insight – mfaani Aug 15 '18 at 14:30
  • @Stefan - create an example project showing your issue... as simple as possible. Really tough to say what might be wrong without being able to run your code. – DonMag Aug 15 '18 at 15:59
  • @DonMag Here's a link to a repo I created showcasing the issue: https://github.com/louiss98/UIViewPropertyAnimator-Layout-Test . It looks like it's an issue in how the constraints are set during the transition possibly. – Stefan Aug 15 '18 at 22:21
  • You still having the problem? – J. Doe Aug 18 '18 at 21:55
  • @J.Doe I've figured out an alternative method that avoids the issue but I still don't really understand why exactly the constraint was breaking. My best guess is that deactivating the constraint actually removes reference to it or maybe setting it after deactivating it created a new instance of it and I was activating/deactivating a "phantom" constraint maybe. I'll update the question to better represent the issue at hand. – Stefan Aug 18 '18 at 22:14

1 Answers1

0

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
}
DonMag
  • 69,424
  • 5
  • 50
  • 86