为什么约束更改或动画不需要调用setNeedsUpdateConstraints? [英] Why calling setNeedsUpdateConstraints isn't needed for constraint changes or animations?

查看:923
本文介绍了为什么约束更改或动画不需要调用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,动画:{
self.centerXConstraint.isActive =!self.centerXConstraint.isActive
self.view.setNeedsUpdateConstraints()
self.view.updateConstraintsIfNeeded()
})
}

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






如果我不这样做想要动画然后使用 view.setNeedsLayout view.setNeedsUpdateConstraints 将其移动到左侧。但是:




  • view.setNeedsLayout ,点击我的按钮后,我的 viewDidLayoutSubviews 达到了断点。但永远不会达到 updateViewConstraints 断点。这让我感到困惑的是约束如何更新...

  • 带有 view.setNeedsUpdateConstraints ,点击按钮后 updateViewConstraints 达到断点,然后 viewDidLayoutSubviews 达到断点。这有意义,更新约束,然后调用layoutSubviews。



问题:



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

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

为什么?



然后我想也许不知何故,它通过其他方式更新约束。所以我在覆盖func updateViewConstraints 覆盖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
}
}

As Apple将其置于自动布局指南


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


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

  @IBAction func toggleLayoutButtonTapped(_ button:UIButton){
// 1.执行约束更改:
toggleLayout()
// 2.动画更改:
UIView.animate(withDuration:1.8,动画:{
view.layoutIfNeeded()
}
}

每当您更改约束时,系统自动计划延迟布局传递,这意味着系统将重新计算近处的布局无需调用 setNeedsUpdateConstraints(),因为你只是 自己更新(更改)约束!需要更新什么d是布局,即所有视图的框架,任何其他约束。



无效原则



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


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


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


  1. 您将某些内容标记为无效。

  2. 如果某些内容无效,你执行一些动作让它再次有效。

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


  1. setNeedsLayout()

  2. layoutIfNeeded ()


  1. setNeedsUpdateConstraints()

  2. updateConstraintsIfNeeded()

第一对方法将导致立即(非延迟)布局pass:首先你使布局无效,然后你立即重新计算布局,如果它是无效的(当然是这样)。



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



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



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



批量更改你的布局



仅当你的布局真的慢,你的UI感觉很迟钝,因为你的布局变化你可以选择一种不同于上述方法的方法:而不是直接更新约束以响应按钮点击你只需做一个注意 你要改变什么,另一个注意你的约束需要更新。

  @IBAction func toggleLayoutButtonTapped(_按钮:UIButton){
// 1.记下您希望如何更改布局:
isCenteredLayout =!isCenteredLayout
// 2.记下您的约束需要更新(无效约束):
setNeedsUpdateConstraints()
}

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



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

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

super.updateConstraints()
}

同样,这只是你的最后的办法,如果布局真的很慢,你正在处理将有数百或数千个约束。我从不需要在我的任何项目中使用 updateConstraints()



<我希望这会让事情变得更清楚。



其他资源:




Readings:

From this answer:

This is what the accepted answer suggests to animate your view changes:

_addBannerDistanceFromBottomConstraint.constant = 0

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

Why do we call layoutIfNeeded when we aren't changing the frames. We are changing the constraints, so (according to this other answer) shouldn't we instead be calling setNeedsUpdateConstraints?

Similarly this highly viewed answer says:

If something changes later on that invalidates one of your constraints, you should remove the constraint immediately and call setNeedsUpdateConstraints

Observations:

I actually did try using them both. Using setNeedsLayout my view animates correctly to the left

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!
}

However using setNeedsUpdateConstraints doesn't animate, It just moves the view rapidly to the left.

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!
}


If I don't want animation then using either of view.setNeedsLayout or view.setNeedsUpdateConstraints move it to the left. However:

  • with view.setNeedsLayout, after my button is tapped, my viewDidLayoutSubviews breakpoint is reached. But the updateViewConstraints breakpoint is never reached. This leaves me baffled as to how the constraints are getting updated...
  • with view.setNeedsUpdateConstraints, after the button is tapped my updateViewConstraints breakpoint is reached and then the viewDidLayoutSubviews breakpoint is reached. This does make sense, the constraints are updated, then the layoutSubviews is called.

Questions:

Based on my readings: if you change constraints then for it to become effective you MUST call setNeedsUpdateConstraints, but based on my observations that's wrong. Having the following code was enough to animate:

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

WHY?

Then I thought maybe somehow under the hoods it's updating the constraints through other means. So I placed a breakpoint at override func updateViewConstraints and override func viewDidLayoutSubviews but only the viewDidLayoutSubviews reached its breakpoint.

So how is the Auto Layout engine managing this?

解决方案

This is a common misunderstanding among iOS developers.

Here's one of my "golden rules" for Auto Layout:

Don't bother about "updating constraints".

You never need to call any of these methods:

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

except for the very rare case that you have a tremendously complex layout which slows down your app (or you deliberately choose to implement layout changes in an atypical way).

The Preferred Way to Change Your Layout

Normally, when you want to change your layout, you would activate / deactivate or change layout constraints directly after a button tap or whichever event triggered the change, e.g. in a button's action method:

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

func toggleLayout() {
    isCenteredLayout = !isCenteredLayout

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

As Apple puts it in their 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.

You can of course also wrap this constraint change in an animation: You first perform the constraint change and then animate the changes by calling layoutIfNeeded() in the animation closure:

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

Whenever you change a constraint, the system automatically schedules a deferred layout pass, which means that the system will recompute the layout in the near future. No need to call setNeedsUpdateConstraints() because you just did update (change) the constraint yourself! What needs to be updated is the layout i.e. the frames of all your views, not any other constraint.

The Principle of Invalidation

As previously stated, the iOS layout system usually doesn't react immediately to constraint changes but only schedules a deferred layout pass. That's for performance reasons. Think of it like this:

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.

Due to this deferred layout pass there is a special mechanism needed to handle layout changes. I call it The Principle of Invalidation. It's a 2-step mechanism:

  1. You mark something as invalid.
  2. If something is invalid, you perform some action to make it valid again.

In terms of the layout engine this corresponds to:

  1. setNeedsLayout()
  2. layoutIfNeeded()

and

  1. setNeedsUpdateConstraints()
  2. updateConstraintsIfNeeded()

The first pair of methods will result in an immediate (not deferred) layout pass: First you invalidate the layout and then you recompute the layout immediately if it's invalid (which it is, of course).

Usually you don't bother if the layout pass will happen now or a couple of milliseconds later so you normally only call setNeedsLayout() to invalidate the layout and then wait for the deferred layout pass. This gives you the opportunity to perform other changes to your constraints and then update the layout slightly later but all at once (→ shopping cart).

You only need to call layoutIfNeeded() when you need the layout to be recomputed right now. That might be the case when you need to perform some other calculations based on the resulting frames of your new layout.

The second pair of methods will result in an immediate call of updateConstraints() (on a view or updateViewConstraints() on a view controller). But that's something you normally shouldn't do.

Changing Your Layout in a Batch

Only when your layout is really slow and your UI feels laggy due to your layout changes you can choose a different approach than the one stated above: Rather than updating a constraint directly in response to a button tap you just make a "note" of what you want to change and another "note" that your constraints need to be updated.

@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()
}

This schedules a deferred layout pass and ensures that updateConstraints() / updateViewConstraints() will be called during the layout pass. So you may now even perform other changes and call setNeedsUpdateConstraints() a thousand times – your constraints will still only be updated once during the next layout pass.

Now you override updateConstraints() / updateViewConstraints() and perform the necessary constraint changes based on your current layout state (i.e. what you have "noted" above in "1."):

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

    super.updateConstraints()
}

Again, this is only your last resort if the layout is really slow and you're dealing will hundreds or thousands of constraints. I have never needed to use updateConstraints() in any of my projects, yet.

I hope this make things a little clearer.

Additional resources:

这篇关于为什么约束更改或动画不需要调用setNeedsUpdateConstraints?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆