UIStackView Hide View Animation
Asked Answered
P

6

116

In iOS 11 the behaviour of the hide animation within a UIStackView has changed, but I have been unable to find this documented anywhere.

iOS 10

iOS 10 animation

iOS 11

iOS 11 animation

The code in both is this:

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

How do I restore the previous behaviour on iOS 11?

Phylogeny answered 20/9, 2017 at 15:36 Comment(0)
M
188

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.

Mikesell answered 25/9, 2017 at 19:16 Comment(9)
In iOS <= 10 there was a bug in which setting the hidden property of a UIStackView's subview in the animation block was being ignored in some cases, so the best way is to change it outside of it, right before the animation.Pallmall
I feel that view.layoutIfNeeded() is better than stackView.layoutIfNeeded()Stoneham
Might be a misunderstanding on my part but it doesn't sound from the docs like view.layoutIfNeeded() would update the position of other views in the StackView which is what we want. developer.apple.com/documentation/uikit/uiview/…Mikesell
@SoftDesigner. You should call layoutIfNeeded() for the direct superview of the animated views which is stackView here.Palermo
i'm calling setNeedsLayout before layoutIfNeeded, i'm using without any issue.Planetary
@RatulSharker I think you only need that if the view either hasn't finished laying out when you attempt to animate or it needs to be updated before you animateMikesell
view.layoutIfNeeded() is ok, however calling view.isHidden = true if view is already hidden (or the opposite) break the thing. So make sure to check if the view is not already is the hidden state you want to change. if(view.isHidden == true) { view.isHidden = false }Underwood
Thanks @glemoulant, I had the same issue and checking the property before setting it seems to do the trick.Torry
Didn't work for me. iOS 15.Any
D
61

It's already mentioned in the comments of the accepted answer, but this was my problem and it's not in any of the answer here so:

Make sure to never set isHidden = true on a view that is already hidden. This will mess up the stack view.

Drain answered 4/9, 2020 at 12:18 Comment(5)
This was my problem, and I didn't need to call layoutIfNeeded so I wonder if this should be the correct answer.Byproduct
This is the right answer for me. It took me a whole day debugging without understanding the cause. A simple block of code will do the job if self.isHidden != shouldHideView { self.isHidden = shouldHideView }Asante
Gosh, how did you find it! Saved me! ❤️Febrific
heroic, how you figured out the "never set isHidden = true on a view that is already hidden" part is beyond me!Lictor
AAAAaaargh, I've lost 6 hours debugging and this answer was the solution I neededAmphithecium
M
12

Swift 4 Extension:

// MARK: - Show hide animations in StackViews

extension UIView {

func hideAnimated(in stackView: UIStackView) {
    if !self.isHidden {
        UIView.animate(
            withDuration: 0.35,
            delay: 0,
            usingSpringWithDamping: 0.9,
            initialSpringVelocity: 1,
            options: [],
            animations: {
                self.isHidden = true
                stackView.layoutIfNeeded()
            },
            completion: nil
        )
    }
}

func showAnimated(in stackView: UIStackView) {
    if self.isHidden {
        UIView.animate(
            withDuration: 0.35,
            delay: 0,
            usingSpringWithDamping: 0.9,
            initialSpringVelocity: 1,
            options: [],
            animations: {
                self.isHidden = false
                stackView.layoutIfNeeded()
            },
            completion: nil
        )
    }
}
}
Mollusc answered 28/11, 2018 at 11:1 Comment(3)
For me the fix was to check for self.isHidden and not set the value if is is already the same.Eruption
that could easily be 1 function called toggleAnimated(in... , show: Bool) . since only one line changes :) plus it didnt work for me :sActivist
Yes, 2 functions would be syntactic sugar after making single funcMollusc
P
8

I want to share this function that is good for hiding and showing many views in UIStackView, because with all the code I used before didn't work smooth because one needs to removeAnimation from some layers:

extension UIStackView {
    public func make(viewsHidden: [UIView], viewsVisible: [UIView], animated: Bool) {
        let viewsHidden = viewsHidden.filter({ $0.superview === self })
        let viewsVisible = viewsVisible.filter({ $0.superview === self })

        let blockToSetVisibility: ([UIView], _ hidden: Bool) -> Void = { views, hidden in
            views.forEach({ $0.isHidden = hidden })
        }

        // need for smooth animation
        let blockToSetAlphaForSubviewsOf: ([UIView], _ alpha: CGFloat) -> Void = { views, alpha in
            views.forEach({ view in
                view.subviews.forEach({ $0.alpha = alpha })
            })
        }

        if !animated {
            blockToSetVisibility(viewsHidden, true)
            blockToSetVisibility(viewsVisible, false)
            blockToSetAlphaForSubviewsOf(viewsHidden, 1)
            blockToSetAlphaForSubviewsOf(viewsVisible, 1)
        } else {
            // update hidden values of all views
            // without that animation doesn't go
            let allViews = viewsHidden + viewsVisible
            self.layer.removeAllAnimations()
            allViews.forEach { view in
                let oldHiddenValue = view.isHidden
                view.layer.removeAllAnimations()
                view.layer.isHidden = oldHiddenValue
            }

            UIView.animate(withDuration: 0.3,
                           delay: 0.0,
                           usingSpringWithDamping: 0.9,
                           initialSpringVelocity: 1,
                           options: [],
                           animations: {

                            blockToSetAlphaForSubviewsOf(viewsVisible, 1)
                            blockToSetAlphaForSubviewsOf(viewsHidden, 0)

                            blockToSetVisibility(viewsHidden, true)
                            blockToSetVisibility(viewsVisible, false)
                            self.layoutIfNeeded()
            },
                           completion: nil)
        }
    }
}
Philosopher answered 12/4, 2019 at 9:29 Comment(1)
This also solved the issue of views not fading in/out. Beautiful!Piecedyed
P
8

Hope this saves others a few hours of frustration.

Animating hiding AND showing multiple UIStackView subviews at the same time is a mess.

In some cases a change to .isHidden in animation blocks displays correctly only until the next animation, then .isHidden is ignored. The only reliable trick I found for this is to repeat the .isHidden instructions in the completion block of the animation.

    let time = 0.3

    UIView.animate(withDuration: time, animations: {

        //shows
        self.googleSignInView.isHidden = false
        self.googleSignInView.alpha = 1
        self.registerView.isHidden = false
        self.registerView.alpha = 1

        //hides
        self.usernameView.isHidden = true
        self.usernameView.alpha = 0
        self.passwordView.isHidden = true
        self.passwordView.alpha = 0

        self.stackView.layoutIfNeeded()

    }) { (finished) in

        self.googleSignInView.isHidden = false
        self.registerView.isHidden = false
        self.usernameView.isHidden = true
        self.passwordView.isHidden = true
    }
Ptisan answered 7/8, 2020 at 22:1 Comment(1)
This is true! It was a mess until I changed the alpha during the animation and hide the view in the completion block. Thanks champ!Mosque
C
1

According to jimpic's answer, I wrote a simple function and solved the problem I had in showing and hiding views in stackView with animation.

func hide(_ vu: UIView) {
    if vu.isHidden == true {
        return
    } else {
        vu.isHidden = true
    }
}

func show(_ vu: UIView) {
    if vu.isHidden == true {
        vu.isHidden = false
    } else {
        return
    }
}

Use the above function:

UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut], animations: {
    self.hide(self.nameTextField)
})
Citrus answered 27/2, 2022 at 8:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.