UIStackView Hide View Animation

IosSwiftAnimationUikitUistackview

Ios Problem Overview


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?

Ios Solutions


Solution 1 - Ios

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.

Solution 2 - Ios

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.

Solution 3 - Ios

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
        )
    }
}
}

Solution 4 - Ios

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 the the .isHidden changes in animation blocks display correctly 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 section of the animation block.

    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
    }

Solution 5 - Ios

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)
        }
    }
}

Solution 6 - Ios

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)
})

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionInfinity JamesView Question on Stackoverflow
Solution 1 - IosA SpringhamView Answer on Stackoverflow
Solution 2 - IosjimpicView Answer on Stackoverflow
Solution 3 - IosergunkocakView Answer on Stackoverflow
Solution 4 - IosMatjanView Answer on Stackoverflow
Solution 5 - IosPaul T.View Answer on Stackoverflow
Solution 6 - IosHamid Reza AnsariView Answer on Stackoverflow