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
})
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):
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:
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:
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
Get Device Token for Push Notification
Xcodebuild Different Provisioning Profile for Target Dependency
Change Tab Bar Item Selected Color in a Storyboard
Failed to Read Values in Cfprefsplistsource iOS 10
Disable Uipageviewcontroller Bounce
How to Access Own Window Within Swiftui View
Xmppframework - Implement Group Chat (Muc)
Working with C APIs from Swift
Invalid Swift Support - Files Don't Match
Afnetworking 3.0 Migration: How to Post with Headers and Http Body
How to Get the Console Logs from the iOS Simulator
Check If a Uiscrollview Reached the Top or Bottom
Scrollview Gesture Recognizer Eating All Touch Events
Arkit - Apply Cifilter to a Specific Vertices of Arfaceanchor
Wait Until Multiple Networking Requests Have All Executed - Including Their Completion Blocks