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

Hon*_*ney 13 animation ios autolayout nslayoutconstraint swift

阅读:

从这个答案:

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

_addBannerDistanceFromBottomConstraint.constant = 0

UIView.animate(withDuration: 5) {
    self.view.layoutIfNeeded()
}
Run Code Online (Sandbox Code Playgroud)

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!
}
Run Code Online (Sandbox Code Playgroud)

但是使用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!
}
Run Code Online (Sandbox Code Playgroud)

如果我不想要动画,那么使用其中任何一个view.setNeedsLayoutview.setNeedsUpdateConstraints向左移动它.然而:

  • view.setNeedsLayout,我的按钮被窃听后,我viewDidLayoutSubviews到达断点.但是updateViewConstraints从未达到断点.这让我感到困惑的是如何更新约束......
  • with view.setNeedsUpdateConstraints,点击按钮后,我的 updateViewConstraints断点到达,然后到达viewDidLayoutSubviews断点.这确实有意义,更新约束,然后调用layoutSubviews.

问题:

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

self.view.setNeedsLayout()
self.view.layoutIfNeeded()
Run Code Online (Sandbox Code Playgroud)

为什么?

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

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

Mis*_*cha 39

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

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

不要为"更新约束"而烦恼.

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

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

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

改变布局的首选方法

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

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

func toggleLayout() {
    isCenteredLayout = !isCenteredLayout

    if isCenteredLayout {
        centerXConstraint.isActive = true 
    } else {
        centerXConstraint.isActive = false
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

无论何时更改约束,系统都会自动计划延迟布局传递,这意味着系统将在不久的将来重新计算布局.无需打电话,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()
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

    super.updateConstraints()
}
Run Code Online (Sandbox Code Playgroud)

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

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

其他资源: