Search code examples
iosanimationstackview

stackview animation - collapse of arranged subviews


I have to use stackview as parent view. I'm trying to animate stackview with 2 rows to get an effect of collapsing and inflating the bottom row. You can say that, what I'm trying to do is the same thing you get when you apply this code to normal autolayouted view with subviews:

func showView()
{
    if(!expand)
    {  UIView.animateWithDuration(0.5, animations:{
        self.expandableViewHeight.constant = 50
        // Update layout of all subviews
        self.parentViewController!.view.layoutIfNeeded()})
    } else {   
      UIView.animateWithDuration(0.5, animations:{
        self.expandableViewHeight.constant = 100
        // Update layout of all subviews
        self.parentViewController!.view.layoutIfNeeded()})
    }
}

Simple and neat. When you let's say click on this view, it expands or collapses with all it's subviews. The subviews are clipped (not resized/rescaled), as the animation goes, and finally you can see only half of the original view.

It looks like a simple thing, but I can't get it done with stackview. When I'm adding second row to stackview and then animate layoutIfNeeded(), as in several tutorials, then : - when inflating the bottom row comes from the left to it's position, - when collapsing the bottom row just disappears (no animation) - only background view is animated properly (see the code)

When I'm using height constraints and don't animate on layoutIfNeeded() on the bottom row, then: - when inflating the bottom row is rescaled to full height as the animation goes - when collapsing - the bottom row rescaled to 0 as the animation goes

Cannot make it clip the bottom row! Any tips appreciated! :)

This is my stackview code:

override func viewDidLoad(){
    super.viewDidLoad()

    background = UIView(frame:CGRectMake(0, 0, frame.width,frame.height))
    background.backgroundColor = UIColor.whiteColor()
    background.layer.cornerRadius = 5
    background.layer.masksToBounds = true

    self.addSubview(background)

    vStack = UIStackView()
    vStack.axis = .Vertical
    vStack.alignment = .Fill
    vStack.distribution = .Fill
    vStack.spacing = 0

    self.addArrangedSubview(vStack)

    hStack = UIStackView()
    hStack.axis = .Horizontal
    hStack.alignment = .Center
    hStack.distribution = .EqualSpacing
    hStack.spacing = 10

    hStack2 = UIStackView()
    hStack2.axis = .Horizontal
    hStack2.alignment = UIStackViewAlignment.Center
    hStack2.distribution = UIStackViewDistribution.EqualCentering
    hStack2.spacing = 10

    lquestionStack = UIStackView()
    questionStack.axis = .Horizontal
    questionStack.alignment = .Center
    questionStack.distribution = .EqualSpacing
    questionStack.spacing = QUESTION_PADDING


    let labelQuestionNumber = UIButton()
    labelQuestionNumber.userInteractionEnabled = false
    let numberImage = UIImage(named: "backgroundImage")
    labelQuestionNumber.setBackgroundImage(numberImage, forState: .Normal)

    questionStack.addArrangedSubview(labelQuestionNumber)


    hStack.addArrangedSubview(questionStack)
    vStack.addArrangedSubview(hStack)

    hStack2.addArrangedSubview(questionStack)
    vStack.addArrangedSubview(hStack2)

 }
 public func collapseInflateAction() {
    if checkWidget.collapsed {
        inflateWidget()
    } else {
        collapseWidget()
    }
    checkWidget.collapsed = !checkWidget.collapsed
}

private func collapseWidget(){
    vStack.removeArrangedSubview(self.hStack2)
    self.hStack2.removeFromSuperview()

    UIView.animateWithDuration(0.5, animations: { () -> Void in
        self.background.frame.size.height = (self.background.frame.size.height)/2
        self.layoutIfNeeded()
        self.superview?.layoutIfNeeded()
        })
}

private func inflateWidget(){
    self.vStack.addArrangedSubview(self.hStack2)

    UIView.animateWithDuration(0.5, animations: { () -> Void in
        self.background.frame.size.height = self.background.frame.size.height*2
        self.layoutIfNeeded()
        self.superview?.layoutIfNeeded()
    })

}

And this is the variation of the 2 last methods that uses constraints instead of layoutIfNeeded() animation. And also the constraint being modified:

override func viewDidLoad(){
    super.viewDidLoad()
(...)
 heightConstraint = NSLayoutConstraint(item: hStack2, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: 0)
    self.addConstraint(heightConstraint)
}
private func collapseWidget(){
    UIView.animateWithDuration(0.5, animations: { () -> Void in
        self.background.frame.size.height = 44
        self.heightConstraint.constant = 0
        self.superview?.layoutIfNeeded()
        })
}

private func inflateWidget(){
    UIView.animateWithDuration(0.5, animations: { () -> Void in
        self.background.frame.size.height = 88
        self.heightConstraint.constant = 44
        self.superview?.layoutIfNeeded()
    })

}

Solution

  • That was a long time ago, but I think it helped to add self.layoutIfNeeded() before layouting superview

       fileprivate func collapseWidget(){
    
        UIView.animate(withDuration: 0.25, animations: { () -> Void in
            self.background.frame.size.height = self.heightCollapsed
            self.heightConstraint.constant = self.heightCollapsed
            self.layoutIfNeeded()
            self.superview?.layoutIfNeeded()
            })
    }
    
    fileprivate func inflateWidget(){
        self.separatorView.isHidden = false
        UIView.animate(withDuration: 0.25, animations: { () -> Void in
            self.background.frame.size.height = self.heightInflated
            self.heightConstraint.constant = self.heightInflated
            self.layoutIfNeeded()
            self.superview?.layoutIfNeeded()
        })
    }