为什么约束更改或动画不需要调用setNeedsUpdateConstraints?

时间:2017-12-14 23:24:32

标签: ios swift animation autolayout nslayoutconstraint

读数:

从此answer

这是接受的答案建议为视图更改设置动画:

_addBannerDistanceFromBottomConstraint.constant = 0

UIView.animate(withDuration: 5) {
    self.view.layoutIfNeeded()
}

为什么当我们更改框架时,我们会致电layoutIfNeeded。我们正在更改约束,因此(根据此other answer)我们不应该调用setNeedsUpdateConstraints吗?

同样,这位备受好评的answer说:

  

如果稍后发生某些变化,则会使您的某个内容无效   约束,你应该立即删除约束并调用   setNeedsUpdateConstraints

观察:

我确实尝试过使用它们。 使用setNeedsLayout我的视图在左侧正确设置动画

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func animate(_ sender: UIButton) {

        UIView.animate(withDuration: 1.8, animations: {
            self.centerXConstraint.isActive = !self.centerXConstraint.isActive
            self.view.setNeedsLayout()
            self.view.layoutIfNeeded()
        })
    }

    @IBOutlet weak var centerYConstraint: NSLayoutConstraint!
    @IBOutlet var centerXConstraint: NSLayoutConstraint!
}

然而,使用setNeedsUpdateConstraints 并不动画,它只是将视图快速移动到左侧

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func animate(_ sender: UIButton) {

        UIView.animate(withDuration: 1.8, animations: {
        self.centerXConstraint.isActive = !self.centerXConstraint.isActive
            self.view.setNeedsUpdateConstraints()
            self.view.updateConstraintsIfNeeded()    
        })
    }        

    @IBOutlet weak var centerYConstraint: NSLayoutConstraint!
    @IBOutlet var centerXConstraint: NSLayoutConstraint!
}

如果我不想要动画,那么使用view.setNeedsLayoutview.setNeedsUpdateConstraints将其移到左侧。但是:

  • 使用view.setNeedsLayout,点击我的按钮后,我的viewDidLayoutSubviews断点就到了。但永远不会达到updateViewConstraints断点。这让我感到困惑的是如何更新约束......
  • 使用view.setNeedsUpdateConstraints,点击按钮后,我的updateViewConstraints断点已到达,然后达到viewDidLayoutSubviews断点。这有意义,更新约束,然后调用layoutSubviews。

的问题:

根据我的读数:如果你改变约束然后让它变得有效你必须调用setNeedsUpdateConstraints,但根据我的观察结果是错误的。拥有以下代码足以动画:

self.view.setNeedsLayout()
self.view.layoutIfNeeded()

为什么?

然后我想也许不知何故,它通过其他方式更新了约束。所以我在override func updateViewConstraintsoverride func viewDidLayoutSubviews放置了一个断点,但只有viewDidLayoutSubviews到达了它的断点。

那么Auto Layout引擎如何管理呢?

2 个答案:

答案 0 :(得分:2)

setNeedsUpdateConstraints将根据您所做的更改更新将要更改的约束。例如,如果您的视图具有与水平距离约束的相邻视图,并且该邻居视图已被删除,则该约束现在无效。在这种情况下,您应该删除该约束并调用setNeedsUpdateConstraints。它基本上确保您的所有约束都有效。这不会重绘视图。您可以阅读更多相关信息here。 另一方面,setNeedsLayout标记重绘并将其置于动画块内的视图使绘图动画化。

答案 1 :(得分:2)

我会尽力解释一下:

首先要记住的是,更新约束不会导致视图布局立即更新。这是出于性能原因,因为将所有内容都放在一边可能需要花费时间,因此它会记录下来。需要进行的更改然后通过单个布局。

更进一步,你甚至可以在影响它们的东西发生变化时更新约束,但只是标记需要更新约束。即使更新约束本身(没有布置视图)也需要时间,而相同的约束可能会改变两种方式(即活动和非活动)。

现在考虑setNeedsUpdateConstraints()所做的就是标记在下一个布局传递之前需要重新计算视图的约束,因为关于它们的某些内容已经改变,它不会对影响进行任何约束更改目前的布局。然后,您应该实现自己的updateConstraints()方法版本,以根据当前应用程序状态等实际对约束进行必要的更改。

因此,当系统决定下一个布局传递时,应该发生任何调用了setNeedsUpdateConstraints()的东西(或者系统决定需要更新)将调用updateConstraints()的实现来进行这些更改。这将在布局完成之前自动发生。

现在setNeedsLayout()和layoutIfNeeded()类似,但用于控制实际的布局处理本身。

当影响视图布局的内容发生变化时,您可以调用setNeedsLayout(),以便该视图被标记为'在下一个布局过程中重新计算它的布局。因此,如果您直接更改约束(而不是使用setNeedsUpdateConstraints()和updateConstraints()),则可以调用setNeedsLayout()来指示视图布局已更改,并且需要在下一个布局过程中重新计算。

layoutIfNeeded()的作用是强制布局传递然后发生,而不是等待系统确定它何时应该发生。它强制根据当前的状态重新计算视图布局。另请注意,当您执行此操作时,任何已使用setNeedsUpdateConstraints()标记的内容都将首先调用它的updateConstraints()实现。

因此,在系统决定进行布局传递或您的应用调用layoutIfNeeded()之前,不会进行布局更改。

在实践中,您很少需要使用setNeedsUpdateConstraints()并实现您自己的updateConstraints()版本,除非某些内容非常复杂,您可以直接更新视图约束并使用setNeedsLayout()和layoutIfNeeded()。

因此,总结setNeedsUpdateConstraints不需要调用以使约束更改生效,实际上如果更改约束,它们将在系统决定布局传递的时间后自动生效。 / p>

当动画制作时,您希望稍微更多地控制正在发生的事情,因为您不希望立即更改布局,但会看到它随时间发生变化。所以为了简单起见,我们假设您有一个需要一秒钟的动画(一个视图从屏幕左侧移动到右侧)您更新约束以使视图从左向右移动但是如果这就是你的全部当系统决定是布局通行证的时候,它会从一个地方跳到另一个地方。所以你做了类似下面的事情(假设testView是self.view的子视图):

testView.leftPositionConstraint.isActive = false // always de-activate
testView.rightPositionConstraint.isActive = true // before activation
UIView.animate(withDuration: 1) {
    self.view.layoutIfNeeded()
}

让我们打破这一点:

首先,testView.leftPositionConstraint.isActive = false关闭约束,使视图保持在左手位置,但视图的布局尚未调整。

其次,testView.rightPositionConstraint.isActive = true启用约束,使视图保持在右手位置,但视图布局尚未调整。

然后你安排动画,并在每个时间片期间说出来。该动画调用self.view.layoutIfNeeded()。那么,每当动画更新时,强制执行self.view的布局传递,导致testView布局根据它在动画中的位置重新计算,即在动画的50%之后布局在说明(当前)布局和所需的新布局之间将是50%。

这样做动画就会生效。

总的来说:

setNeedsConstraint() - 调用以通知系统需要更新视图的约束,因为影响它们的内容已更改。在系统决定需要布局传递或用户强制执行布局传递之前,实际上不会更新约束。

updateConstraints() - 应该为视图实现这一点,以根据应用程序状态更新约束。

setNeedsLayout() - 这会通知系统影响视图布局的内容(可能是约束)已经改变,并且在下一次布局过程中需要重新计算布局。当时的布局没有任何变化。

layoutIfNeeded() - 现在为视图执行布局传递,而不是等待下一个系统调度传递。此时,视图及其子视图布局实际上将被重新计算。

编辑以希望更直接地回答这两个问题:

1)根据我的读数:如果你改变约束然后让它变得有效你必须调用setNeedsUpdateConstraints,但根据我的观察结果是错误的。拥有以下代码足以动画:

self.view.setNeedsLayout()
self.view.layoutIfNeeded()

为什么?

首先,你在阅读中误解了你根本不需要使用setNeedsUpdateConstraints。其次它们就足够了(假设它们在动画块中),因为setNeedsLayout()需要重新计算self.view的布局(因此它的子视图布局)并且& #39; layoutIfNeeded()'强制布局立即发生,因此如果在每次更新动画时都要在动画块内完成。

2)然后我想也许不知何故,它通过其他方式更新约束。所以我在override func updateViewConstraints上设置了一个断点,并覆盖了func viewDidLayoutSubviews,但只有viewDidLayoutSubviews到达了它的断点。

那么Auto Layout引擎如何管理呢?

最好用你原来的例子来展示:

_addBannerDistanceFromBottomConstraint.constant = 0

UIView.animate(withDuration: 5) {
    self.view.layoutIfNeeded()
}

第一行通过更改其常量(不需要使用setNeedsUpdateConstraints)来更新约束,但视图的布局(即它的实际帧位置和大小)尚未更改。当您在动画块中调用self.view.layoutIfNeeded()时,会根据动画的当前时间帧更新self.view的布局。此时,计算并调整视图的帧位置/大小。

我希望这更清楚,但实际上你的问题已在问题正文中得到详细解答,但可能过于详细的解释了。

现在为了帮助清晰,屏幕上的每个视图都有一个控制其大小和位置的框架。此框架可以通过属性手动设置,也可以使用您设置的约束进行计算。无论方法如何,确定视图位置和大小的帧都不是约束。约束仅用于计算视图的帧。

为了让它更清晰,我现在将添加两个实现相同但使用两种不同方法的示例。对于两者都存在testView,其具有将其置于主视图控制器视图的中心的约束(这些不会改变并且可以有效地忽略该示例)。该widthConstraint还有一个heightConstraint和一个testView,用于控制视图的高度和宽度。有expanded bool属性可确定testView是否已展开,还有testButton用于在展开状态和折叠状态之间切换。

这样做的第一种方式是:

class ViewController: UIViewController {
    @IBOutlet var testView: UIView!
    @IBOutlet var testButton: UIButton!
    @IBOutlet var widthConstraint: NSLayoutConstraint!
    @IBOutlet var heightConstraint: NSLayoutConstraint!

    var expanded = false

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func testButtonAction(_ sender: Any) {
        self.expanded = !self.expanded

        if self.expanded {
            self.widthConstraint.constant = 200
            self.heightConstraint.constant = 200
        } else {
            self.widthConstraint.constant = 100
            self.heightConstraint.constant = 100
        }
        self.view.layoutIfNeeded() // You only need to do this if you want the layout of the to be updated immediately.  If you leave it out the system will decide the best time to update the layout of the test view.
    }

}

并且在点击按钮时,切换expanded bool属性,然后通过更改其常量立即更新约束。然后调用layoutIfNeeded来立即重新计算testView的布局(从而更新显示),尽管可以省略这一点,让系统根据新的约束值重新计算布局它需要。

现在这是做同样事情的另一种方式:

class ViewController: UIViewController {
    @IBOutlet var testView: UIView!
    @IBOutlet var testButton: UIButton!
    @IBOutlet var widthConstraint: NSLayoutConstraint!
    @IBOutlet var heightConstraint: NSLayoutConstraint!

    var expanded = false

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func testButtonAction(_ sender: Any) {
        self.expanded = !self.expanded
        self.view.setNeedsUpdateConstraints()
    }

    override func updateViewConstraints() {
        super.updateViewConstraints()
        if self.expanded {
            self.widthConstraint.constant = 200
            self.heightConstraint.constant = 200
        } else {
            self.widthConstraint.constant = 100
            self.heightConstraint.constant = 100
        }
    }
}

此处按下按钮时,“扩展”按钮会被按下。 bool属性被切换,我们使用updateConstraintsIfNeeded向系统标记需要更新约束,然后才能重新计算布局(每当系统确定需要时)。当系统需要知道那些约束来重新计算视图的布局(它决定的东西)时,它会自动调用updateViewConstraints,并且约束会在此时更改为新值。

因此,如果您尝试这些,这两者基本上都是相同的,但它们有不同的用例。

使用方法1允许动画,因为(如上所述)你可以将layoutIfNeeded包裹在这样的动画块中:

    UIView.animate(withDuration: 5) {
        self.view.layoutIfNeeded()
    }

使系统根据自上次计算布局以来的约束更改,在初始布局和新布局之间进行动画处理。

使用方法2允许您推迟更改约束的需要,直到它们是绝对需要的,并且当您的约束非常复杂(很多它们)或者可能会发生许多可能需要的操作时,您可能希望这样做在需要进行下一次布局重新计算之前要更改的约束(以避免在不需要时不断更改约束)。这样做虽然你没有动画变化的能力,但这可能不是问题,因为约束的复杂性会使一切都慢慢爬行。

我希望这会有所帮助。