3

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

rickrvo
  • 543
  • 3
  • 17
  • 1
    Could you provide some code? If I am not mistaken UIStackView doesn't have any default animations. – Stanislav Marynych Aug 25 '21 at 00:14
  • It does. you can do a simple test and see the shrink animation when you hide a view inside the UIStackView. I'll update the question to try to make it more visual – rickrvo Aug 25 '21 at 10:52
  • 1
    @rickrvo - whether you think so or not, you ***do*** need to show us some code. Take a look at this example: https://pastebin.com/mtk1jhkP ... it adds two views to a vertical stack view, adds that stack view to another view, then tapping anywhere toggles `.isHidden` on the second arranged subview. No animation -- even when using Debug -> Slow Animations. – DonMag Aug 25 '21 at 14:01

1 Answers1

3

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

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • I'll accept this answer. my problem was way deeper and caused by a custom UI component I was using from the design team. but this is pretty clear and would be helpful for what I thought the problem would be. thanks – rickrvo Aug 31 '21 at 16:46
  • Great, if this helps you resolve your issue. For future reference, the more information you can supply in your question, the more likely you'll get a useful answer. – DonMag Aug 31 '21 at 17:43