如何禁用 UIStackView 默认 show/hide 动画?

How do I disable UIStackView default show/hide animation?

这可能是个奇怪的问题。当我们在其中显示或隐藏视图时,我们在 UIStackView 中免费获得动画。但是这种行为与我拥有的另一个动画相冲突。那么有没有办法禁用 UIStackView 的默认动画?

我希望它只显示或隐藏没有任何动画的子视图。如何使用 swift 实现此目的?

已更新

因此,如果我执行 view2.isHidden = true,StackView 将默认隐藏带有折叠动画的 View2。我希望它在没有动画的情况下强制隐藏

在没有任何其他信息的情况下,我猜测您正在按照以下思路做某事:

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

您获得堆栈视图动画是因为:

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

避免这种情况的各种方法,包括:

    // 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()
    

和:

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

这里有一个完整的例子来展示差异:

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

看起来像这样: