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

Why calling setNeedsUpdateConstraints isn't needed for constraint changes or animations?

读数:

从这个answer:

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

_addBannerDistanceFromBottomConstraint.constant = 0

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

当我们 更改帧时,为什么我们调用 layoutIfNeeded。我们正在更改约束,所以(根据此 other answer)我们不应该改为调用 setNeedsUpdateConstraints 吗?

同样,这个高浏览量 answer 说:

If something changes later on that invalidates one of your constraints, you should remove the constraint immediately and call 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 之一将其移至左侧。然而:

问题:

根据我的阅读:如果您更改约束,那么要使其生效,您必须调用 setNeedsUpdateConstraints,但根据我的观察,这是错误的。拥有以下代码足以制作动画:

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

为什么?

然后我想也许在某种程度上它正在通过其他方式更新约束。所以我在 override func updateViewConstraintsoverride func viewDidLayoutSubviews 处设置了一个断点,但只有 viewDidLayoutSubviews 到达了它的断点。

那么自动布局引擎是如何管理它的呢?

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

这是 iOS 开发人员的普遍误解。

这是我的 "golden rules" 自动布局之一:

别管"updating constraints".

永远不会需要调用这些方法中的任何一个:

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

除了 极少数情况下,您的布局非常复杂,这会降低您的应用程序速度(或者您故意选择以非典型方式实施布局更改)。

更改布局的首选方式

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

@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {
    toggleLayout()
}

func toggleLayout() {
    isCenteredLayout = !isCenteredLayout

    if isCenteredLayout {
        centerXConstraint.isActive = true 
    } else {
        centerXConstraint.isActive = false
    }
}

正如 Apple 在其 Auto Layout Guide 中所说:

It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred. Deferring these changes to a later method makes the code more complex and harder to understand.

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

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

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

无效原则

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

When you go shopping groceries, you put an item in your cart but you don't pay it immediately. Instead, you put other items in your cart until you feel like you got everything you need. Only then you proceed to the cashier and pay all your groceries at once. It's way more efficient.

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

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

就布局引擎而言,这对应于:

  1. setNeedsLayout()
  2. layoutIfNeeded()

  1. setNeedsUpdateConstraints()
  2. updateConstraintsIfNeeded()

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

通常您不会理会布局传递是现在发生还是几毫秒后发生,因此您通常只调用 setNeedsLayout() 使布局无效,然后等待延迟的布局传递。这使您有机会对您的约束执行其他更改,然后稍后更新布局,但一次全部更新(→ 购物车)。

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

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

批量更改布局

只有当您的布局 确实 缓慢并且您的 UI 由于布局更改而感到滞后时,您可以选择与上述方法不同的方法:而不是直接更新约束以响应按钮点击,您只需 "note" 您想要更改的内容和另一个 "note" 您的约束需要更新的内容。

@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.”中的 "noted"):

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

    super.updateConstraints()
}

同样,如果布局真的很慢并且您要处理成百上千个约束,这只是您最后的选择。我 从来没有 需要在我的任何项目中使用 updateConstraints()

我希望这能让事情更清楚一些。

其他资源:

我尽量简单的解释一下:

首先要记住的是,更新约束不会导致视图布局立即更新。这是出于性能原因,因为布置所有内容可能需要时间,因此 'makes note' 需要进行的更改然后进行单个布局传递。

更进一步,当影响约束的因素发生变化时,您甚至可以不更新约束,而只是标记需要更新约束。即使更新约束本身(不布置视图)也可能需要时间,并且相同的约束可能会双向改变(即活动和非活动)。

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

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

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

当影响视图布局的内容发生变化时,您可以调用 setNeedsLayout() 以便该视图 'flagged' 在下一次布局过程中重新计算其布局。因此,如果您直接更改约束(而不是使用 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 打开约束,将视图保持在右侧位置,但视图的布局仍未调整。

然后您安排动画并在每个 'time slice' 动画调用 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 和 override func viewDidLayoutSubviews 处放置了一个断点,但只有 viewDidLayoutSubviews 到达了它的断点。

那么自动布局引擎是如何管理这个的?

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

_addBannerDistanceFromBottomConstraint.constant = 0

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

第一行通过更改其常量(无需使用 setNeedsUpdateConstraints)更新了约束,但视图的布局(即它的实际框架位置和大小)尚未更改。当您在动画块中调用 self.view.layoutIfNeeded() 时,会根据动画的当前时间帧更新 self.view 的布局。正是在这一点上,视图的框架 position/size 被计算和调整。

我希望这能让它更清楚,但实际上你的问题已经在问题的正文中得到了详细的回答,也许它的解释太详细了。

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

为了让它更清楚,我现在将添加两个示例,它们使用两种不同的方法实现相同的目的。对于两者都有一个 testView ,它有将其放在主视图控制器视图中心的约束(这些不会改变,并且可以有效地忽略该示例)。还有一个 widthConstraintheightConstraint 对应 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
        }
    }
}

在这里,当点击按钮时, 'expanded' bool 属性 被切换,我们使用 updateConstraintsIfNeeded 向系统标记在布局之前需要更新约束可以重新计算(只要系统确定需要)。当系统需要知道这些约束以重新计算视图的布局(它决定的东西)时,它会自动调用 updateViewConstraints 并且此时约束将更改为其新值。

因此,如果您尝试一下,它们的作用基本相同,但它们有不同的用例。

使用方法 1 允许动画,因为(如前所述)您可以像这样将 layoutIfNeeded 包装在动画块中:

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

这会导致系统根据自上次计算布局以来的约束变化在初始布局和新布局之间进行动画处理。

使用方法 2 可以让您推迟更改约束的需要,直到它们是绝对需要的,并且当您的约束非常复杂(很多)或者可能会发生很多操作时,您会想要这样做可能需要在需要下一次布局重新计算之前更改约束(以避免在不需要时不断更改约束)。这样做虽然您缺乏动画更改的能力,但这可能不是问题,因为约束的复杂性无论如何都会使一切变得缓慢。

希望对您有所帮助。