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

阅读:

从这个答案 :

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

_addBannerDistanceFromBottomConstraint.constant = 0 UIView.animate(withDuration: 5) { self.view.layoutIfNeeded() } 

当我们更改帧时,为什么我们调用layoutIfNeeded 。 我们正在改变约束,所以(根据这个其他答案 )我们不应该调用setNeedsUpdateConstraints吗?

同样,这个备受好评的答案说:

如果稍后更改某个约束无效,则应立即删除约束并调用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 updateViewConstraints放置了一个断点,并override func viewDidLayoutSubviews但只有viewDidLayoutSubviews到达了它的断点。

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

这是iOS开发人员常见的误解。

这是我自动布局的“黄金规则”之一:

不要理会“更新约束”

永远不需要调用以下任何方法:

  • setNeedsUpdateConstraints()
  • updateConstraintsIfNeeded()
  • updateConstraints()
  • updateViewConstraints()

除了非常罕见的情况,你有一个非常复杂的布局,这会减慢你的应用程序(或者你故意选择以非典型的方式实现布局更改)。

改变布局的首选方法

通常,当您想要更改布局时,您可以在按钮点击或触发更改的任何事件后直接激活/停用或更改布局约束,例如在按钮的操作方法中:

 @IBAction func toggleLayoutButtonTapped(_ button: UIButton) { toggleLayout() } func toggleLayout() { isCenteredLayout = !isCenteredLayout if isCenteredLayout { centerXConstraint.isActive = true } else { centerXConstraint.isActive = false } } 

正如Apple在其自动布局指南中所说:

在发生影响更改后立即更新约束几乎总是更清晰,更容易。 将这些更改推迟到以后的方法会使代码更复杂,更难理解。

您当然也可以在动画中包装此约束更改:首先执行约束更改,然后通过在动画闭包中调用layoutIfNeeded()来设置更改的动画:

 @IBAction func toggleLayoutButtonTapped(_ button: UIButton) { // 1. Perform constraint changes: toggleLayout() // 2. Animate the changes: UIView.animate(withDuration: 1.8, animations: { view.layoutIfNeeded() } } 

无论何时更改约束,系统都会自动调度延迟布局传递,这意味着系统将在不久的将来重新计算布局。 无需调用setNeedsUpdateConstraints()因为您只是自己更新(更改)约束! 需要更新的是布局,即所有视图的帧, 而不是任何其他约束。

无效原则

如前所述,iOS布局系统通常不会立即对约束更改做出反应,而只调度延迟布局传递。 这是出于性能原因。 想想这样:

当您购买杂货时,您将物品放入购物车但不立即支付。 相反,您将其他物品放入购物车,直到您觉得自己拥有所需的一切。 只有这样,您才能前往收银台并立即支付所有杂货。 它的效率更高。

由于这种延迟布局传递,需要一种特殊的机制来处理布局更改。 我称之为无效原则 。 这是一个两步机制:

  1. 您将某些内容标记为无效。
  2. 如果某些内容无效,您可以执行一些操作以使其再次有效。

在布局引擎方面,这对应于:

  1. setNeedsLayout()
  2. layoutIfNeeded()

  1. setNeedsUpdateConstraints()
  2. updateConstraintsIfNeeded()

第一对方法将导致立即 (非延迟)布局传递:首先使布局无效,然后如果布局无效(当然是布局),则立即重新计算布局。

通常,如果布局传递将在现在或几毫秒之后发生,您通常只会调用setNeedsLayout()来使布局无效,然后等待延迟布局传递。 这使您有机会对约束执行其他更改,然后稍后更新布局,但一次更新(→购物车)。

当您需要立即重新计算布局时,您只需要调用layoutIfNeeded() 。 当您需要根据新布局的结果帧执行一些其他计算时,可能就是这种情况。

第二对方法将导致立即调用updateConstraints() (在视图控制器上的视图或updateViewConstraints()上)。 但这是你通常不应该做的事情。

批量更改布局

只有当您的布局非常缓慢并且您的UI由于布局更改而感觉滞后时,您才能选择与上述方法不同的方法:不是直接更新约束以响应按钮点击,而是只是“注意”什么你想要改变和另一个“注意”你的约束需要更新。

 @IBAction func toggleLayoutButtonTapped(_ button: UIButton) { // 1. Make a note how you want your layout to change: isCenteredLayout = !isCenteredLayout // 2. Make a note that your constraints need to be updated (invalidate constraints): setNeedsUpdateConstraints() } 

这会调度延迟布局传递并确保在布局传递期间调用updateConstraints() / updateViewConstraints() 。 因此,您现在甚至可以执行其他更改并调用setNeedsUpdateConstraints()一千次 – 您的约束在下一个布局过程中仍然只会更新一次

现在,您覆盖updateConstraints() / updateViewConstraints()并根据您当前的布局状态执行必要的约束更改(即您在“1.”上面有“注意到”的内容):

 override func updateConstraints() { if isCenteredLayout { centerXConstraint.isActive = true } else { centerXConstraint.isActive = false } super.updateConstraints() } 

同样,如果布局非常慢并且您正在处理数百或数千个约束,这只是您的最后手段。 我从来没有在任何项目中使用updateConstraints()

我希望这会让事情变得更加清晰。

其他资源:

  • 自动布局 – 从导致到落后 :来自UIKonf 2017的演讲,主题:
    • “布局通行证”和
    • “更新约束”
  • Auto Layout Comprehendium™ :向下滚动到由我维护的“更新约束”部分
  • Apple的Auto Layout Guide :部分
    • “改变约束”
    • “递延布局通行证”

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

我会尽力解释一下:

首先要记住的是,更新约束不会导致视图布局立即更新。 这是出于性能原因,因为铺设所有内容可能需要花费时间,因此它“记录”需要进行的更改,然后执行单个布局。

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

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

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

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

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

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

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

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

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

动画制作时,您希望稍微更多地控制正在发生的事情,因为您不希望立即更改布局,而是看到它随时间变化。 所以为了简单起见,我们假设你有一个需要一秒钟的动画(一个视图从屏幕的左边移动到右边)你更新约束以使视图从左向右移动但是如果这就是你所做的全部它只会当系统决定布局通过的时候,从一个地方跳到另一个地方。 所以你做了类似下面的事情(假设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需要重新计算它的布局(因此它的子视图布局),’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还有一个widthConstraintheightConstraint ,用于控制视图的高度和宽度。 有一个expanded bool属性,用于确定是否展开testButton ,以及一个用于在展开状态和折叠状态之间切换的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允许您推迟更改约束的需要,直到它们是绝对需要的,并且当您的约束非常复杂(很多它们)或者可能发生许多可能需要约束的操作时,您可能希望这样做在需要进行下一次布局重新计算之前进行更改(以避免在不需要时不断更改约束)。 尽管你没有能力对变化进行动画处理,但这可能不是问题,因为约束的复杂性会让一切都变慢。

我希望这会有所帮助。