it-swarm.com.ru

Почему вызов 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.setNeedsLayout или view.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 управляет этим?

8
Honey

Это распространенное недоразумение среди разработчиков iOS.

Вот одно из моих «золотых правил» для Auto Layout:

Не беспокойтесь о "обновление ограничений".

Вам никогда необходимо вызвать любой из этих методов:

  • 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 :

Почти всегда проще и проще обновлять ограничение сразу после того, как произошло изменение. Откладывание этих изменений в более поздний метод делает код более сложным и трудным для понимания.

Конечно, вы также можете обернуть это изменение ограничения в анимацию: сначала вы выполняете изменение ограничения, а затем анимируете изменения, вызывая 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() для контроллера представления). Но это то, что вы обычно не должны делать.

Изменение макета в пакете

Только когда ваш макет является действительно медленным и ваш пользовательский интерфейс чувствует себя медленным из-за изменений макета, вы можете выбрать другой подход, чем тот, который указан выше: вместо того, чтобы обновлять ограничение непосредственно в ответ на нажатие кнопки, которое вы просто делаете «примечание» о том, что вы хотите изменить, и другое «примечание» о том, что ваши ограничения должны быть обновлены.

@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() в любом из моих проектов, пока.

Я надеюсь, что это немного прояснит ситуацию.

Дополнительные ресурсы:

22
Mischa

Я постараюсь объяснить это просто:

Первое, что нужно помнить, это то, что обновление ограничений не приводит к немедленному обновлению макета представлений. Это делается из соображений производительности, поскольку выкладка всего может занять некоторое время, поэтому он «запоминает» изменения, которые должны произойти, а затем выполняет один проход макета.

Сделав еще один шаг вперед, вы можете даже не обновлять ограничения, когда что-то влияющее на них меняется, а просто отметить, что ограничения должны быть обновлены. Даже обновление самих ограничений (без разметки представлений) может занять некоторое время, и одни и те же могут изменить оба пути (то есть активные и неактивные).

Теперь, принимая во внимание все, что 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()

setNeedsLayout() указывает, что self.view должен пересчитать свой макет (и, следовательно, его макеты подвидов), а layoutIfNeeded () заставляет макет немедленно выполняться и следовательно, если внутри блока анимации нужно делать при каждом обновлении анимации.

2) Тогда я подумал, может быть, как-то под капотами это обновляет ограничения другими способами. Поэтому я установил точку останова на переопределение func updateViewConstraints и переопределил func viewDidLayoutSubviews, но только точка зрения viewDidLayoutSubviews достигла его точки останова.

Так как же движок Auto Layout управляет этим?.

Лучше всего показать с вашим оригинальным примером этого:

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

Я надеюсь, что это проясняет ситуацию, но на самом деле ваши вопросы были даны подробные ответы в основной части вопроса, может быть, это было слишком подробное объяснение, хотя.

Теперь для ясности КАЖДЫЙ вид на экране имеет рамку, контролирующую как его размер, так и положение. Этот кадр либо устанавливается вручную через свойство, либо рассчитывается с использованием установленных ограничений. Независимо от метода, именно рамка определяет положение и размер представления, а не ограничения. Ограничения просто используются для вычисления кадра вида.

Чтобы попытаться сделать это еще яснее, я добавлю два примера, которые достигают одного и того же, но с использованием двух разных методов. Для обоих есть 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. } }

теперь вот еще один способ сделать то же самое:.

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

поэтому, если вы попробуете это, они оба сделают в основном то же самое, что и стоят, но для них есть разные варианты использования.

Использование метода 1 позволяет анимацию, потому что (как было отмечено) вы можете обернуть layoutIfNeeded в блок анимации следующим образом:.

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

надеюсь, это поможет больше.

Using method 2 allows you to postpone the need to change constraints until they are absolutely needed and you would want to do this when your constraints are really complex (lots of them) or there could be lots of actions that happen that could require the constraints to be changed before the next layout re-calculation is needed (to avoid continually changing constraints when not needed). Doing this though you lack the ability to animate the changes but that's probably not an issue as the complexity of the constraints would make everything slow to a crawl anyway.

I hope this helps more.

2
Upholder Of Truth

setNeedsUpdateConstraints обновит ограничения, которые будут изменены в зависимости от внесенного вами изменения. Например, если у вашего вида есть соседний вид, с которым существует ограничение горизонтального расстояния, и этот соседний вид был удален, ограничение теперь недопустимо. В этом случае вы должны удалить это ограничение и вызвать setNeedsUpdateConstraints. Это в основном гарантирует, что все ваши ограничения действительны. Это не будет перерисовывать вид. Вы можете прочитать больше об этом здесь .
setNeedsLayout, с другой стороны, помечает представление для перерисовки, а помещение его в блок анимации делает рисунок анимированным.

2
Santhosh R