UIStackView 动画问题

Posted

技术标签:

【中文标题】UIStackView 动画问题【英文标题】:UIStackView Animation Issue 【发布时间】:2018-10-13 23:13:44 【问题描述】:

我在 stackView 中有一个 subStackView,当我隐藏/显示一个 subStackView 的内容时,动画会一直向上移动到其他堆栈视图:https://www.youtube.com/watch?v=vKXwX7OpkxU

这就是我创建 subStackView 的方式。我尝试了使用和不使用 clipToBounds 以及没有使用 translateAutoresizingMaskIntoConstraints。在动画部分也尝试了layoutIfNeeded。

let subStackView = UIStackView(arrangedSubviews: [self.innerView[0], self.innerView[1])
subStackView.translatesAutoresizingMaskIntoConstraints = false
subStackView.axis = .vertical
subStackView.distribution = .fillEqually
subStackView.alignment = .fill
subStackView.spacing = 0
subStackView.clipsToBounds = true

这个 subStackView 然后被加载到导致问题的 mainStackView 中。

【问题讨论】:

您的视频不再可用。 【参考方案1】:

解决问题的一种方法是更直接地控制紫色视图的显示和隐藏方式。你现在正在做的(我假设)是将isHidden 属性设置为true,然后让堆栈视图做它想做的任何事情。相反,让我们将紫色视图放在容器视图中,并将容器视图的高度设置为零。那么它可以是这样的:

使用容器视图而不是直接为紫色视图的高度设置动画的原因是,您可能(通常)有其他约束来控制紫色视图的高度,因此将其高度限制为零也会使您的控制台充满无法满足的约束错误。

这就是我为演示所做的。我做了一个“你好,世界!”紫色背景的标签。我将其高度限制为 80。我将标签放在容器视图中(只是一个普通的UIView)。我像往常一样将标签的顶部、前缘和后缘限制在容器视图中。我还将标签的底部边缘限制为容器视图,但优先级为 999*(低于默认的“必需”优先级 1000)。这意味着容器视图将非常努力地与标签的大小相同,但如果容器视图被强制改变高度,它会这样做而不影响标签的高度。

容器还设置了clipsToBounds,所以如果容器变得比标签短,标签的底部会被隐藏。

为了切换标签的可见性,我在将其高度设置为零的容器视图上激活或停用了所需的优先级高度约束。然后我要求窗口在动画块内布置其子级。

在我的演示中,我还将堆栈视图的 spacing 设置为 12。如果我只是让容器视图“可见”(不是 isHidden)高度为零,堆栈视图将放置 12 个点按钮后的空格,可能看起来不正确。在 ios 11 及更高版本上,我通过在“隐藏”容器时在按钮后设置自定义间距 0 来解决此问题,并在“显示”它时恢复默认间距。

在 iOS 11 之前的 iOS 版本上,我只是在隐藏动画完成后真正隐藏容器(将其 isHidden 设置为 true)。在运行显示动画之前,我会显示容器(将其 isHidden 设置为 false)。当间距立即消失或重新出现时,这会导致轻微的颠簸,但还不错。

处理堆栈视图间距会使代码变得更大,因此如果您不在堆栈视图中使用间距,则可以使用更简单的代码。

无论如何,这是我的代码:

class TaskletViewController: UIViewController 

    @IBAction func buttonWasTapped() 
        if detailContainerHideConstraint == nil 
            detailContainerHideConstraint = detailContainer.heightAnchor.constraint(equalToConstant: 0)
        
        let wantHidden = !(detailContainerHideConstraint?.isActive ?? false)

        if wantHidden 
            UIView.animate(withDuration: 0.25, animations: 
                if #available(iOS 11.0, *) 
                    self.stackView.setCustomSpacing(0, after: self.button)
                
                self.detailContainerHideConstraint?.isActive = true
                self.view.window?.layoutIfNeeded()
            , completion:  _ in
                if #available(iOS 11.0, *)   else 
                    self.detailContainer.isHidden = true
                
            )
         else 
            if #available(iOS 11.0, *)   else 
                detailContainer.isHidden = false
            
            UIView.animate(withDuration: 0.25, animations: 
                if #available(iOS 11.0, *) 
                    self.stackView.setCustomSpacing(self.stackView.spacing, after: self.button)
                
                self.detailContainerHideConstraint?.isActive = false
                self.view.window?.layoutIfNeeded()
            )
        
    

    override func loadView() 
        stackView.axis = .vertical
        stackView.spacing = 12
        stackView.translatesAutoresizingMaskIntoConstraints = false

        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = UIColor.green.withAlphaComponent(0.2)
        button.setTitle("Tap to toggle", for: .normal)
        button.addTarget(self, action: #selector(buttonWasTapped), for: .touchUpInside)
        button.setContentHuggingPriority(.required, for: .vertical)
        button.setContentCompressionResistancePriority(.required, for: .vertical)
        stackView.addArrangedSubview(button)

        detailLabel.translatesAutoresizingMaskIntoConstraints = false
        detailLabel.text = "Hello, world!"
        detailLabel.textAlignment = .center
        detailLabel.backgroundColor = UIColor.purple.withAlphaComponent(0.2)
        detailLabel.heightAnchor.constraint(equalToConstant: 80).isActive = true

        detailContainer.translatesAutoresizingMaskIntoConstraints = false
        detailContainer.clipsToBounds = true
        detailContainer.addSubview(detailLabel)
        let bottomConstraint = detailLabel.bottomAnchor.constraint(equalTo: detailContainer.bottomAnchor)
        bottomConstraint.priority = .init(999)
        NSLayoutConstraint.activate([
            detailLabel.topAnchor.constraint(equalTo: detailContainer.topAnchor),
            detailLabel.leadingAnchor.constraint(equalTo: detailContainer.leadingAnchor),
            detailLabel.trailingAnchor.constraint(equalTo: detailContainer.trailingAnchor),
            bottomConstraint
        ])
        stackView.addArrangedSubview(detailContainer)

        self.view = stackView
    

    private let stackView = UIStackView()
    private let button = UIButton(type: .roundedRect)
    private let detailLabel = UILabel()
    private let detailContainer = UIView()
    private var detailContainerHideConstraint: NSLayoutConstraint?

【讨论】:

天哪!今晚我会试试的:)

以上是关于UIStackView 动画问题的主要内容,如果未能解决你的问题,请参考以下文章

UIStackView 会工作吗?

使用 UINavigationController 时 UIStackView 无法设置底部约束

使用 UIStackView 时,UIImageView 在 UITableViewCell 内被挤压

Swift:扩展 UIStackView 以创建居中的标签列表

UIStackView,通过调整动画大小隐藏子视图

UIStackView 动画问题