Uistackview Hide View Animation

UIStackView Hide View Animation

Just had the same issue.
The fix is adding stackView.layoutIfNeeded() inside the animation block. Where stackView is the container of the items you're wishing to hide.

UIView.animate(withDuration: DiscoverHeaderView.animationDuration,
delay: 0.0,
usingSpringWithDamping: 0.9,
initialSpringVelocity: 1,
options: [],
animations: {
clear.isHidden = hideClear
useMyLocation.isHidden = hideLocation
stackView.layoutIfNeeded()
},
completion: nil)

Not sure why this is suddenly an issue in iOS 11 but to be fair it has always been the recommended approach.

UIStackView - hide and collapse subview with animation

According to Apple's documentation:

You can animate both changes to the arranged subview’s isHidden property and changes to the stack view’s properties by placing these changes inside an animation block.

I've tested the below code using iOS 12.1 Simulator and it works as expected.

UIView.animate(
withDuration: 2.0,
delay: 0.0,
options: [.curveEaseOut],
animations: {
self.label.isHidden = true
self.label.alpha = 0.0
})

Arranged Subview Animation Gif

UIStackView show/hide animation

Stack view's automatic show/hide animation works great --- for some things. For others, such as with a Picker View, not so much (as you've seen).

One approach would be:

  • embed the picker view in a regular view
  • constrain it centered vertically
  • add a default height to the containing view (such as slightly taller than the picker view)
  • animate the view's height constraint

Picker views will not "squeeze" on their own though, so you'll get a "disappearing" picker view. If you want it to "squeeze" as it animates, you'll also need to animate its transform

Here is an example (I use contrasting colors to make it easy to see elements, and I've slowed the animation duration to make it obvious):

Sample Image

Here is sample code:

class StackDemoViewController: UIViewController {

@IBOutlet var pickerHolderView: UIView!
@IBOutlet var pickerHolderHeightConstraint: NSLayoutConstraint!

@IBOutlet var normalButton: UIButton!
@IBOutlet var squeezeButton: UIButton!

@IBOutlet var thePickerView: UIDatePicker!

// this will be assigned in viewDidLoad
var defaultPickerHolderViewHeight: CGFloat = 0.0

// anim duration - change to something like 1.0 to see the effect in "slo-motion"
let animDuration = 0.3

override func viewDidLoad() {
super.viewDidLoad()

// get the original picker holder view height constant
defaultPickerHolderViewHeight = pickerHolderHeightConstraint.constant
}

@IBAction func normalAnim(_ sender: Any) {

// local bool
let bIsHidden = pickerHolderView.isHidden

// if the picker holder view is currently hidden, show it
if bIsHidden {
pickerHolderView.isHidden = false
}

// if picker holder height constant is > 0 (it's open / showing)
// set it to 0
// else
// set it to defaultPickerHolderViewHeight
self.pickerHolderHeightConstraint.constant = self.pickerHolderHeightConstraint.constant > 0 ? 0 : defaultPickerHolderViewHeight

// animate the change
UIView.animate(withDuration: animDuration, animations: {
self.view.layoutIfNeeded()
}) { finished in
// if the picker holder view was showing (NOT hidden)
// hide it
if !bIsHidden {
self.pickerHolderView.isHidden = true
// disable squeeze button until view is showing again
self.squeezeButton.isEnabled = false
} else {
// re-enable squeeze button
self.squeezeButton.isEnabled = true
}
}
}

@IBAction func squeezeAnim(_ sender: Any) {

// local bool
let bIsHidden = pickerHolderView.isHidden

var t = CGAffineTransform.identity

// if the picker holder view is currently hidden, show it
if bIsHidden {
pickerHolderView.isHidden = false
} else {
// we're going to hide it
t = CGAffineTransform(scaleX: 1.0, y: 0.01)
}

// if picker holder height constant is > 0 (it's open / showing)
// set it to 0
// else
// set it to defaultPickerHolderViewHeight
self.pickerHolderHeightConstraint.constant = self.pickerHolderHeightConstraint.constant > 0 ? 0 : defaultPickerHolderViewHeight

// animate the change
UIView.animate(withDuration: animDuration, animations: {
self.thePickerView.transform = t
self.view.layoutIfNeeded()
}) { finished in
// if the picker holder view was showing (NOT hidden)
// hide it
if !bIsHidden {
self.pickerHolderView.isHidden = true
// disable normal button until view is showing again
self.normalButton.isEnabled = false
} else {
// re-enable normal button
self.normalButton.isEnabled = true
}
}
}

}

Using this layout:

Sample Image

and, here is the source of the Storyboard (so you can quickly try it out yourself):
























































































How do I disable UIStackView default show/hide animation?

Without any additional information, I'm going to guess you're doing something along these lines:

    self.view2.isHidden.toggle()

// animate constraint change
self.animLeadingConstraint.isActive.toggle()
self.animTrailingConstraint.isActive = !self.animLeadingConstraint.isActive
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})

You get the stack view animation because:

    // nothing happening between
// hide / show arranged subview
// and
// the animation block

// so, this is the START of the "animation"
self.view2.isHidden.toggle()

// animate constraint change
self.animLeadingConstraint.isActive.toggle()
self.animTrailingConstraint.isActive = !self.animLeadingConstraint.isActive
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})

Various ways to avoid that, including:

    // animate constraint change
self.animLeadingConstraint.isActive.toggle()
self.animTrailingConstraint.isActive = !self.animLeadingConstraint.isActive
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})

// hide / show arranged subview AFTER animation block
self.view2.isHidden.toggle()

and:

    // hide / show arranged subview
self.view2.isHidden.toggle()

// force layout update
self.view.setNeedsLayout()
self.view.layoutIfNeeded()

// now start the animation

// animate constraint change
self.animLeadingConstraint.isActive.toggle()
self.animTrailingConstraint.isActive = !self.animLeadingConstraint.isActive
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})

Here's a full example demonstrating the differences:

class ViewController: UIViewController {

let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
return v
}()

let animView = UILabel()
let view1 = UILabel()
let view2 = UILabel()
let stackContainer = UIView()

var animLeadingConstraint: NSLayoutConstraint!
var animTrailingConstraint: NSLayoutConstraint!

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .white

// add three buttons at the top
let btnsStack = UIStackView()
btnsStack.spacing = 20
btnsStack.distribution = .fillEqually

["Default", "Fix 1", "Fix 2"].forEach { str in
let b = UIButton()
b.setTitle(str, for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.gray, for: .highlighted)
b.backgroundColor = .systemGreen
b.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
btnsStack.addArrangedSubview(b)
}

for (v, s) in zip([animView, view1, view2], ["Will Animate", "View 1", "View 2"]) {
v.text = s
v.textAlignment = .center
v.layer.borderWidth = 2
v.layer.borderColor = UIColor.red.cgColor
}

animView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)

stackContainer.backgroundColor = .systemTeal

[btnsStack, stackView, stackContainer, animView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}

stackView.addArrangedSubview(view1)
stackView.addArrangedSubview(view2)

stackContainer.addSubview(stackView)

view.addSubview(btnsStack)
view.addSubview(stackContainer)
view.addSubview(animView)

let g = view.safeAreaLayoutGuide

// Leading and Trailing constraints for the animView
// so we can "slide" it back and forth
animLeadingConstraint = animView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0)
animLeadingConstraint.priority = .defaultHigh
animTrailingConstraint = animView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0)
animTrailingConstraint.priority = .defaultHigh

NSLayoutConstraint.activate([

btnsStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
btnsStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
btnsStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),

stackContainer.topAnchor.constraint(equalTo: btnsStack.bottomAnchor, constant: 40.0),
stackContainer.centerXAnchor.constraint(equalTo: g.centerXAnchor),

stackView.topAnchor.constraint(equalTo: stackContainer.topAnchor),
stackView.leadingAnchor.constraint(equalTo: stackContainer.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: stackContainer.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: stackContainer.bottomAnchor),

view1.widthAnchor.constraint(equalToConstant: 240.0),
view1.heightAnchor.constraint(equalToConstant: 160.0),

view2.widthAnchor.constraint(equalTo: view1.widthAnchor),
view2.heightAnchor.constraint(equalTo: view1.heightAnchor),

animView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
animView.widthAnchor.constraint(equalToConstant: 160.0),
animView.heightAnchor.constraint(equalToConstant: 40.0),
animLeadingConstraint,

])

}

@objc func btnTap(_ sender: Any?) -> Void {
guard let btn = sender as? UIButton else {
return
}
if btn.currentTitle == "Fix 1" {
fixedApproachOne()
} else if btn.currentTitle == "Fix 2" {
fixedApproachTwo()
} else {
defaultApproach()
}
}

func defaultApproach() -> Void {

// nothing happening between
// hide / show arranged subview
// and
// the animation block

// so, this is the START of the "animation"
self.view2.isHidden.toggle()

runAnim()

}

func fixedApproachOne() -> Void {

// start the animation
runAnim()

// hide / show arranged subview AFTER animation block
self.view2.isHidden.toggle()

}

func fixedApproachTwo() -> Void {

// hide / show arranged subview
self.view2.isHidden.toggle()

// force layout update
self.view.setNeedsLayout()
self.view.layoutIfNeeded()

// now start the animation
runAnim()

}

func runAnim() -> Void {
// animate constraint change
self.animLeadingConstraint.isActive.toggle()
self.animTrailingConstraint.isActive = !self.animLeadingConstraint.isActive
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})
}

}

and it looks like this:

Sample Image

UIStackView different show hide animation between ios versions

It looks like you forgot to decrease priority for view3 height constraint.

UIStackView hides it's subviews by constraining their height to 0, so if you have other constraints for view height, they will conflict with the stack view.



Related Topics



Leave a reply



Submit