Search code examples
swiftanimationshow-hideuistackview

How do I disable UIStackView default show/hide animation?


This might be a strange question. We get the animation for free in a UIStackView when we show or hide a view inside it. But this behaviour is conflicting with another animation that I have. So is there a way to disable the default animations for UIStackView?

I want it to just show or hide the child view without any animation. How do I achieve this with swift?

updated

So if I do a view2.isHidden = true, the StackView will hide the View2 with a collapse animation by default. I want it to just force hide without the animation

enter image description here


Solution

  • 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:

    enter image description here