111

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?

Infinity James
  • 4,667
  • 5
  • 23
  • 36

6 Answers6

184

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.

LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174
A Springham
  • 2,038
  • 1
  • 13
  • 10
  • 1
    You're a hero :D – Infinity James Sep 28 '17 at 10:04
  • 7
    Appropriate name as well 'Springham' – Infinity James Sep 28 '17 at 10:04
  • 4
    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. – Iulian Onofrei Oct 11 '17 at 16:36
  • I feel that `view.layoutIfNeeded()` is better than `stackView.layoutIfNeeded()` – SoftDesigner Apr 29 '18 at 11:18
  • 2
    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. https://developer.apple.com/documentation/uikit/uiview/1622507-layoutifneeded – A Springham Apr 30 '18 at 08:22
  • 1
    @SoftDesigner. You should call `layoutIfNeeded()` for the direct `superview` of the animated views which is `stackView` here. – Musa almatri May 02 '18 at 12:45
  • i'm calling `setNeedsLayout` before `layoutIfNeeded`, i'm using without any issue. – Ratul Sharker Dec 13 '18 at 06:58
  • @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 animate – A Springham Dec 13 '18 at 10:11
  • 13
    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 } – glemoulant Jan 29 '20 at 16:04
  • Thanks @glemoulant, I had the same issue and checking the property before setting it seems to do the trick. – Tieme Aug 18 '20 at 12:59
  • 1
    Didn't work for me. iOS 15. – heiko Nov 07 '21 at 19:47
52

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.

jimpic
  • 5,360
  • 2
  • 28
  • 37
  • 1
    This was my problem, and I didn't need to call `layoutIfNeeded` so I wonder if this should be the correct answer. – B Roy Dawson Oct 13 '20 at 18:51
  • 1
    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 }``` – Thanh-Nhon Nguyen Jan 22 '21 at 21:30
  • 5
    Gosh, how did you find it! Saved me! ❤️ – Alex Shubin Feb 04 '21 at 17:37
  • heroic, how you figured out the "never set isHidden = true on a view that is already hidden" part is beyond me! – James Wolfe Mar 18 '22 at 04:10
11

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
        )
    }
}
}
ergunkocak
  • 3,334
  • 1
  • 32
  • 31
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)
        }
    }
}
Paul T.
  • 4,938
  • 7
  • 45
  • 93
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
    }
Matjan
  • 3,591
  • 1
  • 33
  • 31
  • 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! – Dasoga Sep 16 '21 at 13:54
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)
})
Hamid Reza Ansari
  • 1,107
  • 13
  • 16